tangled
alpha
login
or
join now
arabica.social
/
arabica
7
fork
atom
Coffee journaling on ATProto (alpha)
alpha.arabica.social
coffee
7
fork
atom
overview
issues
pulls
pipelines
feat: view brew page
pdewey.com
1 month ago
e217109b
0b9b80ec
verified
This commit was signed with the committer's
known signature
.
pdewey.com
SSH Key Fingerprint:
SHA256:ePOVkJstqVLchGK8m9/OGQG+aFNHD5XN3xjvW9wKCA4=
+224
-1
6 changed files
expand all
collapse all
unified
split
internal
bff
render.go
handlers
handlers.go
routing
routing.go
templates
brew_view.tmpl
partials
brew_list_content.tmpl
cards
_placeholder.tmpl
+22
internal/bff/render.go
···
240
240
return t.ExecuteTemplate(w, "layout", data)
241
241
}
242
242
243
243
+
// RenderBrewView renders the brew view page
244
244
+
func RenderBrewView(w http.ResponseWriter, brew *models.Brew, isAuthenticated bool, userDID string, userProfile *UserProfile) error {
245
245
+
t, err := parsePageTemplate("brew_view.tmpl")
246
246
+
if err != nil {
247
247
+
return err
248
248
+
}
249
249
+
250
250
+
brewData := &BrewData{
251
251
+
Brew: brew,
252
252
+
PoursJSON: PoursToJSON(brew.Pours),
253
253
+
}
254
254
+
255
255
+
data := &PageData{
256
256
+
Title: "View Brew",
257
257
+
Brew: brewData,
258
258
+
IsAuthenticated: isAuthenticated,
259
259
+
UserDID: userDID,
260
260
+
UserProfile: userProfile,
261
261
+
}
262
262
+
return t.ExecuteTemplate(w, "layout", data)
263
263
+
}
264
264
+
243
265
// RenderManage renders the manage page
244
266
func RenderManage(w http.ResponseWriter, beans []*models.Bean, roasters []*models.Roaster, grinders []*models.Grinder, brewers []*models.Brewer, isAuthenticated bool, userDID string, userProfile *UserProfile) error {
245
267
t, err := parsePageTemplate("manage.tmpl")
+30
internal/handlers/handlers.go
···
295
295
}
296
296
}
297
297
298
298
+
// Show brew view page
299
299
+
func (h *Handler) HandleBrewView(w http.ResponseWriter, r *http.Request) {
300
300
+
rkey := validateRKey(w, r.PathValue("id"))
301
301
+
if rkey == "" {
302
302
+
return
303
303
+
}
304
304
+
305
305
+
// Check authentication (optional for view)
306
306
+
store, authenticated := h.getAtprotoStore(r)
307
307
+
if !authenticated {
308
308
+
http.Redirect(w, r, "/login", http.StatusFound)
309
309
+
return
310
310
+
}
311
311
+
312
312
+
didStr, _ := atproto.GetAuthenticatedDID(r.Context())
313
313
+
userProfile := h.getUserProfile(r.Context(), didStr)
314
314
+
315
315
+
brew, err := store.GetBrewByRKey(r.Context(), rkey)
316
316
+
if err != nil {
317
317
+
http.Error(w, "Brew not found", http.StatusNotFound)
318
318
+
log.Error().Err(err).Str("rkey", rkey).Msg("Failed to get brew for view")
319
319
+
return
320
320
+
}
321
321
+
322
322
+
if err := bff.RenderBrewView(w, brew, authenticated, didStr, userProfile); err != nil {
323
323
+
http.Error(w, "Failed to render page", http.StatusInternalServerError)
324
324
+
log.Error().Err(err).Msg("Failed to render brew view")
325
325
+
}
326
326
+
}
327
327
+
298
328
// Show edit brew form
299
329
func (h *Handler) HandleBrewEdit(w http.ResponseWriter, r *http.Request) {
300
330
rkey := validateRKey(w, r.PathValue("id"))
+2
-1
internal/routing/routing.go
···
56
56
mux.HandleFunc("GET /manage", h.HandleManage)
57
57
mux.HandleFunc("GET /brews", h.HandleBrewList)
58
58
mux.HandleFunc("GET /brews/new", h.HandleBrewNew)
59
59
-
mux.HandleFunc("GET /brews/{id}", h.HandleBrewEdit)
59
59
+
mux.HandleFunc("GET /brews/{id}", h.HandleBrewView)
60
60
+
mux.HandleFunc("GET /brews/{id}/edit", h.HandleBrewEdit)
60
61
mux.Handle("POST /brews", cop.Handler(http.HandlerFunc(h.HandleBrewCreate)))
61
62
mux.Handle("PUT /brews/{id}", cop.Handler(http.HandlerFunc(h.HandleBrewUpdate)))
62
63
mux.Handle("DELETE /brews/{id}", cop.Handler(http.HandlerFunc(h.HandleBrewDelete)))
+167
templates/brew_view.tmpl
···
1
1
+
{{define "content"}}
2
2
+
<div class="max-w-2xl mx-auto">
3
3
+
<div class="bg-gradient-to-br from-brown-100 to-brown-200 rounded-xl shadow-xl p-8 border border-brown-300">
4
4
+
<!-- Header with title and actions -->
5
5
+
<div class="flex justify-between items-start mb-6">
6
6
+
<div>
7
7
+
<h2 class="text-3xl font-bold text-brown-900">Brew Details</h2>
8
8
+
<p class="text-sm text-brown-600 mt-1">{{.Brew.CreatedAt.Format "January 2, 2006 at 3:04 PM"}}</p>
9
9
+
</div>
10
10
+
<div class="flex gap-2">
11
11
+
<a href="/brews/{{.Brew.RKey}}/edit"
12
12
+
class="inline-flex items-center bg-brown-300 text-brown-900 px-4 py-2 rounded-lg hover:bg-brown-400 font-medium transition-colors">
13
13
+
Edit
14
14
+
</a>
15
15
+
<button
16
16
+
hx-delete="/brews/{{.Brew.RKey}}"
17
17
+
hx-confirm="Are you sure you want to delete this brew?"
18
18
+
hx-target="body"
19
19
+
class="inline-flex items-center bg-brown-200 text-brown-700 px-4 py-2 rounded-lg hover:bg-brown-300 font-medium transition-colors">
20
20
+
Delete
21
21
+
</button>
22
22
+
</div>
23
23
+
</div>
24
24
+
25
25
+
<div class="space-y-6">
26
26
+
<!-- Rating (prominent at top) -->
27
27
+
{{if hasValue .Brew.Rating}}
28
28
+
<div class="text-center py-4 bg-brown-50 rounded-lg border border-brown-200">
29
29
+
<div class="text-4xl font-bold text-brown-800">
30
30
+
{{.Brew.Rating}}/10
31
31
+
</div>
32
32
+
<div class="text-sm text-brown-600 mt-1">Rating</div>
33
33
+
</div>
34
34
+
{{end}}
35
35
+
36
36
+
<!-- Coffee Bean -->
37
37
+
<div class="bg-brown-50 rounded-lg p-4 border border-brown-200">
38
38
+
<h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">Coffee Bean</h3>
39
39
+
{{if .Brew.Bean}}
40
40
+
<div class="font-bold text-lg text-brown-900">
41
41
+
{{if .Brew.Bean.Name}}{{.Brew.Bean.Name}}{{else}}{{.Brew.Bean.Origin}}{{end}}
42
42
+
</div>
43
43
+
{{if and .Brew.Bean.Roaster .Brew.Bean.Roaster.Name}}
44
44
+
<div class="text-sm text-brown-700 mt-1">
45
45
+
by {{.Brew.Bean.Roaster.Name}}
46
46
+
</div>
47
47
+
{{end}}
48
48
+
<div class="flex flex-wrap gap-3 mt-2 text-sm text-brown-600">
49
49
+
{{if .Brew.Bean.Origin}}<span>Origin: {{.Brew.Bean.Origin}}</span>{{end}}
50
50
+
{{if .Brew.Bean.RoastLevel}}<span>Roast: {{.Brew.Bean.RoastLevel}}</span>{{end}}
51
51
+
</div>
52
52
+
{{else}}
53
53
+
<span class="text-brown-400">Not specified</span>
54
54
+
{{end}}
55
55
+
</div>
56
56
+
57
57
+
<!-- Brew Parameters -->
58
58
+
<div class="grid grid-cols-2 gap-4">
59
59
+
<!-- Brew Method -->
60
60
+
<div class="bg-brown-50 rounded-lg p-4 border border-brown-200">
61
61
+
<h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">Brew Method</h3>
62
62
+
{{if .Brew.BrewerObj}}
63
63
+
<div class="font-semibold text-brown-900">{{.Brew.BrewerObj.Name}}</div>
64
64
+
{{else if .Brew.Method}}
65
65
+
<div class="font-semibold text-brown-900">{{.Brew.Method}}</div>
66
66
+
{{else}}
67
67
+
<span class="text-brown-400">Not specified</span>
68
68
+
{{end}}
69
69
+
</div>
70
70
+
71
71
+
<!-- Grinder -->
72
72
+
<div class="bg-brown-50 rounded-lg p-4 border border-brown-200">
73
73
+
<h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">Grinder</h3>
74
74
+
{{if .Brew.GrinderObj}}
75
75
+
<div class="font-semibold text-brown-900">{{.Brew.GrinderObj.Name}}</div>
76
76
+
{{else}}
77
77
+
<span class="text-brown-400">Not specified</span>
78
78
+
{{end}}
79
79
+
</div>
80
80
+
81
81
+
<!-- Coffee Amount -->
82
82
+
<div class="bg-brown-50 rounded-lg p-4 border border-brown-200">
83
83
+
<h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">Coffee</h3>
84
84
+
{{if hasValue .Brew.CoffeeAmount}}
85
85
+
<div class="font-semibold text-brown-900">{{.Brew.CoffeeAmount}}g</div>
86
86
+
{{else}}
87
87
+
<span class="text-brown-400">Not specified</span>
88
88
+
{{end}}
89
89
+
</div>
90
90
+
91
91
+
<!-- Water Amount -->
92
92
+
<div class="bg-brown-50 rounded-lg p-4 border border-brown-200">
93
93
+
<h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">Water</h3>
94
94
+
{{if hasValue .Brew.WaterAmount}}
95
95
+
<div class="font-semibold text-brown-900">{{.Brew.WaterAmount}}g</div>
96
96
+
{{else}}
97
97
+
<span class="text-brown-400">Not specified</span>
98
98
+
{{end}}
99
99
+
</div>
100
100
+
101
101
+
<!-- Grind Size -->
102
102
+
<div class="bg-brown-50 rounded-lg p-4 border border-brown-200">
103
103
+
<h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">Grind Size</h3>
104
104
+
{{if .Brew.GrindSize}}
105
105
+
<div class="font-semibold text-brown-900">{{.Brew.GrindSize}}</div>
106
106
+
{{else}}
107
107
+
<span class="text-brown-400">Not specified</span>
108
108
+
{{end}}
109
109
+
</div>
110
110
+
111
111
+
<!-- Temperature -->
112
112
+
<div class="bg-brown-50 rounded-lg p-4 border border-brown-200">
113
113
+
<h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">Temperature</h3>
114
114
+
{{if hasTemp .Brew.Temperature}}
115
115
+
<div class="font-semibold text-brown-900">{{formatTemp .Brew.Temperature}}</div>
116
116
+
{{else}}
117
117
+
<span class="text-brown-400">Not specified</span>
118
118
+
{{end}}
119
119
+
</div>
120
120
+
121
121
+
<!-- Brew Time -->
122
122
+
<div class="bg-brown-50 rounded-lg p-4 border border-brown-200 col-span-2">
123
123
+
<h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">Brew Time</h3>
124
124
+
{{if hasValue .Brew.TimeSeconds}}
125
125
+
<div class="font-semibold text-brown-900">{{formatTime .Brew.TimeSeconds}}</div>
126
126
+
{{else}}
127
127
+
<span class="text-brown-400">Not specified</span>
128
128
+
{{end}}
129
129
+
</div>
130
130
+
</div>
131
131
+
132
132
+
<!-- Pours -->
133
133
+
{{if .Brew.Pours}}
134
134
+
<div class="bg-brown-50 rounded-lg p-4 border border-brown-200">
135
135
+
<h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-3">Pours</h3>
136
136
+
<div class="space-y-2">
137
137
+
{{range .Brew.Pours}}
138
138
+
<div class="flex justify-between items-center bg-white p-3 rounded-lg border border-brown-200">
139
139
+
<div class="flex gap-4 text-sm">
140
140
+
<span class="font-semibold text-brown-800">{{.WaterAmount}}g</span>
141
141
+
<span class="text-brown-600">@ {{formatTime .TimeSeconds}}</span>
142
142
+
</div>
143
143
+
</div>
144
144
+
{{end}}
145
145
+
</div>
146
146
+
</div>
147
147
+
{{end}}
148
148
+
149
149
+
<!-- Tasting Notes -->
150
150
+
{{if .Brew.TastingNotes}}
151
151
+
<div class="bg-brown-50 rounded-lg p-4 border border-brown-200">
152
152
+
<h3 class="text-sm font-medium text-brown-600 uppercase tracking-wider mb-2">Tasting Notes</h3>
153
153
+
<div class="text-brown-900 whitespace-pre-wrap">{{.Brew.TastingNotes}}</div>
154
154
+
</div>
155
155
+
{{end}}
156
156
+
157
157
+
<!-- Back Button -->
158
158
+
<div class="pt-4">
159
159
+
<a href="/brews"
160
160
+
class="inline-block text-brown-700 hover:text-brown-900 font-medium">
161
161
+
← Back to Brews
162
162
+
</a>
163
163
+
</div>
164
164
+
</div>
165
165
+
</div>
166
166
+
</div>
167
167
+
{{end}}
+2
templates/partials/brew_list_content.tmpl
···
119
119
<a href="/brews/{{.RKey}}"
120
120
class="text-brown-700 hover:text-brown-900 font-medium">View</a>
121
121
{{if $.IsOwnProfile}}
122
122
+
<a href="/brews/{{.RKey}}/edit"
123
123
+
class="text-brown-700 hover:text-brown-900 font-medium">Edit</a>
122
124
<button hx-delete="/brews/{{.RKey}}"
123
125
hx-confirm="Are you sure you want to delete this brew?" hx-target="closest tr"
124
126
hx-swap="outerHTML swap:1s" class="text-brown-600 hover:text-brown-800 font-medium">
+1
templates/partials/cards/_placeholder.tmpl
···
1
1
+
{{/* Placeholder to ensure the cards directory is not empty for ParseGlob */}}