Initial lookbook implementation
Pinterest-style visual bookmarking app with: - URL metadata extraction (OG/Twitter meta, oEmbed fallback) - Image caching in Postgres with 480px thumbnails - Multi-tag filtering with Ctrl/Cmd for OR mode - Fuzzy tag suggestions and inline tag editing - Browser console auth() with first-use password setup - Brutalist UI with Commit Mono font and Pico CSS - Light/dark mode via browser preference
This commit is contained in:
commit
fc625fb9cf
486 changed files with 195373 additions and 0 deletions
145
internal/handlers/gallery.go
Normal file
145
internal/handlers/gallery.go
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.soup.land/soup/lookbook/internal/components"
|
||||
"git.soup.land/soup/lookbook/internal/data/image"
|
||||
"git.soup.land/soup/lookbook/internal/data/item"
|
||||
"git.soup.land/soup/lookbook/internal/data/tag"
|
||||
"git.soup.land/soup/sxgo/ssr"
|
||||
)
|
||||
|
||||
type galleryPageData struct {
|
||||
Items []galleryItem
|
||||
Tags []tag.Row
|
||||
HasAuth bool
|
||||
TagNames []string
|
||||
}
|
||||
|
||||
type galleryItem struct {
|
||||
Item item.Row
|
||||
Thumb *image.Ref
|
||||
TagNames []string
|
||||
TagListText string
|
||||
}
|
||||
|
||||
func (g galleryPageData) Render(sw *ssr.Writer) error {
|
||||
return sw.Tmpl(g, `
|
||||
<section class="hero">
|
||||
<form method="POST" action="/items" data-auth-required {{if not .HasAuth}}hidden{{end}}>
|
||||
<input type="url" name="url" placeholder="Paste a link" required>
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
<div class="notice" data-auth-status>Read only</div>
|
||||
</section>
|
||||
|
||||
<section class="filters">
|
||||
{{range .Tags}}
|
||||
<button type="button" class="filter-pill" data-tag-filter="{{.Name}}">{{.Name}}</button>
|
||||
{{end}}
|
||||
<div class="tag-suggestions" data-tag-suggestions data-tags='{{json .TagNames}}'></div>
|
||||
</section>
|
||||
|
||||
<section class="gallery">
|
||||
{{range .Items}}
|
||||
<div class="card" data-item-tags="{{.TagListText}}">
|
||||
<a href="/items/{{.Item.ID}}">
|
||||
{{if .Thumb}}
|
||||
<img src="/images/{{.Thumb.ID}}" alt="{{.Item.Title}}">
|
||||
{{else}}
|
||||
<div class="placeholder">{{if .Item.Title}}{{.Item.Title}}{{else}}{{.Item.SourceURL}}{{end}}</div>
|
||||
{{end}}
|
||||
<div class="overlay">
|
||||
<div class="title">{{if .Item.Title}}{{.Item.Title}}{{else}}{{.Item.SourceURL}}{{end}}</div>
|
||||
<div class="tags">{{range $i, $tag := .TagNames}}{{if $i}} · {{end}}{{$tag}}{{end}}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
`)
|
||||
}
|
||||
|
||||
func HandleGetGallery(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
|
||||
items, err := item.QList(r.Context(), rc.DB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list items: %w", err)
|
||||
}
|
||||
|
||||
var itemIDs []int64
|
||||
for _, row := range items {
|
||||
itemIDs = append(itemIDs, row.ID)
|
||||
}
|
||||
|
||||
thumbs, err := image.QListPrimaryRefsByItems(r.Context(), rc.DB, itemIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list thumbs: %w", err)
|
||||
}
|
||||
|
||||
entries, err := tag.QListItemTags(r.Context(), rc.DB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list tags: %w", err)
|
||||
}
|
||||
|
||||
itemTagNames := make(map[int64][]string)
|
||||
for _, entry := range entries {
|
||||
itemTagNames[entry.ItemID] = append(itemTagNames[entry.ItemID], entry.Name)
|
||||
}
|
||||
|
||||
var cards []galleryItem
|
||||
for _, row := range items {
|
||||
tagNames := itemTagNames[row.ID]
|
||||
cards = append(cards, galleryItem{
|
||||
Item: row,
|
||||
Thumb: refOrNil(thumbs[row.ID]),
|
||||
TagNames: tagNames,
|
||||
TagListText: strings.Join(tagNames, ","),
|
||||
})
|
||||
}
|
||||
|
||||
tags, err := tag.QList(r.Context(), rc.DB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list tags: %w", err)
|
||||
}
|
||||
|
||||
authed, err := isAuthenticated(r, rc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auth status: %w", err)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
sw := ssr.NewWriter(w, rc.TmplCache)
|
||||
|
||||
var tagNames []string
|
||||
for _, row := range tags {
|
||||
tagNames = append(tagNames, row.Name)
|
||||
}
|
||||
|
||||
data := galleryPageData{
|
||||
Items: cards,
|
||||
Tags: tags,
|
||||
HasAuth: authed,
|
||||
TagNames: tagNames,
|
||||
}
|
||||
return components.Page{
|
||||
Title: "Gallery",
|
||||
Content: data,
|
||||
ShowNav: true,
|
||||
HasAuth: authed,
|
||||
}.Render(sw)
|
||||
}
|
||||
|
||||
func refOrNil(ref image.Ref) *image.Ref {
|
||||
if ref.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
return &ref
|
||||
}
|
||||
|
||||
func parseID(value string) (int64, error) {
|
||||
return strconv.ParseInt(value, 10, 64)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue