Skip to content

Commit ac4398f

Browse files
committed
feat: Add configurable default post sort order
Add an admin setting in General Settings to configure the default post sort order (Trending, Most Wanted, Recent, Most Discussed). Previously the frontend always fell back to "trending" regardless of any server-side configuration. Now it reads the tenant's defaultSort setting both server-side (in the handler) and client-side (in the PostsContainer component). Includes database migration to add default_sort column to tenants table.
1 parent 98bf7d9 commit ac4398f

File tree

13 files changed

+103
-57
lines changed

13 files changed

+103
-57
lines changed

app/actions/tenant.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,14 @@ func (action *ResendSignUpEmail) GetKind() enum.EmailVerificationKind {
190190

191191
// UpdateTenantSettings is the input model used to update tenant settings
192192
type UpdateTenantSettings struct {
193-
Logo *dto.ImageUpload `json:"logo"`
194-
Title string `json:"title"`
195-
Invitation string `json:"invitation"`
196-
WelcomeMessage string `json:"welcomeMessage"`
197-
WelcomeHeader string `json:"welcomeHeader"`
198-
Locale string `json:"locale"`
199-
CNAME string `json:"cname" format:"lower"`
193+
Logo *dto.ImageUpload `json:"logo"`
194+
Title string `json:"title"`
195+
Invitation string `json:"invitation"`
196+
WelcomeMessage string `json:"welcomeMessage"`
197+
WelcomeHeader string `json:"welcomeHeader"`
198+
Locale string `json:"locale"`
199+
CNAME string `json:"cname" format:"lower"`
200+
DefaultSort string `json:"defaultSort"`
200201
}
201202

202203
func NewUpdateTenantSettings() *UpdateTenantSettings {
@@ -256,6 +257,18 @@ func (action *UpdateTenantSettings) Validate(ctx context.Context, user *entity.U
256257
result.AddFieldFailure("cname", messages...)
257258
}
258259

260+
validSorts := []string{"trending", "most-wanted", "most-discussed", "recent"}
261+
sortValid := false
262+
for _, s := range validSorts {
263+
if action.DefaultSort == s {
264+
sortValid = true
265+
break
266+
}
267+
}
268+
if !sortValid {
269+
action.DefaultSort = "most-wanted"
270+
}
271+
259272
return result
260273
}
261274

app/handlers/admin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func UpdateSettings() web.HandlerFunc {
6666
WelcomeHeader: action.WelcomeHeader,
6767
CNAME: action.CNAME,
6868
Locale: action.Locale,
69+
DefaultSort: action.DefaultSort,
6970
},
7071
); err != nil {
7172
return c.Failure(err)

app/handlers/post.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ func Index() web.HandlerFunc {
1717
return func(c *web.Context) error {
1818
c.SetCanonicalURL("")
1919

20+
view := c.QueryParam("view")
21+
if view == "" {
22+
view = c.Tenant().DefaultSort
23+
}
24+
2025
searchPosts := &query.SearchPosts{
2126
Query: c.QueryParam("query"),
22-
View: c.QueryParam("view"),
27+
View: view,
2328
Limit: c.QueryParam("limit"),
2429
Tags: c.QueryParamAsArray("tags"),
2530
}
@@ -77,6 +82,7 @@ func Index() web.HandlerFunc {
7782

7883
return c.Page(http.StatusOK, web.Props{
7984
Page: "Home/Home.page",
85+
Title: "Feedback",
8086
Description: description,
8187
// Header: c.Tenant().WelcomeHeader,
8288
Data: data,

app/models/cmd/tenant.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ type UpdateTenantEmailAuthAllowedSettings struct {
2727
}
2828

2929
type UpdateTenantSettings struct {
30-
Logo *dto.ImageUpload
31-
Title string
32-
Invitation string
33-
WelcomeMessage string
34-
WelcomeHeader string
35-
CNAME string
36-
Locale string
30+
Logo *dto.ImageUpload
31+
Title string
32+
Invitation string
33+
WelcomeMessage string
34+
WelcomeHeader string
35+
CNAME string
36+
Locale string
37+
DefaultSort string
3738
}
3839

3940
type UpdateTenantAdvancedSettings struct {

app/models/entity/tenant.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,25 @@ import (
66

77
// Tenant represents a tenant
88
type Tenant struct {
9-
ID int `json:"id"`
10-
Name string `json:"name"`
11-
Subdomain string `json:"subdomain"`
12-
Invitation string `json:"invitation"`
13-
WelcomeMessage string `json:"welcomeMessage"`
14-
WelcomeHeader string `json:"welcomeHeader"`
15-
CNAME string `json:"cname"`
16-
Status enum.TenantStatus `json:"status"`
17-
Locale string `json:"locale"`
18-
IsPrivate bool `json:"isPrivate"`
19-
LogoBlobKey string `json:"logoBlobKey"`
20-
CustomCSS string `json:"-"`
21-
AllowedSchemes string `json:"allowedSchemes"`
22-
IsEmailAuthAllowed bool `json:"isEmailAuthAllowed"`
23-
IsFeedEnabled bool `json:"isFeedEnabled"`
24-
PreventIndexing bool `json:"preventIndexing"`
25-
IsModerationEnabled bool `json:"isModerationEnabled"`
26-
HasCommercialFeatures bool `json:"hasCommercialFeatures"`
9+
ID int `json:"id"`
10+
Name string `json:"name"`
11+
Subdomain string `json:"subdomain"`
12+
Invitation string `json:"invitation"`
13+
WelcomeMessage string `json:"welcomeMessage"`
14+
WelcomeHeader string `json:"welcomeHeader"`
15+
CNAME string `json:"cname"`
16+
Status enum.TenantStatus `json:"status"`
17+
Locale string `json:"locale"`
18+
IsPrivate bool `json:"isPrivate"`
19+
LogoBlobKey string `json:"logoBlobKey"`
20+
CustomCSS string `json:"-"`
21+
AllowedSchemes string `json:"allowedSchemes"`
22+
IsEmailAuthAllowed bool `json:"isEmailAuthAllowed"`
23+
IsFeedEnabled bool `json:"isFeedEnabled"`
24+
PreventIndexing bool `json:"preventIndexing"`
25+
IsModerationEnabled bool `json:"isModerationEnabled"`
26+
HasCommercialFeatures bool `json:"hasCommercialFeatures"`
27+
DefaultSort string `json:"defaultSort"`
2728
}
2829

2930
func (t *Tenant) IsDisabled() bool {

app/services/sqlstore/dbEntities/tenant.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Tenant struct {
2727
IsModerationEnabled bool `db:"is_moderation_enabled"`
2828
IsPro bool `db:"is_pro"`
2929
HasPaddleSubscription bool `db:"has_paddle_subscription"`
30+
DefaultSort string `db:"default_sort"`
3031
}
3132

3233
func (t *Tenant) ToModel() *entity.Tenant {
@@ -63,6 +64,7 @@ func (t *Tenant) ToModel() *entity.Tenant {
6364
PreventIndexing: t.PreventIndexing,
6465
IsModerationEnabled: t.IsModerationEnabled,
6566
HasCommercialFeatures: hasCommercialFeatures,
67+
DefaultSort: t.DefaultSort,
6668
}
6769

6870
return tenant

app/services/sqlstore/postgres/tenant.go

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ func updateTenantSettings(ctx context.Context, c *cmd.UpdateTenantSettings) erro
8080
c.Logo.BlobKey = ""
8181
}
8282

83-
query := "UPDATE tenants SET name = $1, invitation = $2, welcome_message = $3, welcome_header = $4, cname = $5, logo_bkey = $6, locale = $7 WHERE id = $8"
84-
_, err := trx.Execute(query, c.Title, c.Invitation, c.WelcomeMessage, c.WelcomeHeader, c.CNAME, c.Logo.BlobKey, c.Locale, tenant.ID)
83+
query := "UPDATE tenants SET name = $1, invitation = $2, welcome_message = $3, welcome_header = $4, cname = $5, logo_bkey = $6, locale = $7, default_sort = $8 WHERE id = $9"
84+
_, err := trx.Execute(query, c.Title, c.Invitation, c.WelcomeMessage, c.WelcomeHeader, c.CNAME, c.Logo.BlobKey, c.Locale, c.DefaultSort, tenant.ID)
8585
if err != nil {
8686
return errors.Wrap(err, "failed update tenant settings")
8787
}
@@ -91,6 +91,7 @@ func updateTenantSettings(ctx context.Context, c *cmd.UpdateTenantSettings) erro
9191
tenant.CNAME = c.CNAME
9292
tenant.WelcomeMessage = c.WelcomeMessage
9393
tenant.WelcomeHeader = c.WelcomeHeader
94+
tenant.DefaultSort = c.DefaultSort
9495

9596
return nil
9697
})
@@ -189,8 +190,8 @@ func createTenant(ctx context.Context, c *cmd.CreateTenant) error {
189190

190191
var id int
191192
err := trx.Get(&id,
192-
`INSERT INTO tenants (name, subdomain, created_at, cname, invitation, welcome_message, status, is_private, custom_css, logo_bkey, locale, is_email_auth_allowed, is_feed_enabled, prevent_indexing, is_moderation_enabled)
193-
VALUES ($1, $2, $3, '', '', '', $4, false, '', '', $5, true, true, true, false)
193+
`INSERT INTO tenants (name, subdomain, created_at, cname, invitation, welcome_message, status, is_private, custom_css, logo_bkey, locale, is_email_auth_allowed, is_feed_enabled, prevent_indexing, is_moderation_enabled, default_sort)
194+
VALUES ($1, $2, $3, '', '', '', $4, false, '', '', $5, true, true, true, false, 'most-wanted')
194195
RETURNING id`, c.Name, c.Subdomain, now, c.Status, env.Config.Locale)
195196
if err != nil {
196197
return err
@@ -207,13 +208,13 @@ func getFirstTenant(ctx context.Context, q *query.GetFirstTenant) error {
207208
return using(ctx, func(trx *dbx.Trx, _ *entity.Tenant, _ *entity.User) error {
208209
tenant := dbEntities.Tenant{}
209210

210-
err := trx.Get(&tenant, `
211-
SELECT t.id, t.name, t.subdomain, t.cname, t.invitation, t.locale, t.welcome_message, t.welcome_header, t.status, t.is_private, t.logo_bkey, t.custom_css, t.allowed_schemes, t.is_email_auth_allowed, t.is_feed_enabled, t.is_moderation_enabled, t.prevent_indexing, t.is_pro,
212-
(b.paddle_subscription_id IS NOT NULL AND b.stripe_subscription_id IS NULL) AS has_paddle_subscription
213-
FROM tenants t
214-
LEFT JOIN tenants_billing b ON b.tenant_id = t.id
215-
ORDER BY t.id LIMIT 1
216-
`)
211+
err := trx.Get(&tenant, `
212+
SELECT t.id, t.name, t.subdomain, t.cname, t.invitation, t.locale, t.welcome_message, t.welcome_header, t.status, t.is_private, t.logo_bkey, t.custom_css, t.allowed_schemes, t.is_email_auth_allowed, t.is_feed_enabled, t.is_moderation_enabled, t.prevent_indexing, t.is_pro, t.default_sort,
213+
(b.paddle_subscription_id IS NOT NULL AND b.stripe_subscription_id IS NULL) AS has_paddle_subscription
214+
FROM tenants t
215+
LEFT JOIN tenants_billing b ON b.tenant_id = t.id
216+
ORDER BY t.id LIMIT 1
217+
`)
217218
if err != nil {
218219
return errors.Wrap(err, "failed to get first tenant")
219220
}
@@ -227,14 +228,14 @@ func getTenantByDomain(ctx context.Context, q *query.GetTenantByDomain) error {
227228
return using(ctx, func(trx *dbx.Trx, _ *entity.Tenant, _ *entity.User) error {
228229
tenant := dbEntities.Tenant{}
229230

230-
err := trx.Get(&tenant, `
231-
SELECT t.id, t.name, t.subdomain, t.cname, t.invitation, t.locale, t.welcome_message, t.welcome_header, t.status, t.is_private, t.logo_bkey, t.custom_css, t.allowed_schemes, t.is_email_auth_allowed, t.is_feed_enabled, t.is_moderation_enabled, t.prevent_indexing, t.is_pro,
232-
(b.paddle_subscription_id IS NOT NULL AND b.stripe_subscription_id IS NULL) AS has_paddle_subscription
233-
FROM tenants t
234-
LEFT JOIN tenants_billing b ON b.tenant_id = t.id
235-
WHERE t.subdomain = $1 OR t.subdomain = $2 OR t.cname = $3
236-
ORDER BY t.cname DESC
237-
`, env.Subdomain(q.Domain), q.Domain, q.Domain)
231+
err := trx.Get(&tenant, `
232+
SELECT t.id, t.name, t.subdomain, t.cname, t.invitation, t.locale, t.welcome_message, t.welcome_header, t.status, t.is_private, t.logo_bkey, t.custom_css, t.allowed_schemes, t.is_email_auth_allowed, t.is_feed_enabled, t.is_moderation_enabled, t.prevent_indexing, t.is_pro, t.default_sort,
233+
(b.paddle_subscription_id IS NOT NULL AND b.stripe_subscription_id IS NULL) AS has_paddle_subscription
234+
FROM tenants t
235+
LEFT JOIN tenants_billing b ON b.tenant_id = t.id
236+
WHERE t.subdomain = $1 OR t.subdomain = $2 OR t.cname = $3
237+
ORDER BY t.cname DESC
238+
`, env.Subdomain(q.Domain), q.Domain, q.Domain)
238239
if err != nil {
239240
return errors.Wrap(err, "failed to get tenant with domain '%s'", q.Domain)
240241
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ALTER TABLE tenants ADD default_sort VARCHAR(50) NULL;
2+
UPDATE tenants SET default_sort = 'most-wanted';
3+
ALTER TABLE tenants ALTER COLUMN default_sort SET NOT NULL;
4+
ALTER TABLE tenants ALTER COLUMN default_sort SET DEFAULT 'most-wanted';

public/models/identity.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface Tenant {
1515
isFeedEnabled: boolean
1616
isModerationEnabled: boolean
1717
hasCommercialFeatures: boolean
18+
defaultSort: string
1819
}
1920

2021
export enum TenantStatus {

public/pages/Administration/pages/GeneralSettings.page.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ const GeneralSettingsPage = () => {
1616
const [logo, setLogo] = useState<ImageUpload | undefined>(undefined)
1717
const [cname, setCNAME] = useState<string>(fider.session.tenant.cname)
1818
const [locale, setLocale] = useState<string>(fider.session.tenant.locale)
19+
const [defaultSort, setDefaultSort] = useState<string>(fider.session.tenant.defaultSort)
1920
const [error, setError] = useState<Failure | undefined>(undefined)
2021

2122
const handleSave = async (e: ButtonClickEvent) => {
22-
const result = await actions.updateTenantSettings({ title, cname, welcomeMessage, welcomeHeader, invitation, logo, locale })
23+
const result = await actions.updateTenantSettings({ title, cname, welcomeMessage, welcomeHeader, invitation, logo, locale, defaultSort })
2324
if (result.ok) {
2425
e.preventEnable()
2526
location.href = `/`
@@ -143,6 +144,19 @@ const GeneralSettingsPage = () => {
143144
)}
144145
</Select>
145146

147+
<Select
148+
label="Default Sort"
149+
field="defaultSort"
150+
defaultValue={defaultSort}
151+
options={[
152+
{ value: "most-wanted", label: "Most Wanted" },
153+
{ value: "trending", label: "Trending" },
154+
{ value: "most-discussed", label: "Most Discussed" },
155+
{ value: "recent", label: "Recent" },
156+
]}
157+
onChange={(o) => setDefaultSort(o?.value || "most-wanted")}
158+
/>
159+
146160
<div className="field">
147161
<Button disabled={!fider.session.user.isAdministrator} variant="primary" onClick={handleSave}>
148162
Save

0 commit comments

Comments
 (0)