view: implemented subreddits list page
This commit is contained in:
parent
4ca2e854d4
commit
e3bda647f3
|
@ -2,6 +2,8 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/stephenafamo/bob"
|
"github.com/stephenafamo/bob"
|
||||||
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
|
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
|
||||||
|
@ -11,16 +13,34 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListSubredditsParams struct {
|
type ListSubredditsParams struct {
|
||||||
Name string `json:"name"`
|
Q string
|
||||||
Limit int64 `json:"limit"`
|
All bool
|
||||||
Offset int64 `json:"offset"`
|
Limit int64
|
||||||
OrderBy string `json:"order_by"`
|
Offset int64
|
||||||
Sort string `json:"sort"`
|
OrderBy string
|
||||||
|
Sort string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ListSubredditsParams) FillFromQuery(q Queryable) {
|
||||||
|
l.Q = q.Get("q")
|
||||||
|
l.All, _ = strconv.ParseBool(q.Get("all"))
|
||||||
|
l.Limit, _ = strconv.ParseInt(q.Get("limit"), 10, 64)
|
||||||
|
if l.Limit < 0 {
|
||||||
|
l.Limit = 25
|
||||||
|
} else if l.Limit > 100 {
|
||||||
|
l.Limit = 100
|
||||||
|
}
|
||||||
|
l.Offset, _ = strconv.ParseInt(q.Get("offset"), 10, 64)
|
||||||
|
if l.Offset < 0 {
|
||||||
|
l.Offset = 0
|
||||||
|
}
|
||||||
|
l.OrderBy = q.Get("order_by")
|
||||||
|
l.Sort = strings.ToLower(q.Get("sort"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l ListSubredditsParams) Query() (expr []bob.Mod[*dialect.SelectQuery]) {
|
func (l ListSubredditsParams) Query() (expr []bob.Mod[*dialect.SelectQuery]) {
|
||||||
if l.Name != "" {
|
if l.Q != "" {
|
||||||
expr = append(expr, models.SelectWhere.Subreddits.Name.Like("%"+l.Name+"%"))
|
expr = append(expr, models.SelectWhere.Subreddits.Name.Like("%"+l.Q+"%"))
|
||||||
}
|
}
|
||||||
if l.Limit > 0 {
|
if l.Limit > 0 {
|
||||||
expr = append(expr, sm.Limit(l.Limit))
|
expr = append(expr, sm.Limit(l.Limit))
|
||||||
|
@ -34,14 +54,16 @@ func (l ListSubredditsParams) Query() (expr []bob.Mod[*dialect.SelectQuery]) {
|
||||||
} else {
|
} else {
|
||||||
expr = append(expr, sm.OrderBy(l.OrderBy).Asc())
|
expr = append(expr, sm.OrderBy(l.OrderBy).Asc())
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
expr = append(expr, sm.OrderBy(models.SubredditColumns.Name).Asc())
|
||||||
}
|
}
|
||||||
|
|
||||||
return expr
|
return expr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l ListSubredditsParams) CountQuery() (expr []bob.Mod[*dialect.SelectQuery]) {
|
func (l ListSubredditsParams) CountQuery() (expr []bob.Mod[*dialect.SelectQuery]) {
|
||||||
if l.Name != "" {
|
if l.Q != "" {
|
||||||
expr = append(expr, models.SelectWhere.Subreddits.Name.Like("%"+l.Name+"%"))
|
expr = append(expr, models.SelectWhere.Subreddits.Name.Like("%"+l.Q+"%"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return expr
|
return expr
|
||||||
|
@ -68,3 +90,33 @@ func (api *API) ListSubreddits(ctx context.Context, arg ListSubredditsParams) (r
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListSubredditsWithCover returns list of subreddits with cover image.
|
||||||
|
//
|
||||||
|
// Image Relationship `R` struct will be nil if there is no cover image.
|
||||||
|
func (api *API) ListSubredditsWithCover(ctx context.Context, arg ListSubredditsParams) (result ListSubredditsResult, err error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "api.ListSubredditsWithCover")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
result, err = api.ListSubreddits(ctx, arg)
|
||||||
|
if err != nil {
|
||||||
|
return result, errs.Wrapw(err, "failed to list subreddits with cover")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot do batch query because we cannot use GROUP BY with ORDER BY in consistent manner.
|
||||||
|
//
|
||||||
|
// The problem gets worse when using custom ORDER BY from client.
|
||||||
|
//
|
||||||
|
// For consistency, we query images one by one.
|
||||||
|
//
|
||||||
|
// Subreddit list is expected to be small, so this should be fine since SQLITE has no network latency.
|
||||||
|
for _, subreddit := range result.Data {
|
||||||
|
subreddit.R.Images, err = models.Images.Query(ctx, api.db,
|
||||||
|
models.SelectWhere.Images.Subreddit.EQ(subreddit.Name),
|
||||||
|
sm.Limit(1),
|
||||||
|
sm.OrderBy(models.ImageColumns.CreatedAt).Desc(),
|
||||||
|
).All()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
15
rest/devices/create_phone.http
Normal file
15
rest/devices/create_phone.http
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
POST http://localhost:8080/api/v1/devices HTTP/1.1
|
||||||
|
Host: localhost:8080
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Phone",
|
||||||
|
"slug": "phone",
|
||||||
|
"resolution_x": 1080,
|
||||||
|
"resolution_y": 2400,
|
||||||
|
"nsfw": 1,
|
||||||
|
"aspect_ratio_tolerance": 0.2,
|
||||||
|
"enable": 1,
|
||||||
|
"min_x": 1080,
|
||||||
|
"min_y": 2400
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ package routes
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/tigorlazuardi/redmage/api"
|
||||||
|
"github.com/tigorlazuardi/redmage/pkg/errs"
|
||||||
"github.com/tigorlazuardi/redmage/pkg/log"
|
"github.com/tigorlazuardi/redmage/pkg/log"
|
||||||
"github.com/tigorlazuardi/redmage/views"
|
"github.com/tigorlazuardi/redmage/views"
|
||||||
"github.com/tigorlazuardi/redmage/views/subredditsview"
|
"github.com/tigorlazuardi/redmage/views/subredditsview"
|
||||||
|
@ -12,12 +14,26 @@ func (routes *Routes) PageSubreddits(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx, span := tracer.Start(r.Context(), "*Routes.PageSubreddits")
|
ctx, span := tracer.Start(r.Context(), "*Routes.PageSubreddits")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
data := subredditsview.Data{
|
|
||||||
// Subreddits: routes.API.SubredditsList(ctx),
|
|
||||||
}
|
|
||||||
|
|
||||||
c := views.NewContext(routes.Config, r)
|
c := views.NewContext(routes.Config, r)
|
||||||
|
|
||||||
|
var params api.ListSubredditsParams
|
||||||
|
params.FillFromQuery(r.URL.Query())
|
||||||
|
|
||||||
|
var data subredditsview.Data
|
||||||
|
var err error
|
||||||
|
|
||||||
|
data.Subreddits, err = routes.API.ListSubredditsWithCover(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
log.New(ctx).Err(err).Error("failed to list subreddits")
|
||||||
|
code, message := errs.HTTPMessage(err)
|
||||||
|
rw.WriteHeader(code)
|
||||||
|
data.Error = message
|
||||||
|
if err := subredditsview.Subreddit(c, data).Render(ctx, rw); err != nil {
|
||||||
|
log.New(ctx).Err(err).Error("failed to render subreddits")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := subredditsview.Subreddit(c, data).Render(r.Context(), rw); err != nil {
|
if err := subredditsview.Subreddit(c, data).Render(r.Context(), rw); err != nil {
|
||||||
log.New(ctx).Err(err).Error("failed to render subreddits view")
|
log.New(ctx).Err(err).Error("failed to render subreddits view")
|
||||||
rw.WriteHeader(http.StatusInternalServerError)
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
|
@ -26,7 +26,7 @@ func (r *Routes) SubredditsListAPI(rw http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSubredditListQuery(req *http.Request) (params api.ListSubredditsParams) {
|
func parseSubredditListQuery(req *http.Request) (params api.ListSubredditsParams) {
|
||||||
params.Name = req.FormValue("name")
|
params.Q = req.FormValue("name")
|
||||||
params.Limit, _ = strconv.ParseInt(req.FormValue("limit"), 10, 64)
|
params.Limit, _ = strconv.ParseInt(req.FormValue("limit"), 10, 64)
|
||||||
if params.Limit < 1 {
|
if params.Limit < 1 {
|
||||||
params.Limit = 10
|
params.Limit = 10
|
||||||
|
|
|
@ -87,7 +87,7 @@ templ Navbar(c *views.Context) {
|
||||||
<span class="font-bold">Redmage</span>
|
<span class="font-bold">Redmage</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<nav class="pt-4" hx-boost="true" hx-select="main" hx-target="main">
|
<nav class="pt-4">
|
||||||
@navList(c)
|
@navList(c)
|
||||||
</nav>
|
</nav>
|
||||||
<div class="flex-grow"></div>
|
<div class="flex-grow"></div>
|
||||||
|
|
|
@ -18,14 +18,14 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
templ RecentlyAddedImageCard(data *models.Image, opts ImageCardOption) {
|
templ RecentlyAddedImageCard(data *models.Image, opts ImageCardOption) {
|
||||||
<div class="not-prose card card-bordered bg-base-100 hover:bg-base-200 shadow-xl min-w-[256px] rounded-xl top-0 hover:-top-1 hover:drop-shadow-2xl transition-all">
|
<div class="not-prose card card-bordered bg-base-100 hover:bg-base-200 shadow-xl min-w-[16rem] rounded-xl top-0 hover:-top-1 hover:drop-shadow-2xl transition-all">
|
||||||
<figure>
|
<figure>
|
||||||
<a
|
<a
|
||||||
href={ templ.URL(fmt.Sprintf("/img/%s", data.ImageRelativePath)) }
|
href={ templ.URL(fmt.Sprintf("/img/%s", data.ImageRelativePath)) }
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="object-contain max-w-[256px] max-h-[256px]"
|
class="object-contain max-w-[16rem] max-h-[16rem]"
|
||||||
src={ fmt.Sprintf("/img/%s", data.ThumbnailRelativePath) }
|
src={ fmt.Sprintf("/img/%s", data.ThumbnailRelativePath) }
|
||||||
alt={ data.PostTitle }
|
alt={ data.PostTitle }
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,6 +2,9 @@ package subredditsview
|
||||||
|
|
||||||
import "github.com/tigorlazuardi/redmage/views"
|
import "github.com/tigorlazuardi/redmage/views"
|
||||||
import "github.com/tigorlazuardi/redmage/views/components"
|
import "github.com/tigorlazuardi/redmage/views/components"
|
||||||
|
import "github.com/tigorlazuardi/redmage/models"
|
||||||
|
import "strconv"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
templ Subreddit(c *views.Context, data Data) {
|
templ Subreddit(c *views.Context, data Data) {
|
||||||
@components.Doctype() {
|
@components.Doctype() {
|
||||||
|
@ -17,13 +20,52 @@ templ SubredditContent(c *views.Context, data Data) {
|
||||||
@components.Container() {
|
@components.Container() {
|
||||||
<h1>Subreddits</h1>
|
<h1>Subreddits</h1>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
if len(data.Subreddits) == 0 {
|
if data.Subreddits.Total == 0 {
|
||||||
<h3>You have not added any subreddits yet.</h3>
|
<h3>No Subreddits Found</h3>
|
||||||
<p>Click <a class="text-primary" href="/subreddits/add">here</a> to add a new subreddit.</p>
|
<p>Click <a class="text-primary" href="/subreddits/add">here</a> to add a new subreddit.</p>
|
||||||
|
} else {
|
||||||
|
<h2>{ strconv.FormatInt(data.Subreddits.Total, 10) } Subreddits Registered</h2>
|
||||||
}
|
}
|
||||||
for _, subreddit := range data.Subreddits {
|
<div class="flex flex-wrap gap-1">
|
||||||
<p>{ subreddit.Name }</p>
|
for _, subreddit := range data.Subreddits.Data {
|
||||||
}
|
@SubredditCard(c, subreddit)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</main>
|
</main>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ SubredditCard(c *views.Context, data *models.Subreddit) {
|
||||||
|
<div class="not-prose card card-bordered bg-base-100 hover:bg-base-200 shadow-xl w-80 top-0 hover:-top-1 transition-all rounded-none">
|
||||||
|
if len(data.R.Images) > 0 {
|
||||||
|
<figure class="p-8">
|
||||||
|
<a class="flex content-center" href={ templ.URL(fmt.Sprintf("/subreddits/details/%s", data.Name)) }>
|
||||||
|
<img
|
||||||
|
class="object-contain max-w-[16rem] max-h-[16rem]"
|
||||||
|
src={ fmt.Sprintf("/img/%s", data.R.Images[0].ThumbnailRelativePath) }
|
||||||
|
alt={ data.Name }
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
} else {
|
||||||
|
<figure class="p-8">
|
||||||
|
<a href={ templ.URL(fmt.Sprintf("/subreddits/details/%s", data.Name)) }>
|
||||||
|
@imagePlaceholder()
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
}
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex-1"></div>
|
||||||
|
<a href={ templ.URL(fmt.Sprintf("/subreddits/details/%s", data.Name)) }>
|
||||||
|
<p class="text-center my-4 underline text-primary">{ data.Name }</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ imagePlaceholder() {
|
||||||
|
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2" y="2" width="252" height="252" style="fill:#DEDEDE;stroke:#555555;stroke-width:2"></rect>
|
||||||
|
<text x="50%" y="50%" font-size="18" text-anchor="middle" alignment-baseline="middle" font-family="monospace, sans-serif" fill="#555555">256×256</text>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package subredditsview
|
package subredditsview
|
||||||
|
|
||||||
import "github.com/tigorlazuardi/redmage/models"
|
import (
|
||||||
|
"github.com/tigorlazuardi/redmage/api"
|
||||||
|
)
|
||||||
|
|
||||||
type Data struct {
|
type Data struct {
|
||||||
Subreddits models.SubredditSlice
|
Subreddits api.ListSubredditsResult
|
||||||
|
Error string
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue