Skip to content

Commit 5f700c9

Browse files
committed
Frontend enhancements, autocomplete tags, ability to add tags in extension, fun stuff
1 parent 68d26ff commit 5f700c9

28 files changed

Lines changed: 1022 additions & 237 deletions

File tree

backend/internal/api/annotations.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -757,9 +757,10 @@ func (s *AnnotationService) CreateHighlight(w http.ResponseWriter, r *http.Reque
757757
}
758758

759759
type CreateBookmarkRequest struct {
760-
URL string `json:"url"`
761-
Title string `json:"title,omitempty"`
762-
Description string `json:"description,omitempty"`
760+
URL string `json:"url"`
761+
Title string `json:"title,omitempty"`
762+
Description string `json:"description,omitempty"`
763+
Tags []string `json:"tags,omitempty"`
763764
}
764765

765766
func (s *AnnotationService) CreateBookmark(w http.ResponseWriter, r *http.Request) {
@@ -782,6 +783,9 @@ func (s *AnnotationService) CreateBookmark(w http.ResponseWriter, r *http.Reques
782783

783784
urlHash := db.HashURL(req.URL)
784785
record := xrpc.NewBookmarkRecord(req.URL, urlHash, req.Title, req.Description)
786+
if len(req.Tags) > 0 {
787+
record.Tags = req.Tags
788+
}
785789

786790
if err := record.Validate(); err != nil {
787791
http.Error(w, "Validation error: "+err.Error(), http.StatusBadRequest)
@@ -814,6 +818,13 @@ func (s *AnnotationService) CreateBookmark(w http.ResponseWriter, r *http.Reques
814818
descPtr = &req.Description
815819
}
816820

821+
var tagsJSONPtr *string
822+
if len(req.Tags) > 0 {
823+
tagsBytes, _ := json.Marshal(req.Tags)
824+
tagsStr := string(tagsBytes)
825+
tagsJSONPtr = &tagsStr
826+
}
827+
817828
cid := result.CID
818829
bookmark := &db.Bookmark{
819830
URI: result.URI,
@@ -822,6 +833,7 @@ func (s *AnnotationService) CreateBookmark(w http.ResponseWriter, r *http.Reques
822833
SourceHash: urlHash,
823834
Title: titlePtr,
824835
Description: descPtr,
836+
TagsJSON: tagsJSONPtr,
825837
CreatedAt: time.Now(),
826838
IndexedAt: time.Now(),
827839
CID: &cid,

backend/internal/api/handler.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ func (h *Handler) RegisterRoutes(r chi.Router) {
7777
r.Get("/users/{did}/highlights", h.GetUserHighlights)
7878
r.Get("/users/{did}/bookmarks", h.GetUserBookmarks)
7979
r.Get("/users/{did}/targets", h.GetUserTargetItems)
80+
r.Get("/users/{did}/tags", h.HandleGetUserTags)
81+
82+
r.Get("/trending-tags", h.HandleGetTrendingTags)
8083

8184
r.Get("/replies", h.GetReplies)
8285
r.Get("/likes", h.GetLikeCount)

backend/internal/api/tags.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"encoding/json"
55
"net/http"
66
"strconv"
7+
8+
"github.com/go-chi/chi/v5"
79
)
810

911
func (h *Handler) HandleGetTrendingTags(w http.ResponseWriter, r *http.Request) {
@@ -23,3 +25,27 @@ func (h *Handler) HandleGetTrendingTags(w http.ResponseWriter, r *http.Request)
2325
w.Header().Set("Content-Type", "application/json")
2426
json.NewEncoder(w).Encode(tags)
2527
}
28+
29+
func (h *Handler) HandleGetUserTags(w http.ResponseWriter, r *http.Request) {
30+
did := chi.URLParam(r, "did")
31+
if did == "" {
32+
http.Error(w, `{"error": "did is required"}`, http.StatusBadRequest)
33+
return
34+
}
35+
36+
limit := 50
37+
if l := r.URL.Query().Get("limit"); l != "" {
38+
if val, err := strconv.Atoi(l); err == nil && val > 0 && val <= 100 {
39+
limit = val
40+
}
41+
}
42+
43+
tags, err := h.db.GetUserTags(did, limit)
44+
if err != nil {
45+
http.Error(w, `{"error": "Failed to fetch user tags"}`, http.StatusInternalServerError)
46+
return
47+
}
48+
49+
w.Header().Set("Content-Type", "application/json")
50+
json.NewEncoder(w).Encode(tags)
51+
}

backend/internal/db/tags.go

Lines changed: 133 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package db
22

3+
import "database/sql"
4+
35
type TrendingTag struct {
46
Tag string `json:"tag"`
57
Count int `json:"count"`
@@ -9,51 +11,145 @@ func (db *DB) GetTrendingTags(limit int) ([]TrendingTag, error) {
911
var query string
1012
if db.driver == "postgres" {
1113
query = `
12-
SELECT
13-
value as tag,
14-
COUNT(*) as count
15-
FROM annotations, json_array_elements_text(tags_json::json) as value
16-
WHERE tags_json IS NOT NULL
17-
AND tags_json != ''
18-
AND tags_json != '[]'
19-
AND created_at > NOW() - INTERVAL '7 days'
14+
SELECT tag, SUM(cnt) as count FROM (
15+
SELECT value as tag, COUNT(*) as cnt
16+
FROM annotations, json_array_elements_text(tags_json::json) as value
17+
WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
18+
AND created_at > NOW() - INTERVAL '14 days'
19+
GROUP BY tag
20+
UNION ALL
21+
SELECT value as tag, COUNT(*) as cnt
22+
FROM highlights, json_array_elements_text(tags_json::json) as value
23+
WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
24+
AND created_at > NOW() - INTERVAL '14 days'
25+
GROUP BY tag
26+
UNION ALL
27+
SELECT value as tag, COUNT(*) as cnt
28+
FROM bookmarks, json_array_elements_text(tags_json::json) as value
29+
WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
30+
AND created_at > NOW() - INTERVAL '14 days'
31+
GROUP BY tag
32+
) combined
2033
GROUP BY tag
21-
HAVING COUNT(*) > 2
22-
ORDER BY COUNT(*) DESC
34+
HAVING SUM(cnt) >= 2
35+
ORDER BY count DESC
2336
LIMIT $1
2437
`
25-
rows, err := db.Query(query, limit)
26-
if err != nil {
38+
} else {
39+
query = `
40+
SELECT tag, SUM(cnt) as count FROM (
41+
SELECT json_each.value as tag, COUNT(*) as cnt
42+
FROM annotations, json_each(annotations.tags_json)
43+
WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
44+
AND created_at > datetime('now', '-14 days')
45+
GROUP BY tag
46+
UNION ALL
47+
SELECT json_each.value as tag, COUNT(*) as cnt
48+
FROM highlights, json_each(highlights.tags_json)
49+
WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
50+
AND created_at > datetime('now', '-14 days')
51+
GROUP BY tag
52+
UNION ALL
53+
SELECT json_each.value as tag, COUNT(*) as cnt
54+
FROM bookmarks, json_each(bookmarks.tags_json)
55+
WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
56+
AND created_at > datetime('now', '-14 days')
57+
GROUP BY tag
58+
) combined
59+
GROUP BY tag
60+
HAVING SUM(cnt) >= 2
61+
ORDER BY count DESC
62+
LIMIT ?
63+
`
64+
}
65+
66+
var rows *sql.Rows
67+
var err error
68+
if db.driver == "postgres" {
69+
rows, err = db.Query(query, limit)
70+
} else {
71+
rows, err = db.Query(db.Rebind(query), limit)
72+
}
73+
if err != nil {
74+
return nil, err
75+
}
76+
defer rows.Close()
77+
78+
var tags []TrendingTag
79+
for rows.Next() {
80+
var t TrendingTag
81+
if err := rows.Scan(&t.Tag, &t.Count); err != nil {
2782
return nil, err
2883
}
29-
defer rows.Close()
84+
tags = append(tags, t)
85+
}
3086

31-
var tags []TrendingTag
32-
for rows.Next() {
33-
var t TrendingTag
34-
if err := rows.Scan(&t.Tag, &t.Count); err != nil {
35-
return nil, err
36-
}
37-
tags = append(tags, t)
38-
}
39-
return tags, nil
87+
if err = rows.Err(); err != nil {
88+
return nil, err
89+
}
90+
91+
if tags == nil {
92+
return []TrendingTag{}, nil
93+
}
94+
95+
return tags, nil
96+
}
97+
98+
func (db *DB) GetUserTags(did string, limit int) ([]TrendingTag, error) {
99+
var query string
100+
if db.driver == "postgres" {
101+
query = `
102+
SELECT tag, SUM(cnt) as count FROM (
103+
SELECT value as tag, COUNT(*) as cnt
104+
FROM annotations, json_array_elements_text(tags_json::json) as value
105+
WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
106+
GROUP BY tag
107+
UNION ALL
108+
SELECT value as tag, COUNT(*) as cnt
109+
FROM highlights, json_array_elements_text(tags_json::json) as value
110+
WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
111+
GROUP BY tag
112+
UNION ALL
113+
SELECT value as tag, COUNT(*) as cnt
114+
FROM bookmarks, json_array_elements_text(tags_json::json) as value
115+
WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
116+
GROUP BY tag
117+
) combined
118+
GROUP BY tag
119+
ORDER BY count DESC
120+
LIMIT $2
121+
`
122+
} else {
123+
query = `
124+
SELECT tag, SUM(cnt) as count FROM (
125+
SELECT json_each.value as tag, COUNT(*) as cnt
126+
FROM annotations, json_each(annotations.tags_json)
127+
WHERE author_did = ? AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
128+
GROUP BY tag
129+
UNION ALL
130+
SELECT json_each.value as tag, COUNT(*) as cnt
131+
FROM highlights, json_each(highlights.tags_json)
132+
WHERE author_did = ? AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
133+
GROUP BY tag
134+
UNION ALL
135+
SELECT json_each.value as tag, COUNT(*) as cnt
136+
FROM bookmarks, json_each(bookmarks.tags_json)
137+
WHERE author_did = ? AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
138+
GROUP BY tag
139+
) combined
140+
GROUP BY tag
141+
ORDER BY count DESC
142+
LIMIT ?
143+
`
40144
}
41145

42-
query = `
43-
SELECT
44-
json_each.value as tag,
45-
COUNT(*) as count
46-
FROM annotations, json_each(annotations.tags_json)
47-
WHERE tags_json IS NOT NULL
48-
AND tags_json != ''
49-
AND tags_json != '[]'
50-
AND created_at > datetime('now', '-7 days')
51-
GROUP BY tag
52-
HAVING count > 2
53-
ORDER BY count DESC
54-
LIMIT ?
55-
`
56-
rows, err := db.Query(db.Rebind(query), limit)
146+
var rows *sql.Rows
147+
var err error
148+
if db.driver == "postgres" {
149+
rows, err = db.Query(query, did, limit)
150+
} else {
151+
rows, err = db.Query(db.Rebind(query), did, did, did, limit)
152+
}
57153
if err != nil {
58154
return nil, err
59155
}

extension/src/assets/styles.css

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,41 @@
55
@tailwind utilities;
66

77
:root {
8-
--bg-primary: #020617;
9-
--bg-secondary: #0f172a;
10-
--bg-tertiary: #1e293b;
11-
--bg-card: #0f172a;
12-
--bg-elevated: #1e293b;
13-
--bg-hover: #1e293b;
14-
--text-primary: #f8fafc;
15-
--text-secondary: #94a3b8;
16-
--text-tertiary: #64748b;
17-
--border: rgba(148, 163, 184, 0.1);
18-
--border-strong: rgba(148, 163, 184, 0.2);
19-
--accent: #8b5cf6;
20-
--accent-hover: #a78bfa;
21-
--accent-subtle: rgba(139, 92, 246, 0.12);
8+
--bg-primary: #0c0c0c;
9+
--bg-secondary: #141414;
10+
--bg-tertiary: #1c1c1c;
11+
--bg-card: #161616;
12+
--bg-elevated: #1e1e1e;
13+
--bg-hover: #262626;
14+
--text-primary: #e8e8e3;
15+
--text-secondary: #a1a09a;
16+
--text-tertiary: #6b6a65;
17+
--border: rgba(255, 255, 255, 0.08);
18+
--border-strong: rgba(255, 255, 255, 0.14);
19+
--accent: #7aa2f7;
20+
--accent-hover: #9bbcff;
21+
--accent-subtle: rgba(122, 162, 247, 0.14);
2222
--success: #34d399;
2323
--warning: #fbbf24;
24-
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
25-
--shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.4);
24+
--shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
25+
--shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.5);
2626
}
2727

2828
.light {
29-
--bg-primary: #f8fafc;
29+
--bg-primary: #fafaf8;
3030
--bg-secondary: #ffffff;
31-
--bg-tertiary: #f1f5f9;
31+
--bg-tertiary: #f2f2ef;
3232
--bg-card: #ffffff;
3333
--bg-elevated: #ffffff;
34-
--bg-hover: #f1f5f9;
35-
--text-primary: #0f172a;
36-
--text-secondary: #64748b;
37-
--text-tertiary: #94a3b8;
38-
--border: rgba(100, 116, 139, 0.12);
39-
--border-strong: rgba(100, 116, 139, 0.2);
40-
--accent: #7c3aed;
41-
--accent-hover: #6d28d9;
42-
--accent-subtle: rgba(124, 58, 237, 0.08);
34+
--bg-hover: #eaeae6;
35+
--text-primary: #1a1a18;
36+
--text-secondary: #6b6a65;
37+
--text-tertiary: #a1a09a;
38+
--border: rgba(0, 0, 0, 0.08);
39+
--border-strong: rgba(0, 0, 0, 0.14);
40+
--accent: #3b82f6;
41+
--accent-hover: #2563eb;
42+
--accent-subtle: rgba(59, 130, 246, 0.08);
4343
--shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
4444
--shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.08);
4545
}

0 commit comments

Comments
 (0)