package boards2
import (
"net/url"
"strconv"
"strings"
"time"
"gno.land/p/gnoland/boards"
"gno.land/p/jeronimoalbi/mdform"
"gno.land/p/jeronimoalbi/pager"
"gno.land/p/leon/svgbtn"
"gno.land/p/moul/md"
"gno.land/p/moul/mdtable"
"gno.land/p/nt/mux/v0"
)
const (
pageSizeDefault = 6
pageSizeReplies = 10
)
const menuManageBoard = "manageBoard"
var (
createBoardURI = gRealmPath + ":create-board"
adminUsersURI = gRealmPath + ":admin-users"
helpURI = gRealmPath + ":help"
)
func Render(path string) string {
var (
b strings.Builder
router = mux.NewRouter()
)
router.HandleFunc("", renderBoardsList)
router.HandleFunc("help", renderHelp)
router.HandleFunc("admin-users", renderMembers)
router.HandleFunc("create-board", renderCreateBoard)
router.HandleFunc("{board}", renderBoard)
router.HandleFunc("{board}/members", renderMembers)
router.HandleFunc("{board}/invites", renderInvites)
router.HandleFunc("{board}/banned-users", renderBannedUsers)
router.HandleFunc("{board}/create-thread", renderCreateThread)
router.HandleFunc("{board}/invite-member", renderInviteMember)
router.HandleFunc("{board}/{thread}", renderThread)
router.HandleFunc("{board}/{thread}/flag", renderFlagPost)
router.HandleFunc("{board}/{thread}/flagging-reasons", renderFlaggingReasonsPost)
router.HandleFunc("{board}/{thread}/reply", renderReplyPost)
router.HandleFunc("{board}/{thread}/edit", renderEditThread)
router.HandleFunc("{board}/{thread}/repost", renderRepostThread)
router.HandleFunc("{board}/{thread}/{reply}", renderReply)
router.HandleFunc("{board}/{thread}/{reply}/flag", renderFlagPost)
router.HandleFunc("{board}/{thread}/{reply}/flagging-reasons", renderFlaggingReasonsPost)
router.HandleFunc("{board}/{thread}/{reply}/reply", renderReplyPost)
router.HandleFunc("{board}/{thread}/{reply}/edit", renderEditReply)
router.NotFoundHandler = func(res *mux.ResponseWriter, _ *mux.Request) {
res.Write(md.Blockquote("Path not found"))
}
// Render common realm header before resolving render path
if gNotice != "" {
b.WriteString(infoAlert("Notice", gNotice))
}
// Render view for current path
b.WriteString(router.Render(path))
return b.String()
}
func renderHelp(res *mux.ResponseWriter, _ *mux.Request) {
res.Write(md.H1("Boards Help"))
if gHelp != "" {
res.Write(gHelp)
return
}
link := gRealmLink.Call("SetHelp", "content", "")
res.Write(md.H3("Help content has not been uploaded"))
res.Write("Do you want to " + md.Link("upload boards help", link) + "?")
}
func renderBoardsList(res *mux.ResponseWriter, req *mux.Request) {
res.Write(md.H1("Boards"))
renderBoardListMenu(res, req)
res.Write(md.HorizontalRule())
if gListedBoardsByID.Size() == 0 {
res.Write(md.H3("Currently there are no boards"))
res.Write("Be the first to " + md.Link("create a new board", createBoardURI) + "!")
return
}
p, err := pager.New(req.RawPath, gListedBoardsByID.Size(), pager.WithPageSize(pageSizeDefault))
if err != nil {
panic(err)
}
render := func(_ string, v any) bool {
board := v.(*boards.Board)
userLink := userLink(board.Creator)
date := board.CreatedAt.Format(dateFormat)
res.Write(md.H6(md.Link(board.Name, makeBoardURI(board))))
res.Write("Created by " + userLink + " on " + date + ", #" + board.ID.String() + " \n")
status := strconv.Itoa(board.Threads.Size()) + " threads"
if board.Readonly {
status += ", read-only"
}
res.Write(md.Bold(status) + "\n\n")
return false
}
res.Write("Sort by: ")
r := parseRealmPath(req.RawPath)
if r.Query.Get("order") == "desc" {
r.Query.Set("order", "asc")
res.Write(md.Link("newest first", r.String()) + "\n\n")
gListedBoardsByID.ReverseIterateByOffset(p.Offset(), p.PageSize(), render)
} else {
r.Query.Set("order", "desc")
res.Write(md.Link("oldest first", r.String()) + "\n\n")
gListedBoardsByID.IterateByOffset(p.Offset(), p.PageSize(), render)
}
if p.HasPages() {
res.Write(md.HorizontalRule())
res.Write(pager.Picker(p))
}
}
func renderBoardListMenu(res *mux.ResponseWriter, req *mux.Request) {
res.Write(md.Link("Create Board", createBoardURI))
res.Write(" • ")
res.Write(md.Link("List Admin Users", adminUsersURI))
res.Write(" • ")
res.Write(md.Link("Help", helpURI))
res.Write("\n\n")
}
func renderCreateBoard(res *mux.ResponseWriter, _ *mux.Request) {
form := mdform.New("exec", "CreateBoard")
form.Input(
"name",
"placeholder", "Board name",
"required", "true",
)
form.Radio(
"listed",
"true",
"checked", "true",
"description", "Should board be publicly listed?",
)
form.Radio(
"listed",
"false",
)
form.Radio(
"open",
"true",
"description", "Should anyone be allowed to create threads and comments?",
)
form.Radio(
"open",
"false",
"checked", "true",
)
res.Write(md.H1("Boards: Create Board"))
res.Write(md.Link("← Back to boards", gRealmPath) + "\n\n")
res.Write(
md.Paragraph(
"Boards are by default listed by the realm but they can optionally " +
"be created so they are only found by their URL.",
),
)
res.Write(
md.Paragraph(
"They can also be created to be open so anyone is allowed to create " +
"new threads and also to comment on any thread within the open board.",
),
)
res.Write(form.String())
res.Write("\n\n**Done?** " + svgbtn.ButtonWithRadius(136, 32, 4, "#E2E2E2", "#54595D", "Return to boards", gRealmPath) + "\n")
}
func renderMembers(res *mux.ResponseWriter, req *mux.Request) {
boardID := boards.ID(0)
perms := gPerms
name := req.GetVar("board")
if name != "" {
board, found := gBoards.GetByName(name)
if !found {
res.Write(md.H3("Board not found"))
return
}
boardID = board.ID
perms = board.Permissions
res.Write(md.H1(board.Name + " Members"))
res.Write(md.H3("These are the board members"))
} else {
res.Write(md.H1("Admin Users"))
res.Write(md.H3("These are the admin users of the realm"))
}
// Create a pager with a small page size to reduce
// the number of username lookups per page.
p, err := pager.New(req.RawPath, perms.UsersCount(), pager.WithPageSize(pageSizeDefault))
if err != nil {
res.Write(err.Error())
return
}
table := mdtable.Table{
Headers: []string{"Member", "Role", "Actions"},
}
perms.IterateUsers(p.Offset(), p.PageSize(), func(u boards.User) bool {
actions := []string{
md.Link("remove", gRealmLink.Call(
"RemoveMember",
"boardID", boardID.String(),
"member", u.Address.String(),
)),
md.Link("change role", gRealmLink.Call(
"ChangeMemberRole",
"boardID", boardID.String(),
"member", u.Address.String(),
"role", "",
)),
}
table.Append([]string{
userLink(u.Address),
rolesToString(u.Roles),
strings.Join(actions, " • "),
})
return false
})
res.Write(table.String())
if p.HasPages() {
res.Write("\n" + pager.Picker(p))
}
}
func renderInvites(res *mux.ResponseWriter, req *mux.Request) {
name := req.GetVar("board")
board, found := gBoards.GetByName(name)
if !found {
res.Write(md.H3("Board not found"))
return
}
res.Write(md.H1(board.Name + " Invite Requests"))
requests, found := getInviteRequests(board.ID)
if !found || requests.Size() == 0 {
res.Write(md.H3("Board has no invite requests"))
return
}
p, err := pager.New(req.RawPath, requests.Size(), pager.WithPageSize(pageSizeDefault))
if err != nil {
res.Write(err.Error())
return
}
table := mdtable.Table{
Headers: []string{"User", "Request Date", "Actions"},
}
res.Write(md.H3("These users have requested to be invited to the board"))
requests.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
actions := []string{
md.Link("accept", gRealmLink.Call(
"AcceptInvite",
"boardID", board.ID.String(),
"user", addr,
)),
md.Link("revoke", gRealmLink.Call(
"RevokeInvite",
"boardID", board.ID.String(),
"user", addr,
)),
}
table.Append([]string{
userLink(address(addr)),
v.(time.Time).Format(dateFormat),
strings.Join(actions, " • "),
})
return false
})
res.Write(table.String())
if p.HasPages() {
res.Write("\n" + pager.Picker(p))
}
}
func renderBannedUsers(res *mux.ResponseWriter, req *mux.Request) {
name := req.GetVar("board")
board, found := gBoards.GetByName(name)
if !found {
res.Write(md.H3("Board not found"))
return
}
res.Write(md.H1(board.Name + " Banned Users"))
banned, found := getBannedUsers(board.ID)
if !found || banned.Size() == 0 {
res.Write(md.H3("Board has no banned users"))
return
}
p, err := pager.New(req.RawPath, banned.Size(), pager.WithPageSize(pageSizeDefault))
if err != nil {
res.Write(err.Error())
return
}
table := mdtable.Table{
Headers: []string{"User", "Banned Until", "Actions"},
}
res.Write(md.H3("These users have been banned from the board"))
banned.ReverseIterateByOffset(p.Offset(), p.PageSize(), func(addr string, v any) bool {
table.Append([]string{
userLink(address(addr)),
v.(time.Time).Format(dateFormat),
md.Link("unban", gRealmLink.Call(
"Unban",
"boardID", board.ID.String(),
"user", addr,
"reason", "",
)),
})
return false
})
res.Write(table.String())
if p.HasPages() {
res.Write("\n" + pager.Picker(p))
}
}
func infoAlert(title, msg string) string {
header := strings.TrimSpace("[!INFO] " + title)
return md.Blockquote(header + "\n" + msg)
}
func rolesToString(roles []boards.Role) string {
if len(roles) == 0 {
return ""
}
names := make([]string, len(roles))
for i, r := range roles {
names[i] = string(r)
}
return strings.Join(names, ", ")
}
func menuURL(name string) string {
// TODO: Menu URL works because no other GET arguments are being used
return "?menu=" + name
}
func getCurrentMenu(rawURL string) string {
_, rawQuery, found := strings.Cut(rawURL, "?")
if !found {
return ""
}
query, _ := url.ParseQuery(rawQuery)
return query.Get("menu")
}