Forum Memberships & My List
* Add "Browse" tab to the forums to view them all. * Text search * Show all, official, community, or "My List" forums. * Add a Follow/Unfollow button into the header bar of forums to add it to "My List" * On the Categories page, a special "My List" category appears at the top if the user follows categories, with their follows in alphabetical order. * On the Categories & Browse pages: forums you follow will have a green bookmark icon by their name. Permissions: * The forum owner is able to Delete comments by others, but not Edit. Notes: * Currently a max limit of 100 follow forums (no pagination yet).
This commit is contained in:
parent
27540534ad
commit
eb85b2e090
|
@ -20,8 +20,9 @@ var (
|
|||
PageSizeAdminUserNotes = 10 // other users' notes
|
||||
PageSizeSiteGallery = 16
|
||||
PageSizeUserGallery = 16
|
||||
PageSizeInboxList = 20 // sidebar list
|
||||
PageSizeInboxThread = 10 // conversation view
|
||||
PageSizeInboxList = 20 // sidebar list
|
||||
PageSizeInboxThread = 10 // conversation view
|
||||
PageSizeBrowseForums = 20
|
||||
PageSizeForums = 100 // TODO: for main category index view
|
||||
PageSizeThreadList = 20 // 20 threads per board, 20 posts per thread
|
||||
PageSizeForumAdmin = 20
|
||||
|
|
104
pkg/controller/forum/browse.go
Normal file
104
pkg/controller/forum/browse.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package forum
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/config"
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
)
|
||||
|
||||
// Browse all existing forums.
|
||||
func Browse() http.HandlerFunc {
|
||||
// This page shares a template with the board index (Categories) page.
|
||||
tmpl := templates.Must("forum/index.html")
|
||||
|
||||
// Whitelist for ordering options.
|
||||
var sortWhitelist = []string{
|
||||
"title asc",
|
||||
"title desc",
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
searchTerm = r.FormValue("q")
|
||||
search = models.ParseSearchString(searchTerm)
|
||||
|
||||
show = r.FormValue("show")
|
||||
categories = []string{}
|
||||
|
||||
subscribed = r.FormValue("show") == "followed"
|
||||
|
||||
sort = r.FormValue("sort")
|
||||
sortOK bool
|
||||
)
|
||||
|
||||
// Sort options.
|
||||
for _, v := range sortWhitelist {
|
||||
if sort == v {
|
||||
sortOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !sortOK {
|
||||
sort = sortWhitelist[0]
|
||||
}
|
||||
|
||||
// Set of forum categories to filter for.
|
||||
switch show {
|
||||
case "official":
|
||||
categories = config.ForumCategories
|
||||
case "community":
|
||||
categories = []string{""}
|
||||
}
|
||||
|
||||
// Get the current user.
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't get current user: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
var pager = &models.Pagination{
|
||||
Page: 1,
|
||||
PerPage: config.PageSizeBrowseForums,
|
||||
Sort: sort,
|
||||
}
|
||||
pager.ParsePage(r)
|
||||
|
||||
// Browse all forums (no category filter for official)
|
||||
forums, err := models.PaginateForums(currentUser, categories, search, subscribed, pager)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't paginate forums: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
// Bucket the forums into their categories for easy front-end.
|
||||
categorized := models.CategorizeForums(forums, nil)
|
||||
|
||||
// Map statistics for these forums.
|
||||
forumMap := models.MapForumStatistics(forums)
|
||||
followMap := models.MapForumMemberships(currentUser, forums)
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"CurrentForumTab": "browse",
|
||||
"IsBrowseTab": true,
|
||||
"Pager": pager,
|
||||
"Categories": categorized,
|
||||
"ForumMap": forumMap,
|
||||
"FollowMap": followMap,
|
||||
|
||||
// Search filters
|
||||
"SearchTerm": searchTerm,
|
||||
"Show": show,
|
||||
"Sort": sort,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
|
@ -72,10 +72,11 @@ func Forum() http.HandlerFunc {
|
|||
threadMap := models.MapThreadStatistics(threads)
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"Forum": forum,
|
||||
"Threads": threads,
|
||||
"ThreadMap": threadMap,
|
||||
"Pager": pager,
|
||||
"Forum": forum,
|
||||
"IsForumSubscribed": models.IsForumSubscribed(currentUser, forum),
|
||||
"Threads": threads,
|
||||
"ThreadMap": threadMap,
|
||||
"Pager": pager,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
|
@ -40,7 +40,7 @@ func Landing() http.HandlerFunc {
|
|||
}
|
||||
pager.ParsePage(r)
|
||||
|
||||
forums, err := models.PaginateForums(currentUser, config.ForumCategories, pager)
|
||||
forums, err := models.PaginateForums(currentUser, config.ForumCategories, nil, false, pager)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't paginate forums: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
|
@ -50,13 +50,29 @@ func Landing() http.HandlerFunc {
|
|||
// Bucket the forums into their categories for easy front-end.
|
||||
categorized := models.CategorizeForums(forums, config.ForumCategories)
|
||||
|
||||
// Inject the "My List" Category if the user subscribes to forums.
|
||||
myList, err := models.PaginateForums(currentUser, nil, nil, true, pager)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't get your followed forums: %s", err)
|
||||
} else {
|
||||
forums = append(forums, myList...)
|
||||
categorized = append([]*models.CategorizedForum{
|
||||
{
|
||||
Category: "My List",
|
||||
Forums: myList,
|
||||
},
|
||||
}, categorized...)
|
||||
}
|
||||
|
||||
// Map statistics for these forums.
|
||||
forumMap := models.MapForumStatistics(forums)
|
||||
followMap := models.MapForumMemberships(currentUser, forums)
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"Pager": pager,
|
||||
"Categories": categorized,
|
||||
"ForumMap": forumMap,
|
||||
"FollowMap": followMap,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
|
@ -87,6 +87,11 @@ func NewPost() http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// If the current user can moderate the forum thread, e.g. edit or delete posts.
|
||||
// Admins can edit always, user owners of forums can only delete.
|
||||
var canModerate = currentUser.HasAdminScope(config.ScopeForumModerator) ||
|
||||
(forum.OwnerID == currentUser.ID && isDelete)
|
||||
|
||||
// Does the comment have an existing Photo ID?
|
||||
if len(photoID) > 0 {
|
||||
if i, err := strconv.Atoi(photoID); err == nil {
|
||||
|
@ -116,7 +121,7 @@ func NewPost() http.HandlerFunc {
|
|||
comment = found
|
||||
|
||||
// Verify that it is indeed OUR comment.
|
||||
if currentUser.ID != comment.UserID && !currentUser.HasAdminScope(config.ScopeForumModerator) {
|
||||
if currentUser.ID != comment.UserID && !canModerate {
|
||||
templates.ForbiddenPage(w, r)
|
||||
return
|
||||
}
|
||||
|
|
67
pkg/controller/forum/subscribe.go
Normal file
67
pkg/controller/forum/subscribe.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package forum
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
)
|
||||
|
||||
// Subscribe to a forum, adding it to your bookmark list.
|
||||
func Subscribe() http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse the path parameters
|
||||
var (
|
||||
fragment = r.FormValue("fragment")
|
||||
forum *models.Forum
|
||||
intent = r.FormValue("intent")
|
||||
)
|
||||
|
||||
// Look up the forum by its fragment.
|
||||
if found, err := models.ForumByFragment(fragment); err != nil {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
} else {
|
||||
forum = found
|
||||
}
|
||||
|
||||
// Get the current user.
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't get current user: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
return
|
||||
}
|
||||
|
||||
// Is it a private forum?
|
||||
if forum.Private && !currentUser.IsAdmin {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
switch intent {
|
||||
case "follow":
|
||||
_, err := models.CreateForumMembership(currentUser, forum)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't follow this forum: %s", err)
|
||||
} else {
|
||||
session.Flash(w, r, "You have added %s to your forum list.", forum.Title)
|
||||
}
|
||||
case "unfollow":
|
||||
fm, err := models.GetForumMembership(currentUser, forum)
|
||||
if err == nil {
|
||||
err = fm.Delete()
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't delete your forum membership: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
session.Flash(w, r, "You have removed %s from your forum list.", forum.Title)
|
||||
default:
|
||||
session.Flash(w, r, "Unknown intent.")
|
||||
}
|
||||
|
||||
templates.Redirect(w, "/f/"+fragment)
|
||||
})
|
||||
}
|
|
@ -57,6 +57,13 @@ func Thread() http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Can we moderate this forum? (from a user-owned forum perspective,
|
||||
// e.g. can we delete threads and posts, not edit them)
|
||||
var canModerate bool
|
||||
if currentUser.HasAdminScope(config.ScopeForumModerator) || forum.OwnerID == currentUser.ID {
|
||||
canModerate = true
|
||||
}
|
||||
|
||||
// Ping the view count on this thread.
|
||||
if err := thread.View(currentUser.ID); err != nil {
|
||||
log.Error("Couldn't ping view count on thread %d: %s", thread.ID, err)
|
||||
|
@ -100,6 +107,7 @@ func Thread() http.HandlerFunc {
|
|||
"LikeMap": commentLikeMap,
|
||||
"PhotoMap": photos,
|
||||
"Pager": pager,
|
||||
"CanModerate": canModerate,
|
||||
"IsSubscribed": isSubscribed,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
|
|
|
@ -78,7 +78,7 @@ Parameters:
|
|||
- categories: optional, filter within categories
|
||||
- pager
|
||||
*/
|
||||
func PaginateForums(user *User, categories []string, pager *Pagination) ([]*Forum, error) {
|
||||
func PaginateForums(user *User, categories []string, search *Search, subscribed bool, pager *Pagination) ([]*Forum, error) {
|
||||
var (
|
||||
fs = []*Forum{}
|
||||
query = (&Forum{}).Preload()
|
||||
|
@ -101,6 +101,33 @@ func PaginateForums(user *User, categories []string, pager *Pagination) ([]*Foru
|
|||
wheres = append(wheres, "private is not true")
|
||||
}
|
||||
|
||||
// Followed forums only? (for the My List category on home page)
|
||||
if subscribed {
|
||||
wheres = append(wheres, `
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM forum_memberships
|
||||
WHERE user_id = ?
|
||||
AND forum_id = forums.id
|
||||
)
|
||||
`)
|
||||
placeholders = append(placeholders, user.ID)
|
||||
}
|
||||
|
||||
// Apply their search terms.
|
||||
if search != nil {
|
||||
for _, term := range search.Includes {
|
||||
var ilike = "%" + strings.ToLower(term) + "%"
|
||||
wheres = append(wheres, "(fragment ILIKE ? OR title ILIKE ? OR description ILIKE ?)")
|
||||
placeholders = append(placeholders, ilike, ilike, ilike)
|
||||
}
|
||||
for _, term := range search.Excludes {
|
||||
var ilike = "%" + strings.ToLower(term) + "%"
|
||||
wheres = append(wheres, "(fragment NOT ILIKE ? AND title NOT ILIKE ? AND description NOT ILIKE ?)")
|
||||
placeholders = append(placeholders, ilike, ilike, ilike)
|
||||
}
|
||||
}
|
||||
|
||||
// Filters?
|
||||
if len(wheres) > 0 {
|
||||
query = query.Where(
|
||||
|
@ -178,6 +205,15 @@ func CategorizeForums(fs []*Forum, categories []string) []*CategorizedForum {
|
|||
idxMap = map[string]int{}
|
||||
)
|
||||
|
||||
// Forum Browse page: we are not grouping by categories but still need at least one.
|
||||
if len(categories) == 0 {
|
||||
return []*CategorizedForum{
|
||||
{
|
||||
Forums: fs,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the result set.
|
||||
for i, category := range categories {
|
||||
result = append(result, &CategorizedForum{
|
||||
|
|
121
pkg/models/forum_membership.go
Normal file
121
pkg/models/forum_membership.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ForumMembership table.
|
||||
type ForumMembership struct {
|
||||
ID uint64 `gorm:"primaryKey"`
|
||||
UserID uint64 `gorm:"index"`
|
||||
User User `gorm:"foreignKey:user_id"`
|
||||
ForumID uint64 `gorm:"index"`
|
||||
Forum Forum `gorm:"foreignKey:forum_id"`
|
||||
Approved bool `gorm:"index"`
|
||||
IsModerator bool `gorm:"index"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// Preload related tables for the forum (classmethod).
|
||||
func (f *ForumMembership) Preload() *gorm.DB {
|
||||
return DB.Preload("User").Preload("Forum")
|
||||
}
|
||||
|
||||
// CreateForumMembership subscribes the user to a forum.
|
||||
func CreateForumMembership(user *User, forum *Forum) (*ForumMembership, error) {
|
||||
var (
|
||||
f = &ForumMembership{
|
||||
User: *user,
|
||||
Forum: *forum,
|
||||
Approved: true,
|
||||
}
|
||||
result = DB.Create(f)
|
||||
)
|
||||
return f, result.Error
|
||||
}
|
||||
|
||||
// GetForumMembership looks up a forum membership.
|
||||
func GetForumMembership(user *User, forum *Forum) (*ForumMembership, error) {
|
||||
var (
|
||||
f = &ForumMembership{}
|
||||
result = f.Preload().Where(
|
||||
"user_id = ? AND forum_id = ?",
|
||||
user.ID, forum.ID,
|
||||
).First(&f)
|
||||
)
|
||||
return f, result.Error
|
||||
}
|
||||
|
||||
// IsForumSubscribed checks if the current user subscribes to this forum.
|
||||
func IsForumSubscribed(user *User, forum *Forum) bool {
|
||||
f, _ := GetForumMembership(user, forum)
|
||||
return f.UserID == user.ID
|
||||
}
|
||||
|
||||
// Delete a forum membership.
|
||||
func (f *ForumMembership) Delete() error {
|
||||
return DB.Delete(f).Error
|
||||
}
|
||||
|
||||
// PaginateForumMemberships paginates over a user's ForumMemberships.
|
||||
func PaginateForumMemberships(user *User, pager *Pagination) ([]*ForumMembership, error) {
|
||||
var (
|
||||
fs = []*ForumMembership{}
|
||||
query = (&ForumMembership{}).Preload()
|
||||
wheres = []string{}
|
||||
placeholders = []interface{}{}
|
||||
)
|
||||
|
||||
query = query.Where(
|
||||
strings.Join(wheres, " AND "),
|
||||
placeholders...,
|
||||
).Order(pager.Sort)
|
||||
|
||||
query.Model(&ForumMembership{}).Count(&pager.Total)
|
||||
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs)
|
||||
return fs, result.Error
|
||||
}
|
||||
|
||||
// ForumMembershipMap maps table IDs to Likes metadata.
|
||||
type ForumMembershipMap map[uint64]bool
|
||||
|
||||
// Get like stats from the map.
|
||||
func (fm ForumMembershipMap) Get(id uint64) bool {
|
||||
return fm[id]
|
||||
}
|
||||
|
||||
// MapForumMemberships looks up a user's memberships in bulk.
|
||||
func MapForumMemberships(user *User, forums []*Forum) ForumMembershipMap {
|
||||
var (
|
||||
result = ForumMembershipMap{}
|
||||
forumIDs = []uint64{}
|
||||
)
|
||||
|
||||
// Initialize the result set.
|
||||
for _, forum := range forums {
|
||||
result[forum.ID] = false
|
||||
forumIDs = append(forumIDs, forum.ID)
|
||||
}
|
||||
|
||||
// Map the forum IDs the user subscribes to.
|
||||
var followIDs = []uint64{}
|
||||
if res := DB.Model(&ForumMembership{}).Select(
|
||||
"forum_id",
|
||||
).Where(
|
||||
"user_id = ? AND forum_id IN ?",
|
||||
user.ID, forumIDs,
|
||||
).Scan(&followIDs); res.Error != nil {
|
||||
log.Error("MapForumMemberships: %s", res.Error)
|
||||
}
|
||||
|
||||
for _, forumID := range followIDs {
|
||||
result[forumID] = true
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -35,4 +35,5 @@ func AutoMigrate() {
|
|||
DB.AutoMigrate(&IPAddress{})
|
||||
DB.AutoMigrate(&PushNotification{})
|
||||
DB.AutoMigrate(&WorldCities{})
|
||||
DB.AutoMigrate(&ForumMembership{})
|
||||
}
|
||||
|
|
|
@ -87,8 +87,10 @@ func New() http.Handler {
|
|||
mux.Handle("GET /forum", middleware.CertRequired(forum.Landing()))
|
||||
mux.Handle("/forum/post", middleware.CertRequired(forum.NewPost()))
|
||||
mux.Handle("GET /forum/thread/{id}", middleware.CertRequired(forum.Thread()))
|
||||
mux.Handle("GET /forum/browse", middleware.CertRequired(forum.Browse()))
|
||||
mux.Handle("GET /forum/newest", middleware.CertRequired(forum.Newest()))
|
||||
mux.Handle("GET /forum/search", middleware.CertRequired(forum.Search()))
|
||||
mux.Handle("POST /forum/subscribe", middleware.CertRequired(forum.Subscribe()))
|
||||
mux.Handle("GET /f/{fragment}", middleware.CertRequired(forum.Forum()))
|
||||
mux.Handle("POST /poll/vote", middleware.CertRequired(poll.Vote()))
|
||||
|
||||
|
|
|
@ -4,10 +4,39 @@
|
|||
<section class="hero is-light is-success">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
<span class="icon mr-4"><i class="fa fa-comments"></i></span>
|
||||
<span>{{.Forum.Title}}</span>
|
||||
</h1>
|
||||
<div class="level">
|
||||
<div class="level-left mb-4">
|
||||
<h1 class="title">
|
||||
<span class="icon mr-4"><i class="fa fa-comments"></i></span>
|
||||
<span>{{.Forum.Title}}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{{if .FeatureUserForumsEnabled}}
|
||||
<div class="level-right">
|
||||
<!-- Follow/Unfollow This Forum -->
|
||||
<form action="/forum/subscribe" method="POST" class="is-inline">
|
||||
{{InputCSRF}}
|
||||
<input type="hidden" name="fragment" value="{{.Forum.Fragment}}">
|
||||
|
||||
{{if .IsForumSubscribed}}
|
||||
<button type="submit" class="button"
|
||||
name="intent" value="unfollow"
|
||||
onclick="return confirm('Do you want to remove this forum from your list?')">
|
||||
<span class="icon"><i class="fa fa-bookmark"></i></span>
|
||||
<span>Followed</span>
|
||||
</button>
|
||||
{{else}}
|
||||
<button type="submit" class="button"
|
||||
name="intent" value="follow">
|
||||
<span class="icon"><i class="fa-regular fa-bookmark"></i></span>
|
||||
<span>Follow</span>
|
||||
</button>
|
||||
{{end}}
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -29,7 +58,7 @@
|
|||
</div>
|
||||
<div class="column is-narrow">
|
||||
{{if or .CurrentUser.IsAdmin (not .Forum.Privileged) (eq .Forum.OwnerID .CurrentUser.ID)}}
|
||||
<a href="/forum/post?to={{.Forum.Fragment}}" class="button is-primary">
|
||||
<a href="/forum/post?to={{.Forum.Fragment}}" class="button is-info">
|
||||
<span class="icon"><i class="fa fa-plus"></i></span>
|
||||
<span>New Thread</span>
|
||||
</a>
|
||||
|
|
|
@ -32,9 +32,101 @@
|
|||
<!-- Tab bar -->
|
||||
{{template "ForumTabs" .}}
|
||||
|
||||
<!-- Filters for the Browse tab -->
|
||||
{{if .IsBrowseTab}}
|
||||
<div class="block mb-0 p-4">
|
||||
<form action="{{.Request.URL.Path}}" method="GET">
|
||||
|
||||
<div class="card nonshy-collapsible-mobile">
|
||||
<header class="card-header has-background-link-light">
|
||||
<p class="card-header-title has-text-dark">
|
||||
Search Filters
|
||||
</p>
|
||||
<button class="card-header-icon" type="button">
|
||||
<span class="icon">
|
||||
<i class="fa fa-angle-up"></i>
|
||||
</span>
|
||||
</button>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="columns">
|
||||
|
||||
<div class="column is-half pr-1">
|
||||
<div class="field">
|
||||
<label class="label" for="q">Search terms:</label>
|
||||
<input type="text" class="input"
|
||||
name="q" id="q"
|
||||
autocomplete="off"
|
||||
value="{{.SearchTerm}}">
|
||||
<p class="help">
|
||||
Tip: you can <span class="has-text-success">"quote exact phrases"</span> and
|
||||
<span class="has-text-success">-exclude</span> words (or
|
||||
<span class="has-text-success">-"exclude phrases"</span>) from your search.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column px-1">
|
||||
<div class="field">
|
||||
<label class="label" for="show">Show:</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select id="show" name="show">
|
||||
<option value=""{{if eq .Show ""}} selected{{end}}>All forums</option>
|
||||
<option value="official"{{if eq .Show "official"}} selected{{end}}>Official nonshy forums</option>
|
||||
<option value="community"{{if eq .Show "community"}} selected{{end}}>Community forums only</option>
|
||||
<option value="followed"{{if eq .Show "followed"}} selected{{end}}>My List</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column px-1">
|
||||
<div class="field">
|
||||
<label class="label" for="sort">Sort by:</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select id="sort" name="sort">
|
||||
<option value="title asc"{{if eq .Sort "title asc"}} selected{{end}}>Title (A-Z)</option>
|
||||
<option value="title desc"{{if eq .Sort "title desc"}} selected{{end}}>Title (Z-A)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-narrow pl-1 has-text-right">
|
||||
<label class="label"> </label>
|
||||
<a href="{{.Request.URL.Path}}" class="button">Reset</a>
|
||||
<button type="submit" class="button is-success">
|
||||
<span>Search</span>
|
||||
<span class="icon"><i class="fa fa-search"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="block p-4">
|
||||
Found <strong>{{.Pager.Total}}</strong> forum{{Pluralize64 .Pager.Total}}.
|
||||
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
||||
</p>
|
||||
|
||||
<div class="block p-4">
|
||||
{{SimplePager .Pager}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{range .Categories}}
|
||||
<div class="block p-4">
|
||||
<h1 class="title">{{.Category}}</h1>
|
||||
{{if .Category}}
|
||||
<h1 class="title">
|
||||
{{.Category}}
|
||||
{{if eq .Category "My List"}}
|
||||
<i class="fa fa-book-bookmark ml-2"></i>
|
||||
{{end}}
|
||||
</h1>
|
||||
{{end}}
|
||||
|
||||
{{if eq (len .Forums) 0}}
|
||||
<em>
|
||||
|
@ -50,6 +142,9 @@
|
|||
<div class="column is-3 pt-0 pb-1">
|
||||
|
||||
<h2 class="is-size-4">
|
||||
{{if $Root.FollowMap.Get .ID}}
|
||||
<sup class="fa fa-bookmark has-text-success is-size-6 mr-1" title="Followed"></sup>
|
||||
{{end}}
|
||||
<strong><a href="/f/{{.Fragment}}">{{.Title}}</a></strong>
|
||||
</h2>
|
||||
|
||||
|
@ -156,4 +251,11 @@
|
|||
</div>
|
||||
{{end}}<!-- range .Categories -->
|
||||
|
||||
<!-- Pager footer for Browse tab -->
|
||||
{{if .IsBrowseTab}}
|
||||
<div class="block p-4">
|
||||
{{SimplePager .Pager}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{end}}
|
|
@ -289,6 +289,9 @@
|
|||
<span>Edit</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if or $Root.CanModerate ($Root.CurrentUser.HasAdminScope "social.moderator.forum") (eq $Root.CurrentUser.ID .User.ID)}}
|
||||
<div class="column is-narrow">
|
||||
<a href="/forum/post?to={{$Root.Forum.Fragment}}&thread={{$Root.Thread.ID}}&edit={{.ID}}&delete=true" onclick="return confirm('Are you sure you want to delete this comment?')" class="has-text-dark">
|
||||
<span class="icon"><i class="fa fa-trash"></i></span>
|
||||
|
|
|
@ -16,6 +16,11 @@ Variables that your template should set:
|
|||
Categories
|
||||
</a>
|
||||
</li>
|
||||
<li {{if eq .CurrentForumTab "browse" }}class="is-active"{{end}}>
|
||||
<a href="/forum/browse">
|
||||
Browse
|
||||
</a>
|
||||
</li>
|
||||
<li {{if eq .CurrentForumTab "newest" }}class="is-active"{{end}}>
|
||||
<a href="/forum/newest">
|
||||
Newest
|
||||
|
|
Loading…
Reference in New Issue
Block a user