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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
@@ -34,7 +35,7 @@ func conferencesPrequel(ctxt ui.AmContext) (string, any, error) {
|
||||
ctxt.SetRC(http.StatusNotFound)
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
@@ -70,3 +94,67 @@ func Conferences(ctxt ui.AmContext) (string, any, error) {
|
||||
ctxt.VarMap().Set("conferences", clist)
|
||||
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -204,6 +205,28 @@ func AmGetConferenceByAlias(alias string) (*Conference, error) {
|
||||
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.
|
||||
* Parameters:
|
||||
* cid - Community ID to get conferences for.
|
||||
|
||||
+160
-1
@@ -9,8 +9,13 @@
|
||||
// The database package contains database management and storage logic.
|
||||
package database
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Topic is the top-level structure detailing topics.
|
||||
type Topic struct {
|
||||
TopicId int32 `db:"topicid"`
|
||||
ConfId int32 `db:"confid"`
|
||||
@@ -25,6 +30,7 @@ type Topic struct {
|
||||
Name string `db:"name"`
|
||||
}
|
||||
|
||||
// TopicSettings contains per-user settings for topics, including the "last read" message pointer.
|
||||
type TopicSettings struct {
|
||||
TopicId int32 `db:"topicid"`
|
||||
Uid int32 `db:"uid"`
|
||||
@@ -34,3 +40,156 @@ type TopicSettings struct {
|
||||
LastPost *time.Time `db:"last_post"`
|
||||
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.POST("/comm/:cid/admin/logo", ui.AmWrap(EditCommunityLogo))
|
||||
e.GET("/comm/:cid/conf", ui.AmWrap(Conferences))
|
||||
e.GET("/comm/:cid/conf/:confid", ui.AmWrap(Topics))
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
+2
-2
@@ -1009,7 +1009,7 @@ INSERT INTO commmember (commid, uid, granted_lvl, locked)
|
||||
# (CONFID = 2)
|
||||
INSERT INTO confs (confid, createdate, read_lvl, post_lvl, create_lvl, hide_lvl, nuke_lvl, change_lvl,
|
||||
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.');
|
||||
INSERT INTO commtoconf (commid, confid, sequence) VALUES (2, 2, 10);
|
||||
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)
|
||||
INSERT INTO confs (confid, createdate, read_lvl, post_lvl, create_lvl, hide_lvl, nuke_lvl, change_lvl,
|
||||
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.');
|
||||
INSERT INTO commtoconf (commid, confid, sequence) VALUES (2, 3, 20);
|
||||
INSERT INTO confalias (confid, alias) VALUES (3, 'Test');
|
||||
|
||||
@@ -50,6 +50,7 @@ type AmContext interface {
|
||||
RC() int
|
||||
OutputType() string
|
||||
Parameter(string) string
|
||||
QueryParamInt(string, int) int
|
||||
RemoteIP() string
|
||||
ReplaceUser(*database.User)
|
||||
SaveSession() error
|
||||
@@ -241,6 +242,19 @@ func (c *amContext) Parameter(name string) string {
|
||||
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.
|
||||
func (c *amContext) RemoteIP() string {
|
||||
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.
|
||||
func extractCommunityLogo(a jet.Arguments) reflect.Value {
|
||||
rc := "/img/builtin/default-community.jpg"
|
||||
@@ -189,6 +198,7 @@ func SetupTemplates() {
|
||||
views.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION)
|
||||
views.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT)
|
||||
views.AddGlobal("GlobalConfig", config.GlobalConfig)
|
||||
views.AddGlobalFunc("iif", immediateIf)
|
||||
views.AddGlobalFunc("MakeIntRange", makeIntRange)
|
||||
views.AddGlobalFunc("MakeYearRange", makeYearRange)
|
||||
views.AddGlobalFunc("ExtractCommunityLogo", extractCommunityLogo)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<span class="text-lg pt-0.5 flex-shrink-0">🟣</span>
|
||||
<div class="flex-1">
|
||||
<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>
|
||||
<span class="text-gray-600 text-sm ml-2">- Latest activity: {{ DisplayActivity(c.LastUpdate, .) }}</span>
|
||||
</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