landed sidebox management
This commit is contained in:
+102
-6
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* 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
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
@@ -15,13 +15,16 @@ import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// Sidebox represents a user sidebox.
|
||||
type Sidebox struct {
|
||||
Uid int32 `db:"uid"`
|
||||
Boxid int32 `db:"boxid"`
|
||||
Sequence int32 `db:"sequence"`
|
||||
Param *string `db:"param"`
|
||||
Uid int32 `db:"uid"` // ID of the user
|
||||
Boxid int32 `db:"boxid"` // ID of the sidebox
|
||||
Sequence int32 `db:"sequence"` // sequence number of the sidebox
|
||||
Param *string `db:"param"` // parameter string
|
||||
}
|
||||
|
||||
const SIDEBOX_SEQUENCE_SPACING = 100
|
||||
|
||||
// Known sidebox IDs.
|
||||
const (
|
||||
SideboxIDCommunities = int32(1)
|
||||
@@ -29,9 +32,12 @@ const (
|
||||
SideboxIDOnlineUsers = int32(3)
|
||||
)
|
||||
|
||||
// maxSidebox is the maximum sidebox index.
|
||||
const maxSidebox = SideboxIDOnlineUsers
|
||||
|
||||
// copySideboxes copies sideboxes from one user to another.
|
||||
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)
|
||||
if err == nil {
|
||||
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)
|
||||
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~~
|
||||
- ~~"New" flag on Conference Hotlist sidebox~~
|
||||
- ~~Manage on Conference Hotlist sidebox~~
|
||||
- Sidebox configuration
|
||||
- ~~Sidebox configuration~~
|
||||
- Topics view:
|
||||
- ~~Find~~
|
||||
- Manage:
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/config"
|
||||
@@ -72,6 +73,8 @@ func setupEcho() *echo.Echo {
|
||||
e.GET("/user/:uname", ui.AmWrap(ShowProfile))
|
||||
e.POST("/quick_email", ui.AmWrap(QuickEMail))
|
||||
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("/create_comm", ui.AmWrap(CreateCommunityForm))
|
||||
e.POST("/create_comm", ui.AmWrap(CreateCommunity))
|
||||
@@ -175,8 +178,8 @@ func main() {
|
||||
closer = ui.SetupAmContext()
|
||||
defer closer()
|
||||
|
||||
// Set up to trap SIGINT and shut down gracefully
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
// Set up to trap SIGINT/SIGTERM and shut down gracefully
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
// Set up ampool.
|
||||
|
||||
@@ -12,8 +12,11 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/config"
|
||||
"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.
|
||||
type DisplaySidebox struct {
|
||||
BoxId int32 // box ID
|
||||
Title string // title to display
|
||||
TitleAnon string // title to display if user is anon
|
||||
TemplateName string // name of template to render
|
||||
@@ -124,6 +128,7 @@ var sideboxRegistry map[int32]*DisplaySidebox
|
||||
func init() {
|
||||
sideboxRegistry = make(map[int32]*DisplaySidebox)
|
||||
sb1 := DisplaySidebox{
|
||||
BoxId: database.SideboxIDCommunities,
|
||||
Title: "Your Communities",
|
||||
TitleAnon: "Featured Communities",
|
||||
TemplateName: "sb_comm.jet",
|
||||
@@ -131,6 +136,7 @@ func init() {
|
||||
}
|
||||
sideboxRegistry[database.SideboxIDCommunities] = &sb1
|
||||
sb2 := DisplaySidebox{
|
||||
BoxId: database.SideboxIDConferences,
|
||||
Title: "Your Conference Hotlist",
|
||||
TitleAnon: "Featured Conferences",
|
||||
TemplateName: "sb_conf.jet",
|
||||
@@ -138,6 +144,7 @@ func init() {
|
||||
}
|
||||
sideboxRegistry[database.SideboxIDConferences] = &sb2
|
||||
sb3 := DisplaySidebox{
|
||||
BoxId: database.SideboxIDOnlineUsers,
|
||||
Title: "Users Online",
|
||||
TitleAnon: "Users Online",
|
||||
TemplateName: "sb_online.jet",
|
||||
@@ -205,8 +212,7 @@ func TopPage(ctxt ui.AmContext) (string, any) {
|
||||
disp := make([]*DisplaySidebox, 0, len(sboxes))
|
||||
rx := sbRender{ctxt: ctxt, id: 0}
|
||||
for _, sb := range sboxes {
|
||||
dsb, ok := sideboxRegistry[sb.Boxid]
|
||||
if ok {
|
||||
if dsb, ok := sideboxRegistry[sb.Boxid]; ok {
|
||||
rx.id = sb.Boxid
|
||||
err := dsb.Renderer(ctxt.Ctx(), ctxt.CurrentUser(), dsb, sb.Param, &rx)
|
||||
if err != nil {
|
||||
@@ -274,3 +280,101 @@ func JumpToShortcut(ctxt ui.AmContext) (string, any) {
|
||||
}
|
||||
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">
|
||||
</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 -->
|
||||
<div class="max-w-4xl mb-8">
|
||||
<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 }}
|
||||
{{ if !.CurrentUser().IsAnon }}
|
||||
<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>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
Reference in New Issue
Block a user