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

refactor: reorganize record cards in templ files

pdewey.com f795befa 6c3c2b12

verified
+211 -263
+1 -1
CLAUDE.md
··· 424 424 425 425 The generated `*_templ.go` should be regenerated whenever `.templ` files change. 426 426 427 - Templ files must use tabs rather than spaces. 427 + Templ files must use tabs rather than spaces. **Never use spaces for indentation in `.templ` files** — the templ parser will error with a parse failure. This applies when writing new components, editing existing ones, and constructing multi-line template strings. A post-edit hook runs `templ fmt` automatically to catch any accidental spaces. 428 428 429 429 ## Command-Line Flags 430 430
+14 -14
internal/web/components/layout.templ
··· 4 4 5 5 // LayoutData contains all the data needed for the layout 6 6 type LayoutData struct { 7 - Title string 8 - IsAuthenticated bool 9 - UserDID string 10 - UserProfile *bff.UserProfile 11 - CSPNonce string 12 - IsModerator bool // User has moderation permissions 13 - UnreadNotificationCount int // Number of unread notifications 7 + Title string 8 + IsAuthenticated bool 9 + UserDID string 10 + UserProfile *bff.UserProfile 11 + CSPNonce string 12 + IsModerator bool // User has moderation permissions 13 + UnreadNotificationCount int // Number of unread notifications 14 14 15 15 // OpenGraph metadata (optional, uses defaults if empty) 16 16 OGTitle string // Falls back to Title + " - Arabica" ··· 75 75 <link rel="icon" href="/static/favicon.svg" type="image/svg+xml"/> 76 76 <link rel="icon" href="/static/favicon-32.svg" type="image/svg+xml" sizes="32x32"/> 77 77 <link rel="apple-touch-icon" href="/static/icon-192.svg"/> 78 - <link rel="stylesheet" href="/static/css/output.css?v=0.6.0"/> 78 + <link rel="stylesheet" href="/static/css/output.css?v=0.6.1"/> 79 79 <style> 80 80 [x-cloak] { display: none !important; } 81 81 </style> ··· 168 168 </button> 169 169 </div> 170 170 @HeaderWithProps(HeaderProps{ 171 - IsAuthenticated: data.IsAuthenticated, 172 - UserProfile: data.UserProfile, 173 - UserDID: data.UserDID, 174 - IsModerator: data.IsModerator, 175 - UnreadNotificationCount: data.UnreadNotificationCount, 176 - }) 171 + IsAuthenticated: data.IsAuthenticated, 172 + UserProfile: data.UserProfile, 173 + UserDID: data.UserDID, 174 + IsModerator: data.IsModerator, 175 + UnreadNotificationCount: data.UnreadNotificationCount, 176 + }) 177 177 <main class="flex-grow container mx-auto py-8" data-transition> 178 178 @content 179 179 </main>
+1 -76
internal/web/components/profile_brew_card.templ
··· 49 49 href={ templ.SafeURL(fmt.Sprintf("/brews/%s?owner=%s", props.Brew.RKey, props.ProfileHandle)) } 50 50 class="block hover:opacity-90 transition-opacity" 51 51 > 52 - @ProfileBrewContent(props.Brew) 52 + @BrewContent(props.Brew) 53 53 </a> 54 54 <!-- Action bar --> 55 55 @ActionBar(ActionBarProps{ ··· 120 120 return fmt.Sprintf("/brews/%s", rkey) 121 121 } 122 122 123 - // ProfileBrewContent renders the brew details inside the card 124 - templ ProfileBrewContent(brew *models.Brew) { 125 - <div class="feed-content-box"> 126 - <!-- Bean info with rating --> 127 - <div class="flex items-start justify-between gap-3 mb-3"> 128 - <div class="flex-1 min-w-0"> 129 - @BeanSummary(BeanSummaryProps{ 130 - Bean: brew.Bean, 131 - CoffeeAmount: brew.CoffeeAmount, 132 - }) 133 - </div> 134 - if brew.Rating > 0 { 135 - <span class="badge-rating"> 136 - ⭐ { fmt.Sprintf("%d/10", brew.Rating) } 137 - </span> 138 - } 139 - </div> 140 - <!-- Brewer --> 141 - if brew.BrewerObj != nil || brew.Method != "" { 142 - <div class="mb-2"> 143 - <span class="text-meta">Brewer:</span> 144 - <span class="text-sm font-semibold text-brown-900"> 145 - if brew.BrewerObj != nil { 146 - { brew.BrewerObj.Name } 147 - } else if brew.Method != "" { 148 - { brew.Method } 149 - } 150 - </span> 151 - </div> 152 - } 153 - <!-- Brew parameters in compact grid --> 154 - <div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700"> 155 - if brew.GrinderObj != nil { 156 - <div> 157 - <span class="text-label">Grinder:</span> 158 - { " " }{ brew.GrinderObj.Name } 159 - if brew.GrindSize != "" { 160 - ({ brew.GrindSize }) 161 - } 162 - </div> 163 - } else if brew.GrindSize != "" { 164 - <div> 165 - <span class="text-label">Grind:</span> { brew.GrindSize } 166 - </div> 167 - } 168 - if len(brew.Pours) > 0 { 169 - <div class="col-span-2"> 170 - <span class="text-label">Pours:</span> 171 - for _, pour := range brew.Pours { 172 - <div class="pl-2 text-brown-600">• { fmt.Sprintf("%dg @ %s", pour.WaterAmount, bff.FormatTime(pour.TimeSeconds)) }</div> 173 - } 174 - </div> 175 - } else if brew.WaterAmount > 0 { 176 - <div> 177 - <span class="text-label">Water:</span> { fmt.Sprintf("%dg", brew.WaterAmount) } 178 - </div> 179 - } 180 - if bff.HasTemp(brew.Temperature) { 181 - <div> 182 - <span class="text-label">Temp:</span> { bff.FormatTemp(brew.Temperature) } 183 - </div> 184 - } 185 - if brew.TimeSeconds > 0 { 186 - <div> 187 - <span class="text-label">Time:</span> { bff.FormatTime(brew.TimeSeconds) } 188 - </div> 189 - } 190 - </div> 191 - if brew.TastingNotes != "" { 192 - <div class="mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2"> 193 - "{ brew.TastingNotes }" 194 - </div> 195 - } 196 - </div> 197 - }
+17
internal/web/components/record_bean.templ
··· 1 + package components 2 + 3 + import ( 4 + "arabica/internal/models" 5 + ) 6 + 7 + // BeanContent renders bean details inside a card 8 + templ BeanContent(bean *models.Bean) { 9 + <div class="feed-content-box-sm"> 10 + @BeanSummary(BeanSummaryProps{ 11 + Bean: bean, 12 + }) 13 + if bean.Description != "" { 14 + <div class="mt-2 text-sm text-brown-800 italic">"{ bean.Description }"</div> 15 + } 16 + </div> 17 + }
+83
internal/web/components/record_brew.templ
··· 1 + package components 2 + 3 + import ( 4 + "arabica/internal/models" 5 + "arabica/internal/web/bff" 6 + "fmt" 7 + ) 8 + 9 + // BrewContent renders brew details inside a card 10 + templ BrewContent(brew *models.Brew) { 11 + <div class="feed-content-box"> 12 + <!-- Bean info with rating --> 13 + <div class="flex items-start justify-between gap-3 mb-3"> 14 + <div class="flex-1 min-w-0"> 15 + @BeanSummary(BeanSummaryProps{ 16 + Bean: brew.Bean, 17 + CoffeeAmount: brew.CoffeeAmount, 18 + }) 19 + </div> 20 + if brew.Rating > 0 { 21 + <span class="badge-rating"> 22 + ⭐ { fmt.Sprintf("%d/10", brew.Rating) } 23 + </span> 24 + } 25 + </div> 26 + <!-- Brewer --> 27 + if brew.BrewerObj != nil || brew.Method != "" { 28 + <div class="mb-2"> 29 + <span class="text-meta">Brewer:</span> 30 + <span class="text-sm font-semibold text-brown-900"> 31 + if brew.BrewerObj != nil { 32 + { brew.BrewerObj.Name } 33 + } else if brew.Method != "" { 34 + { brew.Method } 35 + } 36 + </span> 37 + </div> 38 + } 39 + <!-- Brew parameters in compact grid --> 40 + <div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700"> 41 + if brew.GrinderObj != nil { 42 + <div> 43 + <span class="text-label">Grinder:</span> 44 + { " " }{ brew.GrinderObj.Name } 45 + if brew.GrindSize != "" { 46 + ({ brew.GrindSize }) 47 + } 48 + </div> 49 + } else if brew.GrindSize != "" { 50 + <div> 51 + <span class="text-label">Grind:</span> { brew.GrindSize } 52 + </div> 53 + } 54 + if len(brew.Pours) > 0 { 55 + <div class="col-span-2"> 56 + <span class="text-label">Pours:</span> 57 + for _, pour := range brew.Pours { 58 + <div class="pl-2 text-brown-600">• { fmt.Sprintf("%dg @ %s", pour.WaterAmount, bff.FormatTime(pour.TimeSeconds)) }</div> 59 + } 60 + </div> 61 + } else if brew.WaterAmount > 0 { 62 + <div> 63 + <span class="text-label">Water:</span> { fmt.Sprintf("%dg", brew.WaterAmount) } 64 + </div> 65 + } 66 + if bff.HasTemp(brew.Temperature) { 67 + <div> 68 + <span class="text-label">Temp:</span> { bff.FormatTemp(brew.Temperature) } 69 + </div> 70 + } 71 + if brew.TimeSeconds > 0 { 72 + <div> 73 + <span class="text-label">Time:</span> { bff.FormatTime(brew.TimeSeconds) } 74 + </div> 75 + } 76 + </div> 77 + if brew.TastingNotes != "" { 78 + <div class="mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2"> 79 + "{ brew.TastingNotes }" 80 + </div> 81 + } 82 + </div> 83 + }
+22
internal/web/components/record_brewer.templ
··· 1 + package components 2 + 3 + import ( 4 + "arabica/internal/models" 5 + ) 6 + 7 + // BrewerContent renders brewer details inside a card 8 + templ BrewerContent(brewer *models.Brewer) { 9 + <div class="feed-content-box-sm"> 10 + <div class="text-base mb-2"> 11 + <span class="font-bold text-brown-900">{ brewer.Name }</span> 12 + </div> 13 + if brewer.BrewerType != "" { 14 + <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 15 + <span class="inline-flex items-center gap-0.5">🫖 { brewer.BrewerType }</span> 16 + </div> 17 + } 18 + if brewer.Description != "" { 19 + <div class="mt-2 text-sm text-brown-800 italic">"{ brewer.Description }"</div> 20 + } 21 + </div> 22 + }
+25
internal/web/components/record_grinder.templ
··· 1 + package components 2 + 3 + import ( 4 + "arabica/internal/models" 5 + ) 6 + 7 + // GrinderContent renders grinder details inside a card 8 + templ GrinderContent(grinder *models.Grinder) { 9 + <div class="feed-content-box-sm"> 10 + <div class="text-base mb-2"> 11 + <span class="font-bold text-brown-900">{ grinder.Name }</span> 12 + </div> 13 + <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 14 + if grinder.GrinderType != "" { 15 + <span class="inline-flex items-center gap-0.5">⚙️ { grinder.GrinderType }</span> 16 + } 17 + if grinder.BurrType != "" { 18 + <span class="inline-flex items-center gap-0.5">🔩 { grinder.BurrType }</span> 19 + } 20 + </div> 21 + if grinder.Notes != "" { 22 + <div class="mt-2 text-sm text-brown-800 italic">"{ grinder.Notes }"</div> 23 + } 24 + </div> 25 + }
+27
internal/web/components/record_roaster.templ
··· 1 + package components 2 + 3 + import ( 4 + "arabica/internal/models" 5 + "arabica/internal/web/bff" 6 + ) 7 + 8 + // RoasterContent renders roaster details inside a card 9 + templ RoasterContent(roaster *models.Roaster) { 10 + <div class="feed-content-box-sm"> 11 + <div class="text-base mb-2"> 12 + <span class="font-bold text-brown-900">{ roaster.Name }</span> 13 + </div> 14 + <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 15 + if roaster.Location != "" { 16 + <span class="inline-flex items-center gap-0.5">📍 { roaster.Location }</span> 17 + } 18 + if roaster.Website != "" { 19 + if safeWebsite := bff.SafeWebsiteURL(roaster.Website); safeWebsite != "" { 20 + <span class="inline-flex items-center gap-0.5"> 21 + 🔗 <a href={ templ.SafeURL(safeWebsite) } target="_blank" rel="noopener noreferrer" class="text-brown-800 hover:underline">{ safeWebsite }</a> 22 + </span> 23 + } 24 + } 25 + </div> 26 + </div> 27 + }
+21 -172
internal/web/pages/feed.templ
··· 3 3 import ( 4 4 "arabica/internal/feed" 5 5 "arabica/internal/lexicons" 6 - "arabica/internal/web/bff" 7 6 "arabica/internal/web/components" 8 7 "fmt" 9 8 ) ··· 197 196 case lexicons.RecordTypeBrew: 198 197 @FeedBrewContentClickable(item) 199 198 case lexicons.RecordTypeBean: 200 - @FeedEntityContentClickable(item, FeedBeanContent) 199 + if item.Bean != nil { 200 + <a href={ templ.SafeURL(getFeedItemShareURL(item)) } class="block hover:opacity-90 transition-opacity"> 201 + @components.BeanContent(item.Bean) 202 + </a> 203 + } 201 204 case lexicons.RecordTypeRoaster: 202 - @FeedEntityContentClickable(item, FeedRoasterContent) 205 + if item.Roaster != nil { 206 + <a href={ templ.SafeURL(getFeedItemShareURL(item)) } class="block hover:opacity-90 transition-opacity"> 207 + @components.RoasterContent(item.Roaster) 208 + </a> 209 + } 203 210 case lexicons.RecordTypeGrinder: 204 - @FeedEntityContentClickable(item, FeedGrinderContent) 211 + if item.Grinder != nil { 212 + <a href={ templ.SafeURL(getFeedItemShareURL(item)) } class="block hover:opacity-90 transition-opacity"> 213 + @components.GrinderContent(item.Grinder) 214 + </a> 215 + } 205 216 case lexicons.RecordTypeBrewer: 206 - @FeedEntityContentClickable(item, FeedBrewerContent) 217 + if item.Brewer != nil { 218 + <a href={ templ.SafeURL(getFeedItemShareURL(item)) } class="block hover:opacity-90 transition-opacity"> 219 + @components.BrewerContent(item.Brewer) 220 + </a> 221 + } 207 222 } 208 223 <!-- Action bar --> 209 224 if item.SubjectURI != "" && item.SubjectCID != "" { ··· 290 305 } 291 306 } 292 307 293 - // FeedEntityContentClickable wraps any entity content component in a clickable link 294 - templ FeedEntityContentClickable(item *feed.FeedItem, content func(*feed.FeedItem) templ.Component) { 295 - <a 296 - href={ templ.SafeURL(getFeedItemShareURL(item)) } 297 - class="block hover:opacity-90 transition-opacity" 298 - > 299 - @content(item) 300 - </a> 301 - } 302 - 303 308 // FeedBrewContentClickable renders brew content wrapped in a clickable link 304 309 templ FeedBrewContentClickable(item *feed.FeedItem) { 305 310 if item.Brew != nil { ··· 307 312 href={ templ.SafeURL(fmt.Sprintf("/brews/%s?owner=%s", item.Brew.RKey, item.Author.DID)) } 308 313 class="block hover:opacity-90 transition-opacity" 309 314 > 310 - @FeedBrewContent(item) 315 + @components.BrewContent(item.Brew) 311 316 </a> 312 - } 313 - } 314 - 315 - // FeedBrewContent renders brew content in a feed card 316 - templ FeedBrewContent(item *feed.FeedItem) { 317 - if item.Brew != nil { 318 - <div class="feed-content-box"> 319 - <!-- Bean info with rating --> 320 - <div class="flex items-start justify-between gap-3 mb-3"> 321 - <div class="flex-1 min-w-0"> 322 - @components.BeanSummary(components.BeanSummaryProps{ 323 - Bean: item.Brew.Bean, 324 - CoffeeAmount: item.Brew.CoffeeAmount, 325 - }) 326 - </div> 327 - if item.Brew.Rating > 0 { 328 - <span class="badge-rating"> 329 - ⭐ { fmt.Sprintf("%d/10", item.Brew.Rating) } 330 - </span> 331 - } 332 - </div> 333 - <!-- Brewer --> 334 - if item.Brew.BrewerObj != nil || item.Brew.Method != "" { 335 - <div class="mb-2"> 336 - <span class="text-meta">Brewer:</span> 337 - <span class="text-sm font-semibold text-brown-900"> 338 - if item.Brew.BrewerObj != nil { 339 - { item.Brew.BrewerObj.Name } 340 - } else if item.Brew.Method != "" { 341 - { item.Brew.Method } 342 - } 343 - </span> 344 - </div> 345 - } 346 - <!-- Brew parameters in compact grid --> 347 - <div class="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-brown-700"> 348 - if item.Brew.GrinderObj != nil { 349 - <div> 350 - <span class="text-label">Grinder:</span> 351 - { " " }{ item.Brew.GrinderObj.Name } 352 - if item.Brew.GrindSize != "" { 353 - ({ item.Brew.GrindSize }) 354 - } 355 - </div> 356 - } else if item.Brew.GrindSize != "" { 357 - <div> 358 - <span class="text-label">Grind:</span> { item.Brew.GrindSize } 359 - </div> 360 - } 361 - if len(item.Brew.Pours) > 0 { 362 - <div class="col-span-2"> 363 - <span class="text-label">Pours:</span> 364 - for _, pour := range item.Brew.Pours { 365 - <div class="pl-2 text-brown-600">• { fmt.Sprintf("%dg @ %s", pour.WaterAmount, bff.FormatTime(pour.TimeSeconds)) }</div> 366 - } 367 - </div> 368 - } else if item.Brew.WaterAmount > 0 { 369 - <div> 370 - <span class="text-label">Water:</span> { fmt.Sprintf("%dg", item.Brew.WaterAmount) } 371 - </div> 372 - } 373 - if bff.HasTemp(item.Brew.Temperature) { 374 - <div> 375 - <span class="text-label">Temp:</span> { bff.FormatTemp(item.Brew.Temperature) } 376 - </div> 377 - } 378 - if item.Brew.TimeSeconds > 0 { 379 - <div> 380 - <span class="text-label">Time:</span> { bff.FormatTime(item.Brew.TimeSeconds) } 381 - </div> 382 - } 383 - </div> 384 - if item.Brew.TastingNotes != "" { 385 - <div class="mt-3 text-sm text-brown-800 italic border-t border-brown-200 pt-2"> 386 - "{ item.Brew.TastingNotes }" 387 - </div> 388 - } 389 - </div> 390 - } 391 - } 392 - 393 - // FeedBeanContent renders bean content in a feed card 394 - templ FeedBeanContent(item *feed.FeedItem) { 395 - if item.Bean != nil { 396 - <div class="feed-content-box-sm"> 397 - @components.BeanSummary(components.BeanSummaryProps{ 398 - Bean: item.Bean, 399 - }) 400 - if item.Bean.Description != "" { 401 - <div class="mt-2 text-sm text-brown-800 italic">"{ item.Bean.Description }"</div> 402 - } 403 - </div> 404 - } 405 - } 406 - 407 - // FeedRoasterContent renders roaster content in a feed card 408 - templ FeedRoasterContent(item *feed.FeedItem) { 409 - if item.Roaster != nil { 410 - <div class="feed-content-box-sm"> 411 - <div class="text-base mb-2"> 412 - <span class="font-bold text-brown-900">{ item.Roaster.Name }</span> 413 - </div> 414 - <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 415 - if item.Roaster.Location != "" { 416 - <span class="inline-flex items-center gap-0.5">📍 { item.Roaster.Location }</span> 417 - } 418 - if item.Roaster.Website != "" { 419 - if safeWebsite := bff.SafeWebsiteURL(item.Roaster.Website); safeWebsite != "" { 420 - <span class="inline-flex items-center gap-0.5"> 421 - 🔗 <a href={ templ.SafeURL(safeWebsite) } target="_blank" rel="noopener noreferrer" class="text-brown-800 hover:underline">{ safeWebsite }</a> 422 - </span> 423 - } 424 - } 425 - </div> 426 - </div> 427 - } 428 - } 429 - 430 - // FeedGrinderContent renders grinder content in a feed card 431 - templ FeedGrinderContent(item *feed.FeedItem) { 432 - if item.Grinder != nil { 433 - <div class="feed-content-box-sm"> 434 - <div class="text-base mb-2"> 435 - <span class="font-bold text-brown-900">{ item.Grinder.Name }</span> 436 - </div> 437 - <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 438 - if item.Grinder.GrinderType != "" { 439 - <span class="inline-flex items-center gap-0.5">⚙️ { item.Grinder.GrinderType }</span> 440 - } 441 - if item.Grinder.BurrType != "" { 442 - <span class="inline-flex items-center gap-0.5">🔩 { item.Grinder.BurrType }</span> 443 - } 444 - </div> 445 - if item.Grinder.Notes != "" { 446 - <div class="mt-2 text-sm text-brown-800 italic">"{ item.Grinder.Notes }"</div> 447 - } 448 - </div> 449 - } 450 - } 451 - 452 - // FeedBrewerContent renders brewer content in a feed card 453 - templ FeedBrewerContent(item *feed.FeedItem) { 454 - if item.Brewer != nil { 455 - <div class="feed-content-box-sm"> 456 - <div class="text-base mb-2"> 457 - <span class="font-bold text-brown-900">{ item.Brewer.Name }</span> 458 - </div> 459 - if item.Brewer.BrewerType != "" { 460 - <div class="text-xs text-brown-600 mt-1 flex flex-wrap gap-x-2 gap-y-0.5"> 461 - <span class="inline-flex items-center gap-0.5">🫖 { item.Brewer.BrewerType }</span> 462 - </div> 463 - } 464 - if item.Brewer.Description != "" { 465 - <div class="mt-2 text-sm text-brown-800 italic">"{ item.Brewer.Description }"</div> 466 - } 467 - </div> 468 317 } 469 318 } 470 319