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
141
internal/data/tag/queries.go
Normal file
141
internal/data/tag/queries.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package tag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Row struct {
|
||||
ID int64
|
||||
Name string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ItemTag struct {
|
||||
ItemID int64
|
||||
TagID int64
|
||||
Name string
|
||||
}
|
||||
|
||||
func QList(ctx context.Context, db *sql.DB) ([]Row, error) {
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT id, name, created_at
|
||||
FROM tag
|
||||
ORDER BY name ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tags []Row
|
||||
for rows.Next() {
|
||||
var row Row
|
||||
if err := rows.Scan(&row.ID, &row.Name, &row.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, row)
|
||||
}
|
||||
return tags, rows.Err()
|
||||
}
|
||||
|
||||
func QListByItem(ctx context.Context, db *sql.DB, itemID int64) ([]Row, error) {
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT t.id, t.name, t.created_at
|
||||
FROM tag t
|
||||
JOIN item_tag it ON it.tag_id = t.id
|
||||
WHERE it.item_id = $1
|
||||
ORDER BY t.name ASC
|
||||
`, itemID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tags []Row
|
||||
for rows.Next() {
|
||||
var row Row
|
||||
if err := rows.Scan(&row.ID, &row.Name, &row.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, row)
|
||||
}
|
||||
return tags, rows.Err()
|
||||
}
|
||||
|
||||
func QListItemTags(ctx context.Context, db *sql.DB) ([]ItemTag, error) {
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT it.item_id, t.id, t.name
|
||||
FROM item_tag it
|
||||
JOIN tag t ON t.id = it.tag_id
|
||||
ORDER BY t.name ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var entries []ItemTag
|
||||
for rows.Next() {
|
||||
var row ItemTag
|
||||
if err := rows.Scan(&row.ItemID, &row.TagID, &row.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, row)
|
||||
}
|
||||
return entries, rows.Err()
|
||||
}
|
||||
|
||||
func QUpsert(ctx context.Context, db *sql.DB, name string) (Row, error) {
|
||||
cleaned := strings.TrimSpace(strings.ToLower(name))
|
||||
var row Row
|
||||
err := db.QueryRowContext(ctx, `
|
||||
INSERT INTO tag (name)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id, name, created_at
|
||||
`, cleaned).Scan(&row.ID, &row.Name, &row.CreatedAt)
|
||||
return row, err
|
||||
}
|
||||
|
||||
func QReplaceItemTags(ctx context.Context, db *sql.DB, itemID int64, names []string) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
_, err = tx.ExecContext(ctx, `DELETE FROM item_tag WHERE item_id = $1`, itemID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
continue
|
||||
}
|
||||
var tagID int64
|
||||
err = tx.QueryRowContext(ctx, `
|
||||
INSERT INTO tag (name)
|
||||
VALUES ($1)
|
||||
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
||||
RETURNING id
|
||||
`, strings.TrimSpace(strings.ToLower(name))).Scan(&tagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO item_tag (item_id, tag_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
`, itemID, tagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue