Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee

feat: view brew page

pdewey.com e217109b 0b9b80ec

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