landed sidebox management
This commit is contained in:
+102
-6
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Amsterdam Web Communities System
|
* Amsterdam Web Communities System
|
||||||
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
@@ -15,13 +15,16 @@ import (
|
|||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Sidebox represents a user sidebox.
|
||||||
type Sidebox struct {
|
type Sidebox struct {
|
||||||
Uid int32 `db:"uid"`
|
Uid int32 `db:"uid"` // ID of the user
|
||||||
Boxid int32 `db:"boxid"`
|
Boxid int32 `db:"boxid"` // ID of the sidebox
|
||||||
Sequence int32 `db:"sequence"`
|
Sequence int32 `db:"sequence"` // sequence number of the sidebox
|
||||||
Param *string `db:"param"`
|
Param *string `db:"param"` // parameter string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SIDEBOX_SEQUENCE_SPACING = 100
|
||||||
|
|
||||||
// Known sidebox IDs.
|
// Known sidebox IDs.
|
||||||
const (
|
const (
|
||||||
SideboxIDCommunities = int32(1)
|
SideboxIDCommunities = int32(1)
|
||||||
@@ -29,9 +32,12 @@ const (
|
|||||||
SideboxIDOnlineUsers = int32(3)
|
SideboxIDOnlineUsers = int32(3)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// maxSidebox is the maximum sidebox index.
|
||||||
|
const maxSidebox = SideboxIDOnlineUsers
|
||||||
|
|
||||||
// copySideboxes copies sideboxes from one user to another.
|
// copySideboxes copies sideboxes from one user to another.
|
||||||
func copySideboxes(ctx context.Context, tx *sqlx.Tx, toUid int32, fromUid int32) error {
|
func copySideboxes(ctx context.Context, tx *sqlx.Tx, toUid int32, fromUid int32) error {
|
||||||
sbox := make([]Sidebox, 0, 3)
|
sbox := make([]Sidebox, 0, maxSidebox)
|
||||||
err := tx.SelectContext(ctx, &sbox, "SELECT * from sideboxes WHERE uid = ?", fromUid)
|
err := tx.SelectContext(ctx, &sbox, "SELECT * from sideboxes WHERE uid = ?", fromUid)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, sb := range sbox {
|
for _, sb := range sbox {
|
||||||
@@ -57,3 +63,93 @@ func AmGetSideboxes(ctx context.Context, uid int32) ([]*Sidebox, error) {
|
|||||||
err := amdb.SelectContext(ctx, &sboxes, "SELECT * FROM sideboxes WHERE uid = ? ORDER BY SEQUENCE", uid)
|
err := amdb.SelectContext(ctx, &sboxes, "SELECT * FROM sideboxes WHERE uid = ? ORDER BY SEQUENCE", uid)
|
||||||
return sboxes, err
|
return sboxes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AmReorderSideboxes changes the position of two sideboxes on the user's list.
|
||||||
|
func AmReorderSideboxes(ctx context.Context, uid int32, seq1, seq2 int32) error {
|
||||||
|
success := false
|
||||||
|
tx := amdb.MustBegin()
|
||||||
|
defer func() {
|
||||||
|
if !success {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err := tx.ExecContext(ctx, "UPDATE sideboxes SET sequence = -1 WHERE uid = ? AND sequence = ?", uid, seq1)
|
||||||
|
if err == nil {
|
||||||
|
_, err = tx.ExecContext(ctx, "UPDATE sideboxes SET sequence = ? WHERE uid = ? AND sequence = ?", seq1, uid, seq2)
|
||||||
|
if err == nil {
|
||||||
|
_, err = tx.ExecContext(ctx, "UPDATE sideboxes SET sequence = ? WHERE uid = ? AND sequence = -1", seq2, uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AmRemoveSidebox removes a sidebox from the user configuration.
|
||||||
|
func AmRemoveSidebox(ctx context.Context, uid int32, boxid int32) error {
|
||||||
|
success := false
|
||||||
|
tx := amdb.MustBegin()
|
||||||
|
defer func() {
|
||||||
|
if !success {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Get the old sequence number.
|
||||||
|
row := tx.QueryRowContext(ctx, "SELECT sequence FROM sideboxes WHERE uid = ? AND boxid = ?", uid, boxid)
|
||||||
|
var oldseq int32
|
||||||
|
err := row.Scan(&oldseq)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the sidebox entry.
|
||||||
|
_, err = tx.ExecContext(ctx, "DELETE FROM sideboxes WHERE uid = ? AND boxid = ?", uid, boxid)
|
||||||
|
if err == nil {
|
||||||
|
// Renumber the other sideboxes to close the gap.
|
||||||
|
_, err = tx.ExecContext(ctx, "UPDATE sideboxes SET sequence = sequence - ? WHERE uid = ? AND sequence > ?", SIDEBOX_SEQUENCE_SPACING, uid, oldseq)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AmAppendSidebox appends a new sidebox to the existing user's configuration.
|
||||||
|
func AmAppendSidebox(ctx context.Context, uid int32, boxid int32, param *string) error {
|
||||||
|
success := false
|
||||||
|
tx := amdb.MustBegin()
|
||||||
|
defer func() {
|
||||||
|
if !success {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
row := tx.QueryRowContext(ctx, "SELECT MAX(sequence) FROM sideboxes WHERE uid = ?", uid)
|
||||||
|
var topseq int32
|
||||||
|
err := row.Scan(&topseq)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, "INSERT INTO sideboxes (uid, boxid, sequence, param) VALUES (?, ?, ?, ?)",
|
||||||
|
uid, boxid, topseq+SIDEBOX_SEQUENCE_SPACING, param)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ _(italicized items can be deferred)_
|
|||||||
- ~~Conference Hotlist sidebox~~
|
- ~~Conference Hotlist sidebox~~
|
||||||
- ~~"New" flag on Conference Hotlist sidebox~~
|
- ~~"New" flag on Conference Hotlist sidebox~~
|
||||||
- ~~Manage on Conference Hotlist sidebox~~
|
- ~~Manage on Conference Hotlist sidebox~~
|
||||||
- Sidebox configuration
|
- ~~Sidebox configuration~~
|
||||||
- Topics view:
|
- Topics view:
|
||||||
- ~~Find~~
|
- ~~Find~~
|
||||||
- Manage:
|
- Manage:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.erbosoft.com/amy/amsterdam/config"
|
"git.erbosoft.com/amy/amsterdam/config"
|
||||||
@@ -72,6 +73,8 @@ func setupEcho() *echo.Echo {
|
|||||||
e.GET("/user/:uname", ui.AmWrap(ShowProfile))
|
e.GET("/user/:uname", ui.AmWrap(ShowProfile))
|
||||||
e.POST("/quick_email", ui.AmWrap(QuickEMail))
|
e.POST("/quick_email", ui.AmWrap(QuickEMail))
|
||||||
e.GET("/hotlist", ui.AmWrap(Hotlist))
|
e.GET("/hotlist", ui.AmWrap(Hotlist))
|
||||||
|
e.GET("/sideboxes", ui.AmWrap(ManageSideboxes))
|
||||||
|
e.POST("/sideboxes", ui.AmWrap(AddSidebox))
|
||||||
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
|
e.GET("/sysadmin", ui.AmWrap(SysAdminMenu))
|
||||||
e.GET("/create_comm", ui.AmWrap(CreateCommunityForm))
|
e.GET("/create_comm", ui.AmWrap(CreateCommunityForm))
|
||||||
e.POST("/create_comm", ui.AmWrap(CreateCommunity))
|
e.POST("/create_comm", ui.AmWrap(CreateCommunity))
|
||||||
@@ -175,8 +178,8 @@ func main() {
|
|||||||
closer = ui.SetupAmContext()
|
closer = ui.SetupAmContext()
|
||||||
defer closer()
|
defer closer()
|
||||||
|
|
||||||
// Set up to trap SIGINT and shut down gracefully
|
// Set up to trap SIGINT/SIGTERM and shut down gracefully
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
defer stop()
|
defer stop()
|
||||||
|
|
||||||
// Set up ampool.
|
// Set up ampool.
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.erbosoft.com/amy/amsterdam/config"
|
"git.erbosoft.com/amy/amsterdam/config"
|
||||||
"git.erbosoft.com/amy/amsterdam/database"
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
@@ -38,6 +41,7 @@ type SideboxRenderFunc func(context.Context, *database.User, *DisplaySidebox, *s
|
|||||||
|
|
||||||
// DisplaySidebox is the structure used to display a sidebox.
|
// DisplaySidebox is the structure used to display a sidebox.
|
||||||
type DisplaySidebox struct {
|
type DisplaySidebox struct {
|
||||||
|
BoxId int32 // box ID
|
||||||
Title string // title to display
|
Title string // title to display
|
||||||
TitleAnon string // title to display if user is anon
|
TitleAnon string // title to display if user is anon
|
||||||
TemplateName string // name of template to render
|
TemplateName string // name of template to render
|
||||||
@@ -124,6 +128,7 @@ var sideboxRegistry map[int32]*DisplaySidebox
|
|||||||
func init() {
|
func init() {
|
||||||
sideboxRegistry = make(map[int32]*DisplaySidebox)
|
sideboxRegistry = make(map[int32]*DisplaySidebox)
|
||||||
sb1 := DisplaySidebox{
|
sb1 := DisplaySidebox{
|
||||||
|
BoxId: database.SideboxIDCommunities,
|
||||||
Title: "Your Communities",
|
Title: "Your Communities",
|
||||||
TitleAnon: "Featured Communities",
|
TitleAnon: "Featured Communities",
|
||||||
TemplateName: "sb_comm.jet",
|
TemplateName: "sb_comm.jet",
|
||||||
@@ -131,6 +136,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
sideboxRegistry[database.SideboxIDCommunities] = &sb1
|
sideboxRegistry[database.SideboxIDCommunities] = &sb1
|
||||||
sb2 := DisplaySidebox{
|
sb2 := DisplaySidebox{
|
||||||
|
BoxId: database.SideboxIDConferences,
|
||||||
Title: "Your Conference Hotlist",
|
Title: "Your Conference Hotlist",
|
||||||
TitleAnon: "Featured Conferences",
|
TitleAnon: "Featured Conferences",
|
||||||
TemplateName: "sb_conf.jet",
|
TemplateName: "sb_conf.jet",
|
||||||
@@ -138,6 +144,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
sideboxRegistry[database.SideboxIDConferences] = &sb2
|
sideboxRegistry[database.SideboxIDConferences] = &sb2
|
||||||
sb3 := DisplaySidebox{
|
sb3 := DisplaySidebox{
|
||||||
|
BoxId: database.SideboxIDOnlineUsers,
|
||||||
Title: "Users Online",
|
Title: "Users Online",
|
||||||
TitleAnon: "Users Online",
|
TitleAnon: "Users Online",
|
||||||
TemplateName: "sb_online.jet",
|
TemplateName: "sb_online.jet",
|
||||||
@@ -205,8 +212,7 @@ func TopPage(ctxt ui.AmContext) (string, any) {
|
|||||||
disp := make([]*DisplaySidebox, 0, len(sboxes))
|
disp := make([]*DisplaySidebox, 0, len(sboxes))
|
||||||
rx := sbRender{ctxt: ctxt, id: 0}
|
rx := sbRender{ctxt: ctxt, id: 0}
|
||||||
for _, sb := range sboxes {
|
for _, sb := range sboxes {
|
||||||
dsb, ok := sideboxRegistry[sb.Boxid]
|
if dsb, ok := sideboxRegistry[sb.Boxid]; ok {
|
||||||
if ok {
|
|
||||||
rx.id = sb.Boxid
|
rx.id = sb.Boxid
|
||||||
err := dsb.Renderer(ctxt.Ctx(), ctxt.CurrentUser(), dsb, sb.Param, &rx)
|
err := dsb.Renderer(ctxt.Ctx(), ctxt.CurrentUser(), dsb, sb.Param, &rx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -274,3 +280,101 @@ func JumpToShortcut(ctxt ui.AmContext) (string, any) {
|
|||||||
}
|
}
|
||||||
return "redirect", targetURL
|
return "redirect", targetURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ManageSideboxes displays the "sidebox management" page.
|
||||||
|
* Parameters:
|
||||||
|
* ctxt - The AmContext for the request.
|
||||||
|
* Returns:
|
||||||
|
* Command string dictating what to be rendered.
|
||||||
|
* Data as a parameter for the command string.
|
||||||
|
*/
|
||||||
|
func ManageSideboxes(ctxt ui.AmContext) (string, any) {
|
||||||
|
if ctxt.CurrentUser().IsAnon {
|
||||||
|
return "error", ELOGIN
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the sideboxes from the database.
|
||||||
|
sboxes, err := database.AmGetSideboxes(ctxt.Ctx(), ctxt.CurrentUserId())
|
||||||
|
if err != nil {
|
||||||
|
return "error", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctxt.HasParameter("m") {
|
||||||
|
index := ctxt.QueryParamInt("m", -1)
|
||||||
|
if index == -1 {
|
||||||
|
return "error", EINVAL
|
||||||
|
}
|
||||||
|
delta := ctxt.QueryParamInt("n", 0)
|
||||||
|
if delta == 0 {
|
||||||
|
return "error", EINVAL
|
||||||
|
}
|
||||||
|
err = database.AmReorderSideboxes(ctxt.Ctx(), ctxt.CurrentUserId(), sboxes[index].Sequence, sboxes[index+delta].Sequence)
|
||||||
|
if err != nil {
|
||||||
|
return "error", err
|
||||||
|
}
|
||||||
|
tmp := sboxes[index]
|
||||||
|
sboxes[index] = sboxes[index+delta]
|
||||||
|
sboxes[index+delta] = tmp
|
||||||
|
} else if ctxt.HasParameter("d") {
|
||||||
|
index := ctxt.QueryParamInt("d", -1)
|
||||||
|
if index == -1 {
|
||||||
|
return "error", EINVAL
|
||||||
|
}
|
||||||
|
err = database.AmRemoveSidebox(ctxt.Ctx(), ctxt.CurrentUserId(), sboxes[index].Boxid)
|
||||||
|
if err != nil {
|
||||||
|
return "error", err
|
||||||
|
}
|
||||||
|
sboxes = append(sboxes[:index], sboxes[index+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the display list.
|
||||||
|
avail := maps.Clone(sideboxRegistry)
|
||||||
|
disp := make([]*DisplaySidebox, 0, len(sboxes))
|
||||||
|
for _, sb := range sboxes {
|
||||||
|
if dsb, ok := avail[sb.Boxid]; ok {
|
||||||
|
disp = append(disp, dsb)
|
||||||
|
delete(avail, sb.Boxid)
|
||||||
|
} else {
|
||||||
|
log.Errorf("TopPage: unknown sidebox ID %d", sb.Boxid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctxt.VarMap().Set("sideboxes", disp)
|
||||||
|
|
||||||
|
// Create the list of available sideboxes to add.
|
||||||
|
addList := make([]*DisplaySidebox, 0, len(avail))
|
||||||
|
for _, v := range avail {
|
||||||
|
addList = append(addList, v)
|
||||||
|
}
|
||||||
|
if len(addList) > 0 {
|
||||||
|
slices.SortFunc(addList, func(a, b *DisplaySidebox) int {
|
||||||
|
return strings.Compare(strings.ToLower(a.Title), strings.ToLower(b.Title))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ctxt.VarMap().Set("addList", addList)
|
||||||
|
|
||||||
|
ctxt.SetFrameTitle("Your Front Page Configuration")
|
||||||
|
return "framed", "sideboxes.jet"
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AddSidebox performs the "add sidebox" operation.
|
||||||
|
* Parameters:
|
||||||
|
* ctxt - The AmContext for the request.
|
||||||
|
* Returns:
|
||||||
|
* Command string dictating what to be rendered.
|
||||||
|
* Data as a parameter for the command string.
|
||||||
|
*/
|
||||||
|
func AddSidebox(ctxt ui.AmContext) (string, any) {
|
||||||
|
if ctxt.CurrentUser().IsAnon {
|
||||||
|
return "error", ELOGIN
|
||||||
|
}
|
||||||
|
|
||||||
|
newId, err := ctxt.FormFieldInt("sbid")
|
||||||
|
if err != nil {
|
||||||
|
return "error", err
|
||||||
|
}
|
||||||
|
err = database.AmAppendSidebox(ctxt.Ctx(), ctxt.CurrentUserId(), int32(newId), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "error", err
|
||||||
|
}
|
||||||
|
return "redirect", "/sideboxes"
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,14 @@
|
|||||||
<hr class="border-2 border-gray-400 w-4/5 mb-6">
|
<hr class="border-2 border-gray-400 w-4/5 mb-6">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Backlink -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<a class="text-blue-700 hover:text-blue-900 text-sm flex items-center gap-2 w-fit" href="/">
|
||||||
|
<span>←</span>
|
||||||
|
Return to Front Page
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Hotlist Table -->
|
<!-- Hotlist Table -->
|
||||||
<div class="max-w-4xl mb-8">
|
<div class="max-w-4xl mb-8">
|
||||||
<div class="bg-white border border-gray-300 rounded-lg overflow-hidden">
|
<div class="bg-white border border-gray-300 rounded-lg overflow-hidden">
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
{*
|
||||||
|
* Amsterdam Web Communities System
|
||||||
|
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*}
|
||||||
|
<div class="p-4">
|
||||||
|
<!-- Page Title -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-blue-800 text-4xl font-bold mb-2">Your Front Page Configuration</h1>
|
||||||
|
<hr class="border-2 border-gray-400 w-4/5 mb-6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backlink -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<a class="text-blue-700 hover:text-blue-900 text-sm flex items-center gap-2 w-fit" href="/">
|
||||||
|
<span>←</span>
|
||||||
|
Return to Front Page
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebox Table -->
|
||||||
|
<div class="max-w-4xl mb-8">
|
||||||
|
<div class="bg-white border border-gray-300 rounded-lg overflow-hidden">
|
||||||
|
{{ if len(sideboxes) > 0 }}
|
||||||
|
<table class="w-full">
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
{{ range i, sb := sideboxes }}
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-center w-12">
|
||||||
|
{{ if i < (len(sideboxes) - 1) }}
|
||||||
|
<a href="/sideboxes?m={{ i }}&n=1" class="text-2xl hover:scale-125 inline-block transition-transform"
|
||||||
|
title="Move Down">⬇️</a>
|
||||||
|
{{ else }}
|
||||||
|
<span class="text-2xl text-gray-300"> </span>
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-center w-12">
|
||||||
|
{{ if i > 0 }}
|
||||||
|
<a href="/sideboxes?m={{ i }}&n=-1" class="text-2xl hover:scale-125 inline-block transition-transform"
|
||||||
|
title="Move Up">⬆️</a>
|
||||||
|
{{ else }}
|
||||||
|
<span class="text-2xl text-gray-300"> </span>
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-center w-12">
|
||||||
|
<a href="/sideboxes?d={{ i }}" class="text-2xl hover:scale-125 inline-block transition-transform"
|
||||||
|
title="Remove">❌</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
|
<div class="font-bold text-black">{{ sb.Title }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{ else }}
|
||||||
|
<span class="text-gray-800"><i>No sideboxes currently configured.</i></span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<div class="max-w-4xl mb-6">
|
||||||
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<h2 class="text-blue-800 font-bold text-lg mb-3">How to update your front page configuration:</h2>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-2xl">⬇️</span>
|
||||||
|
<span class="text-gray-700">Click this symbol to move the specified sidebox down on the front page.</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-2xl">⬆️</span>
|
||||||
|
<span class="text-gray-700">Click this symbol to move the specified sidebox up on the front page.</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="text-2xl">❌</span>
|
||||||
|
<span class="text-gray-700">Click this symbol to remove the specified sidebox from the front page.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if len(addList) > 0 }}
|
||||||
|
<form method="POST" action="/sideboxes">
|
||||||
|
<div class="justify-between mb-6">
|
||||||
|
<label for="sbid" class="w-64 text-right pr-4 text-black text-sm">Add sidebox:</label>
|
||||||
|
<select id="sbid" name="sbid" class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
{{ range _, sb := addList }}
|
||||||
|
<option value="{{ sb.BoxId }}">{{ sb.Title }}</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
<button type="submit" name="add" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded font-medium transition-colors">Add</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
</div>
|
||||||
+1
-1
@@ -65,7 +65,7 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if !.CurrentUser().IsAnon }}
|
{{ if !.CurrentUser().IsAnon }}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a href="/TODO/config-sideboxes"
|
<a href="/sideboxes"
|
||||||
class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors inline-block">Configure</a>
|
class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors inline-block">Configure</a>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
Reference in New Issue
Block a user