Landed the topic list page (no topics yet, so appearances are deceiving)
This commit is contained in:
+89
-1
@@ -12,6 +12,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.erbosoft.com/amy/amsterdam/database"
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
@@ -34,7 +35,7 @@ func conferencesPrequel(ctxt ui.AmContext) (string, any, error) {
|
|||||||
ctxt.SetRC(http.StatusNotFound)
|
ctxt.SetRC(http.StatusNotFound)
|
||||||
return ui.ErrorPage(ctxt, errors.New("this community does not use conferencing services"))
|
return ui.ErrorPage(ctxt, errors.New("this community does not use conferencing services"))
|
||||||
}
|
}
|
||||||
if comm.MembersOnly && !ctxt.IsMember() {
|
if comm.MembersOnly && !ctxt.IsMember() && !ctxt.TestPermission("Community.NoJoinRequired") {
|
||||||
ctxt.SetRC(http.StatusForbidden)
|
ctxt.SetRC(http.StatusForbidden)
|
||||||
return ui.ErrorPage(ctxt, errors.New("you are not a member of this community"))
|
return ui.ErrorPage(ctxt, errors.New("you are not a member of this community"))
|
||||||
}
|
}
|
||||||
@@ -45,6 +46,29 @@ func conferencesPrequel(ctxt ui.AmContext) (string, any, error) {
|
|||||||
return "", nil, nil
|
return "", nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func singleConferencePrequel(ctxt ui.AmContext) (string, any, error) {
|
||||||
|
cmd, arg, err := conferencesPrequel(ctxt)
|
||||||
|
if cmd != "" {
|
||||||
|
return cmd, arg, err
|
||||||
|
}
|
||||||
|
var conf *database.Conference
|
||||||
|
conf, err = database.AmGetConferenceByAliasInCommunity(ctxt.CurrentCommunity().Id, ctxt.URLParam("confid"))
|
||||||
|
if err != nil {
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
m, lvl, err := conf.Membership(ctxt.CurrentUser())
|
||||||
|
if err != nil {
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
myLevel := ctxt.EffectiveLevel()
|
||||||
|
if m && lvl > myLevel {
|
||||||
|
myLevel = lvl
|
||||||
|
}
|
||||||
|
ctxt.SetScratch("currentConference", conf)
|
||||||
|
ctxt.SetScratch("levelInConference", myLevel)
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
/* Conferences displayes the list of conferences in a community.
|
/* Conferences displayes the list of conferences in a community.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* ctxt - The AmContext for the request.
|
* ctxt - The AmContext for the request.
|
||||||
@@ -70,3 +94,67 @@ func Conferences(ctxt ui.AmContext) (string, any, error) {
|
|||||||
ctxt.VarMap().Set("conferences", clist)
|
ctxt.VarMap().Set("conferences", clist)
|
||||||
return "framed_template", "conflist.jet", err
|
return "framed_template", "conflist.jet", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Topics(ctxt ui.AmContext) (string, any, error) {
|
||||||
|
cmd, arg, err := singleConferencePrequel(ctxt)
|
||||||
|
if cmd != "" {
|
||||||
|
return cmd, arg, err
|
||||||
|
}
|
||||||
|
prefs, err := ctxt.CurrentUser().Prefs()
|
||||||
|
if err != nil {
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
comm := ctxt.CurrentCommunity()
|
||||||
|
conf := ctxt.GetScratch("currentConference").(*database.Conference)
|
||||||
|
myLevel := ctxt.GetScratch("levelInConference").(uint16)
|
||||||
|
if !conf.TestPermission("Conference.Read", myLevel) {
|
||||||
|
ctxt.SetRC(http.StatusForbidden)
|
||||||
|
return ui.ErrorPage(ctxt, errors.New("you are not permitted to read this conference"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get view and sort parameters from query, session, or defaults. Store to session.
|
||||||
|
trustSessionValues := false
|
||||||
|
if ctxt.IsSession("topic.conf") {
|
||||||
|
v := ctxt.GetSession("topic.conf").(int32)
|
||||||
|
if v == conf.ConfId {
|
||||||
|
trustSessionValues = true
|
||||||
|
} else {
|
||||||
|
ctxt.SetSession("topic.conf", conf.ConfId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view := database.TopicViewActive
|
||||||
|
if trustSessionValues && ctxt.IsSession("topic.view") {
|
||||||
|
view = ctxt.GetSession("topic.view").(int)
|
||||||
|
}
|
||||||
|
view = ctxt.QueryParamInt("view", view)
|
||||||
|
ctxt.SetSession("topic.view", view)
|
||||||
|
sort := database.TopicSortNumber
|
||||||
|
if trustSessionValues && ctxt.IsSession("topic.sort") {
|
||||||
|
sort = ctxt.GetSession("topic.sort").(int)
|
||||||
|
}
|
||||||
|
sort = ctxt.QueryParamInt("sort", sort)
|
||||||
|
ctxt.SetSession("topic.sort", sort)
|
||||||
|
|
||||||
|
topics, err := database.AmListTopics(conf.ConfId, ctxt.CurrentUserId(), view, sort, false)
|
||||||
|
if err != nil {
|
||||||
|
return ui.ErrorPage(ctxt, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tz := prefs.Location()
|
||||||
|
loc := prefs.Localizer()
|
||||||
|
fdate := make([]string, len(topics))
|
||||||
|
for i, t := range topics {
|
||||||
|
fdate[i] = loc.Strftime("%x %X", t.LastUpdate.In(tz))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt.VarMap().Set("conferenceName", conf.Name)
|
||||||
|
ctxt.VarMap().Set("urlBack", fmt.Sprintf("/comm/%s/conf", comm.Alias))
|
||||||
|
ctxt.VarMap().Set("urlStem", fmt.Sprintf("/comm/%s/conf/%s", comm.Alias, ctxt.URLParam("confid")))
|
||||||
|
ctxt.VarMap().Set("permalink", "TODO")
|
||||||
|
ctxt.VarMap().Set("view", view)
|
||||||
|
ctxt.VarMap().Set("sort", sort)
|
||||||
|
ctxt.VarMap().Set("topics", topics)
|
||||||
|
ctxt.VarMap().Set("formattedDate", fdate)
|
||||||
|
ctxt.VarMap().Set("amsterdam_pageTitle", "Topics in "+conf.Name)
|
||||||
|
return "framed_template", "topiclist.jet", nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -204,6 +205,28 @@ func AmGetConferenceByAlias(alias string) (*Conference, error) {
|
|||||||
return AmGetConference(int32(confid.(int)))
|
return AmGetConference(int32(confid.(int)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AmGetConferenceByAliasInCommunity returns a conference in a community given its alias.
|
||||||
|
* Parameters:
|
||||||
|
* cid - The community to look inside.
|
||||||
|
* alias - The alias to look up.
|
||||||
|
* Returns:
|
||||||
|
* Pointer to the conference, or nil.
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func AmGetConferenceByAliasInCommunity(cid int32, alias string) (*Conference, error) {
|
||||||
|
rs, err := amdb.Query(`SELECT c.confid FROM commtoconf c, confalias a WHERE c.confid = a.confid
|
||||||
|
AND c.commid = ? AND a.alias = ?`, cid, alias)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !rs.Next() {
|
||||||
|
return nil, errors.New("conference not found")
|
||||||
|
}
|
||||||
|
var confid int32
|
||||||
|
rs.Scan(&confid)
|
||||||
|
return AmGetConference(confid)
|
||||||
|
}
|
||||||
|
|
||||||
/* AmGetCommunityConferences returns all conferences for a given community.
|
/* AmGetCommunityConferences returns all conferences for a given community.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* cid - Community ID to get conferences for.
|
* cid - Community ID to get conferences for.
|
||||||
|
|||||||
+160
-1
@@ -9,8 +9,13 @@
|
|||||||
// The database package contains database management and storage logic.
|
// The database package contains database management and storage logic.
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Topic is the top-level structure detailing topics.
|
||||||
type Topic struct {
|
type Topic struct {
|
||||||
TopicId int32 `db:"topicid"`
|
TopicId int32 `db:"topicid"`
|
||||||
ConfId int32 `db:"confid"`
|
ConfId int32 `db:"confid"`
|
||||||
@@ -25,6 +30,7 @@ type Topic struct {
|
|||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TopicSettings contains per-user settings for topics, including the "last read" message pointer.
|
||||||
type TopicSettings struct {
|
type TopicSettings struct {
|
||||||
TopicId int32 `db:"topicid"`
|
TopicId int32 `db:"topicid"`
|
||||||
Uid int32 `db:"uid"`
|
Uid int32 `db:"uid"`
|
||||||
@@ -34,3 +40,156 @@ type TopicSettings struct {
|
|||||||
LastPost *time.Time `db:"last_post"`
|
LastPost *time.Time `db:"last_post"`
|
||||||
Subscribe bool `db:"subscribe"`
|
Subscribe bool `db:"subscribe"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TopicSummary is a smaller data structure that gets topic information to create the topic list display.
|
||||||
|
type TopicSummary struct {
|
||||||
|
TopicID int32
|
||||||
|
Number int16
|
||||||
|
Name string
|
||||||
|
Unread int32
|
||||||
|
Total int32
|
||||||
|
LastUpdate time.Time
|
||||||
|
Frozen bool
|
||||||
|
Archived bool
|
||||||
|
Subscribed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// View and sort constants for AmListTopics.
|
||||||
|
const (
|
||||||
|
TopicViewAll = 0
|
||||||
|
TopicViewNew = 1
|
||||||
|
TopicViewActive = 2
|
||||||
|
TopicViewAllVisible = 3
|
||||||
|
TopicViewHidden = 4
|
||||||
|
TopicViewArchive = 5
|
||||||
|
|
||||||
|
TopicSortID = 0
|
||||||
|
TopicSortNumber = 1
|
||||||
|
TopicSortName = 2
|
||||||
|
TopicSortUnread = 3
|
||||||
|
TopicSortTotal = 4
|
||||||
|
TopicSortDate = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
/* AmListTopics produces a list of topic summary information according to specific options.
|
||||||
|
* Parameters:
|
||||||
|
* confid - The ID of the conference to list topics in.
|
||||||
|
* uid - The UID of the user to consider the settings of.
|
||||||
|
* viewOption - One of the following constants:
|
||||||
|
* TopicViewAll - List all topics.
|
||||||
|
* TopicViewNew - List only visible topics with new messages.
|
||||||
|
* TopicViewActive - List only visible topics, with "active" ones coming first.
|
||||||
|
* TopicViewAllVisible - List only visible topics.
|
||||||
|
* TopicViewHidden - List only hidden topics (including archived ones).
|
||||||
|
* TopicViewArchive - List only archived, non-hidden topics.
|
||||||
|
* sortOption - One of the following constants:
|
||||||
|
* TopicSortID - Sort by topic ID.
|
||||||
|
* TopicSortNumber - Sort by topic number in the conference. May be negated to sort in reverse order.
|
||||||
|
* TopicSortName - Sort by topic name. May be negated to sort in reverse order.
|
||||||
|
* TopicSortUnread - Sort by number of unread messages. May be negated to sort in reverse order.
|
||||||
|
* TopicSortTotal - Sort by total number of messages. May be negated to sort in reverse order.
|
||||||
|
* TopicSortDate - Sort by last topic update date. May be negated to sort in reverse order.
|
||||||
|
* ignoreSticky - If false, sticky topics will precede nonsticky ones; if true, stickiness is ignored.
|
||||||
|
* Returns:
|
||||||
|
* List of TopicSummary pointers.
|
||||||
|
* Standard Go error status.
|
||||||
|
*/
|
||||||
|
func AmListTopics(confid int32, uid int32, viewOption int, sortOption int, ignoreSticky bool) ([]*TopicSummary, error) {
|
||||||
|
// Decode the viewOption into a WHERE clause.
|
||||||
|
var whereClause string
|
||||||
|
switch viewOption {
|
||||||
|
case TopicViewAll:
|
||||||
|
whereClause = ""
|
||||||
|
case TopicViewNew:
|
||||||
|
tail := "t.top_message > IFNULL(s.last_message,-1)"
|
||||||
|
if !ignoreSticky {
|
||||||
|
tail = "(t.sticky = 1 OR " + tail + ")"
|
||||||
|
}
|
||||||
|
whereClause = "t.archived = 0 AND IFNULL(s.hidden,0) = 0 AND " + tail
|
||||||
|
case TopicViewActive, TopicViewAllVisible:
|
||||||
|
whereClause = "t.archived = 0 AND IFNULL(s.hidden,0) = 0"
|
||||||
|
case TopicViewHidden:
|
||||||
|
whereClause = "IFNULL(s.hidden,0) = 1"
|
||||||
|
case TopicViewArchive:
|
||||||
|
whereClause = "t.archived = 1 AND IFNULL(s.hidden,0) = 0"
|
||||||
|
default:
|
||||||
|
return nil, errors.New("invalid view option specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the sortOption into an ORDER BY clause.
|
||||||
|
var reverse bool = false
|
||||||
|
if sortOption < 0 {
|
||||||
|
reverse = true
|
||||||
|
sortOption = -sortOption
|
||||||
|
}
|
||||||
|
var orderByClause string
|
||||||
|
switch sortOption {
|
||||||
|
case TopicSortID:
|
||||||
|
orderByClause = "t.topicid ASC"
|
||||||
|
case TopicSortNumber:
|
||||||
|
if reverse {
|
||||||
|
orderByClause = "t.num DESC"
|
||||||
|
} else {
|
||||||
|
orderByClause = "t.num ASC"
|
||||||
|
}
|
||||||
|
case TopicSortName:
|
||||||
|
if reverse {
|
||||||
|
orderByClause = "t.name DESC, t.num DESC"
|
||||||
|
} else {
|
||||||
|
orderByClause = "t.name ASC, t.num ASC"
|
||||||
|
}
|
||||||
|
case TopicSortUnread:
|
||||||
|
if reverse {
|
||||||
|
orderByClause = "unread ASC, t.num DESC"
|
||||||
|
} else {
|
||||||
|
orderByClause = "unread DESC, t.num ASC"
|
||||||
|
}
|
||||||
|
case TopicSortTotal:
|
||||||
|
if reverse {
|
||||||
|
orderByClause = "total ASC, t.num DESC"
|
||||||
|
} else {
|
||||||
|
orderByClause = "total DESC, t.num ASC"
|
||||||
|
}
|
||||||
|
case TopicSortDate:
|
||||||
|
if reverse {
|
||||||
|
orderByClause = "t.lastupdate ASC, t.num DESC"
|
||||||
|
} else {
|
||||||
|
orderByClause = "t.lastupdate DESC, t.num ASC"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, errors.New("invalid sort option specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the full SQL statement
|
||||||
|
var fullStatement strings.Builder
|
||||||
|
fullStatement.WriteString("SELECT t.topicid, t.num, t.name, (t.top_message - IFNULL(s.last_message,-1)) AS unread, ")
|
||||||
|
fullStatement.WriteString("(t.top_message + 1) AS total, t.lastupdate, t.frozen, t.archived, IFNULL(s.subscribe,0) AS subscribe, ")
|
||||||
|
fullStatement.WriteString("t.sticky, GREATEST(SIGN(t.top_message - IFNULL(s.last_message,-1)),0) AS newflag ")
|
||||||
|
fullStatement.WriteString("FROM topics t LEFT JOIN topicsettings s ON t.topicid = s.topicid AND s.uid = ? WHERE t.confid = ? ")
|
||||||
|
if whereClause != "" {
|
||||||
|
fullStatement.WriteString("AND ")
|
||||||
|
fullStatement.WriteString(whereClause)
|
||||||
|
}
|
||||||
|
fullStatement.WriteString(" ORDER BY ")
|
||||||
|
if ignoreSticky {
|
||||||
|
fullStatement.WriteString("t.sticky DESC, ")
|
||||||
|
}
|
||||||
|
if viewOption == TopicViewActive {
|
||||||
|
fullStatement.WriteString("newflag DESC, ")
|
||||||
|
}
|
||||||
|
fullStatement.WriteString(orderByClause)
|
||||||
|
|
||||||
|
// Execute and capture results
|
||||||
|
rs, err := amdb.Query(fullStatement.String(), uid, confid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rc := make([]*TopicSummary, 0)
|
||||||
|
for rs.Next() {
|
||||||
|
var rec TopicSummary
|
||||||
|
rs.Scan(&rec.TopicID, &rec.Number, &rec.Name, &rec.Unread, &rec.Total, &rec.LastUpdate, &rec.Frozen, &rec.Archived,
|
||||||
|
&rec.Subscribed)
|
||||||
|
rc = append(rc, &rec)
|
||||||
|
}
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ func setupEcho() *echo.Echo {
|
|||||||
e.GET("/comm/:cid/admin/logo", ui.AmWrap(CommunityLogoForm))
|
e.GET("/comm/:cid/admin/logo", ui.AmWrap(CommunityLogoForm))
|
||||||
e.POST("/comm/:cid/admin/logo", ui.AmWrap(EditCommunityLogo))
|
e.POST("/comm/:cid/admin/logo", ui.AmWrap(EditCommunityLogo))
|
||||||
e.GET("/comm/:cid/conf", ui.AmWrap(Conferences))
|
e.GET("/comm/:cid/conf", ui.AmWrap(Conferences))
|
||||||
|
e.GET("/comm/:cid/conf/:confid", ui.AmWrap(Topics))
|
||||||
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1009,7 +1009,7 @@ INSERT INTO commmember (commid, uid, granted_lvl, locked)
|
|||||||
# (CONFID = 2)
|
# (CONFID = 2)
|
||||||
INSERT INTO confs (confid, createdate, read_lvl, post_lvl, create_lvl, hide_lvl, nuke_lvl, change_lvl,
|
INSERT INTO confs (confid, createdate, read_lvl, post_lvl, create_lvl, hide_lvl, nuke_lvl, change_lvl,
|
||||||
delete_lvl, top_topic, name, descr)
|
delete_lvl, top_topic, name, descr)
|
||||||
VALUES (2, '2000-12-01 00:00:00', 6500, 6500, 6500, 52500, 52500, 52500, 58000, 0, 'General Discussion',
|
VALUES (2, '2000-12-01 00:00:00', 2500, 2500, 2500, 60500, 60500, 60500, 61000, 0, 'General Discussion',
|
||||||
'Your place for general discussion about the system and general topics.');
|
'Your place for general discussion about the system and general topics.');
|
||||||
INSERT INTO commtoconf (commid, confid, sequence) VALUES (2, 2, 10);
|
INSERT INTO commtoconf (commid, confid, sequence) VALUES (2, 2, 10);
|
||||||
INSERT INTO confalias (confid, alias) VALUES (2, 'General');
|
INSERT INTO confalias (confid, alias) VALUES (2, 'General');
|
||||||
@@ -1022,7 +1022,7 @@ INSERT INTO confmember (confid, uid, granted_lvl) VALUES (2, 2, 52500);
|
|||||||
# (CONFID = 3)
|
# (CONFID = 3)
|
||||||
INSERT INTO confs (confid, createdate, read_lvl, post_lvl, create_lvl, hide_lvl, nuke_lvl, change_lvl,
|
INSERT INTO confs (confid, createdate, read_lvl, post_lvl, create_lvl, hide_lvl, nuke_lvl, change_lvl,
|
||||||
delete_lvl, top_topic, name, descr)
|
delete_lvl, top_topic, name, descr)
|
||||||
VALUES (3, '2000-12-01 00:00:00', 6500, 6500, 6500, 52500, 52500, 52500, 58000, 0, 'Test Postings',
|
VALUES (3, '2000-12-01 00:00:00', 2500, 2500, 2500, 60500, 60500, 60500, 61000, 0, 'Test Postings',
|
||||||
'Use this conference to test the conferencing system.');
|
'Use this conference to test the conferencing system.');
|
||||||
INSERT INTO commtoconf (commid, confid, sequence) VALUES (2, 3, 20);
|
INSERT INTO commtoconf (commid, confid, sequence) VALUES (2, 3, 20);
|
||||||
INSERT INTO confalias (confid, alias) VALUES (3, 'Test');
|
INSERT INTO confalias (confid, alias) VALUES (3, 'Test');
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ type AmContext interface {
|
|||||||
RC() int
|
RC() int
|
||||||
OutputType() string
|
OutputType() string
|
||||||
Parameter(string) string
|
Parameter(string) string
|
||||||
|
QueryParamInt(string, int) int
|
||||||
RemoteIP() string
|
RemoteIP() string
|
||||||
ReplaceUser(*database.User)
|
ReplaceUser(*database.User)
|
||||||
SaveSession() error
|
SaveSession() error
|
||||||
@@ -241,6 +242,19 @@ func (c *amContext) Parameter(name string) string {
|
|||||||
return rc
|
return rc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueryParamInt returns the value of a query parameter as an integer, with a default.
|
||||||
|
func (c *amContext) QueryParamInt(name string, defval int) int {
|
||||||
|
s := c.echoContext.QueryParam(name)
|
||||||
|
if s == "" {
|
||||||
|
return defval
|
||||||
|
}
|
||||||
|
rc, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return defval
|
||||||
|
}
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
// RemoteIP returns the remote IP address.
|
// RemoteIP returns the remote IP address.
|
||||||
func (c *amContext) RemoteIP() string {
|
func (c *amContext) RemoteIP() string {
|
||||||
return c.echoContext.RealIP()
|
return c.echoContext.RealIP()
|
||||||
|
|||||||
@@ -96,6 +96,15 @@ func makeYearRange(a jet.Arguments) reflect.Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func immediateIf(a jet.Arguments) reflect.Value {
|
||||||
|
cond := a.Get(0).Convert(reflect.TypeFor[bool]()).Bool()
|
||||||
|
if cond {
|
||||||
|
return a.Get(1)
|
||||||
|
} else {
|
||||||
|
return a.Get(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// extractCommunityLogo extracts a community logo URL from a community.
|
// extractCommunityLogo extracts a community logo URL from a community.
|
||||||
func extractCommunityLogo(a jet.Arguments) reflect.Value {
|
func extractCommunityLogo(a jet.Arguments) reflect.Value {
|
||||||
rc := "/img/builtin/default-community.jpg"
|
rc := "/img/builtin/default-community.jpg"
|
||||||
@@ -189,6 +198,7 @@ func SetupTemplates() {
|
|||||||
views.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION)
|
views.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION)
|
||||||
views.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT)
|
views.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT)
|
||||||
views.AddGlobal("GlobalConfig", config.GlobalConfig)
|
views.AddGlobal("GlobalConfig", config.GlobalConfig)
|
||||||
|
views.AddGlobalFunc("iif", immediateIf)
|
||||||
views.AddGlobalFunc("MakeIntRange", makeIntRange)
|
views.AddGlobalFunc("MakeIntRange", makeIntRange)
|
||||||
views.AddGlobalFunc("MakeYearRange", makeYearRange)
|
views.AddGlobalFunc("MakeYearRange", makeYearRange)
|
||||||
views.AddGlobalFunc("ExtractCommunityLogo", extractCommunityLogo)
|
views.AddGlobalFunc("ExtractCommunityLogo", extractCommunityLogo)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<span class="text-lg pt-0.5 flex-shrink-0">🟣</span>
|
<span class="text-lg pt-0.5 flex-shrink-0">🟣</span>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<a href="/TODO/comm/{{ commAlias }}/conf/{{ c.AliasesQ()[0] }}"
|
<a href="/comm/{{ commAlias }}/conf/{{ c.AliasesQ()[0] }}"
|
||||||
class="text-blue-700 hover:text-blue-900 font-bold text-lg">{{ c.Name }}</a>
|
class="text-blue-700 hover:text-blue-900 font-bold text-lg">{{ c.Name }}</a>
|
||||||
<span class="text-gray-600 text-sm ml-2">- Latest activity: {{ DisplayActivity(c.LastUpdate, .) }}</span>
|
<span class="text-gray-600 text-sm ml-2">- Latest activity: {{ DisplayActivity(c.LastUpdate, .) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
{*
|
||||||
|
* Amsterdam Web Communities System
|
||||||
|
* Copyright (c) 2025 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">Topics in {{ conferenceName }}</h1>
|
||||||
|
<hr class="border-2 border-gray-400 w-4/5 mb-6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="max-w-6xl mb-6">
|
||||||
|
<div class="flex gap-2 flex-wrap mb-4">
|
||||||
|
<a href="{{ urlBack }}"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors">
|
||||||
|
Conference List
|
||||||
|
</a>
|
||||||
|
<a href="/TODO{{ urlStem }}/new_topic"
|
||||||
|
class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors">
|
||||||
|
Add Topic
|
||||||
|
</a>
|
||||||
|
<a href="/TODO/find"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors">
|
||||||
|
Find
|
||||||
|
</a>
|
||||||
|
<a href="/TODO{{ urlStem }}/manage"
|
||||||
|
class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded text-sm font-medium transition-colors">
|
||||||
|
Manage
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600">
|
||||||
|
<a href="{{ permalink }}"
|
||||||
|
class="text-blue-700 hover:text-blue-900">
|
||||||
|
[Permalink to this conference]
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Topics Table -->
|
||||||
|
<div class="max-w-6xl">
|
||||||
|
<div class="bg-white border border-gray-300 rounded-lg overflow-hidden">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-gray-100 border-b border-gray-300">
|
||||||
|
<tr>
|
||||||
|
{{ sp := iif(sort == 1, -1, 1) }}
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
<a href="{{ urlStem }}?sort={{ sp }}"
|
||||||
|
class="text-blue-700 hover:text-blue-900">#</a>
|
||||||
|
</th>
|
||||||
|
{{ sp = iif(sort == 2, -2, 2) }}
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
<a href="{{ urlStem }}?sort={{ sp }}"
|
||||||
|
class="text-blue-700 hover:text-blue-900">Topic Name</a>
|
||||||
|
</th>
|
||||||
|
{{ sp = iif(sort == 3, -3, 3) }}
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
<a href="{{ urlStem }}?sort={{ sp }}"
|
||||||
|
class="text-blue-700 hover:text-blue-900">New</a>
|
||||||
|
</th>
|
||||||
|
{{ sp = iif(sort == 4, -4, 4) }}
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
<a href="{{ urlStem }}?sort={{ sp }}"
|
||||||
|
class="text-blue-700 hover:text-blue-900">Total</a>
|
||||||
|
</th>
|
||||||
|
{{ sp = iif(sort == 5, -5, 5) }}
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
|
<a href="{{ urlStem }}?sort={{ sp }}"
|
||||||
|
class="text-blue-700 hover:text-blue-900">Last Response</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
{{ range i, t := topics }}
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
||||||
|
<a href="{{ urlStem }}/topic/{{ t.Number }}?new=1"
|
||||||
|
class="text-blue-700 hover:text-blue-900 font-medium">{{ t.Number }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
|
<a href="{{ urlStem }}/topic/{{ t.Number }}?new=1"
|
||||||
|
class="text-blue-700 hover:text-blue-900 font-medium">{{ t.Name }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-right">
|
||||||
|
<a href="{{ urlStem }}/topic/{{ t.Number }}?new=1"
|
||||||
|
class="text-blue-700 hover:text-blue-900">{{ t.Unread }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-right">
|
||||||
|
<a href="{{ urlStem }}/topic/{{ t.Number }}?st=0&en=-1"
|
||||||
|
class="text-blue-700 hover:text-blue-900">{{ t.Total }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-sm">
|
||||||
|
<a href="{{ urlStem }}/topic/{{ t.Number }}?new=1"
|
||||||
|
class="text-blue-700 hover:text-blue-900">{{ formattedDate[i] }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- View Filter -->
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<div class="inline-flex items-center gap-2 text-sm">
|
||||||
|
<span class="font-bold">[</span>
|
||||||
|
{{ if view == 1 }}
|
||||||
|
<span class="font-bold">New</span>
|
||||||
|
{{ else }}
|
||||||
|
<a href="{{ urlStem }}?view=1" class="text-blue-700 hover:text-blue-900">New</a>
|
||||||
|
{{ end }}
|
||||||
|
<span class="font-bold">|</span>
|
||||||
|
{{ if view == 2 }}
|
||||||
|
<span class="font-bold">Active</span>
|
||||||
|
{{ else }}
|
||||||
|
<a href="{{ urlStem }}?view=2" class="text-blue-700 hover:text-blue-900">Active</a>
|
||||||
|
{{ end }}
|
||||||
|
<span class="font-bold">|</span>
|
||||||
|
{{ if view == 3 }}
|
||||||
|
<span class="font-bold">All</span>
|
||||||
|
{{ else }}
|
||||||
|
<a href="{{ urlStem }}?view=3" class="text-blue-700 hover:text-blue-900">All</a>
|
||||||
|
{{ end }}
|
||||||
|
<span class="font-bold">|</span>
|
||||||
|
{{ if view == 4 }}
|
||||||
|
<span class="font-bold">Hidden</span>
|
||||||
|
{{ else }}
|
||||||
|
<a href="{{ urlStem }}?view=4" class="text-blue-700 hover:text-blue-900">Hidden</a>
|
||||||
|
{{ end }}
|
||||||
|
<span class="font-bold">|</span>
|
||||||
|
{{ if view == 5 }}
|
||||||
|
<span class="font-bold">Archived</span>
|
||||||
|
{{ else }}
|
||||||
|
<a href="{{ urlStem }}?view=5" class="text-blue-700 hover:text-blue-900">Archived</a>
|
||||||
|
{{ end }}
|
||||||
|
<span class="font-bold">]</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user