view: implemented subreddits list page
This commit is contained in:
parent
4ca2e854d4
commit
e3bda647f3
|
@ -2,6 +2,8 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/stephenafamo/bob"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
|
||||
|
@ -11,16 +13,34 @@ import (
|
|||
)
|
||||
|
||||
type ListSubredditsParams struct {
|
||||
Name string `json:"name"`
|
||||
Limit int64 `json:"limit"`
|
||||
Offset int64 `json:"offset"`
|
||||
OrderBy string `json:"order_by"`
|
||||
Sort string `json:"sort"`
|
||||
Q string
|
||||
All bool
|
||||
Limit int64
|
||||
Offset int64
|
||||
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]) {
|
||||
if l.Name != "" {
|
||||
expr = append(expr, models.SelectWhere.Subreddits.Name.Like("%"+l.Name+"%"))
|
||||
if l.Q != "" {
|
||||
expr = append(expr, models.SelectWhere.Subreddits.Name.Like("%"+l.Q+"%"))
|
||||
}
|
||||
if l.Limit > 0 {
|
||||
expr = append(expr, sm.Limit(l.Limit))
|
||||
|
@ -34,14 +54,16 @@ func (l ListSubredditsParams) Query() (expr []bob.Mod[*dialect.SelectQuery]) {
|
|||
} else {
|
||||
expr = append(expr, sm.OrderBy(l.OrderBy).Asc())
|
||||
}
|
||||
} else {
|
||||
expr = append(expr, sm.OrderBy(models.SubredditColumns.Name).Asc())
|
||||
}
|
||||
|
||||
return expr
|
||||
}
|
||||
|
||||
func (l ListSubredditsParams) CountQuery() (expr []bob.Mod[*dialect.SelectQuery]) {
|
||||
if l.Name != "" {
|
||||
expr = append(expr, models.SelectWhere.Subreddits.Name.Like("%"+l.Name+"%"))
|
||||
if l.Q != "" {
|
||||
expr = append(expr, models.SelectWhere.Subreddits.Name.Like("%"+l.Q+"%"))
|
||||
}
|
||||
|
||||
return expr
|
||||
|
@ -68,3 +90,33 @@ func (api *API) ListSubreddits(ctx context.Context, arg ListSubredditsParams) (r
|
|||
|
||||
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 (
|
||||
"net/http"
|
||||
|
||||
"github.com/tigorlazuardi/redmage/api"
|
||||
"github.com/tigorlazuardi/redmage/pkg/errs"
|
||||
"github.com/tigorlazuardi/redmage/pkg/log"
|
||||
"github.com/tigorlazuardi/redmage/views"
|
||||
"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")
|
||||
defer span.End()
|
||||
|
||||
data := subredditsview.Data{
|
||||
// Subreddits: routes.API.SubredditsList(ctx),
|
||||
}
|
||||
|
||||
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 {
|
||||
log.New(ctx).Err(err).Error("failed to render subreddits view")
|
||||
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) {
|
||||
params.Name = req.FormValue("name")
|
||||
params.Q = req.FormValue("name")
|
||||
params.Limit, _ = strconv.ParseInt(req.FormValue("limit"), 10, 64)
|
||||
if params.Limit < 1 {
|
||||
params.Limit = 10
|
||||
|
|
|
@ -87,7 +87,7 @@ templ Navbar(c *views.Context) {
|
|||
<span class="font-bold">Redmage</span>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<nav class="pt-4" hx-boost="true" hx-select="main" hx-target="main">
|
||||
<nav class="pt-4">
|
||||
@navList(c)
|
||||
</nav>
|
||||
<div class="flex-grow"></div>
|
||||
|
|
|
@ -18,14 +18,14 @@ const (
|
|||
)
|
||||
|
||||
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>
|
||||
<a
|
||||
href={ templ.URL(fmt.Sprintf("/img/%s", data.ImageRelativePath)) }
|
||||
target="_blank"
|
||||
>
|
||||
<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) }
|
||||
alt={ data.PostTitle }
|
||||
/>
|
||||
|
|
|
@ -2,6 +2,9 @@ package subredditsview
|
|||
|
||||
import "github.com/tigorlazuardi/redmage/views"
|
||||
import "github.com/tigorlazuardi/redmage/views/components"
|
||||
import "github.com/tigorlazuardi/redmage/models"
|
||||
import "strconv"
|
||||
import "fmt"
|
||||
|
||||
templ Subreddit(c *views.Context, data Data) {
|
||||
@components.Doctype() {
|
||||
|
@ -17,13 +20,52 @@ templ SubredditContent(c *views.Context, data Data) {
|
|||
@components.Container() {
|
||||
<h1>Subreddits</h1>
|
||||
<div class="divider"></div>
|
||||
if len(data.Subreddits) == 0 {
|
||||
<h3>You have not added any subreddits yet.</h3>
|
||||
if data.Subreddits.Total == 0 {
|
||||
<h3>No Subreddits Found</h3>
|
||||
<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 {
|
||||
<p>{ subreddit.Name }</p>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
for _, subreddit := range data.Subreddits.Data {
|
||||
@SubredditCard(c, subreddit)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</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
|
||||
|
||||
import "github.com/tigorlazuardi/redmage/models"
|
||||
import (
|
||||
"github.com/tigorlazuardi/redmage/api"
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
Subreddits models.SubredditSlice
|
||||
Subreddits api.ListSubredditsResult
|
||||
Error string
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue