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

Configure Feed

Select the types of activity you want to include in your feed.

feat: view page redesign cont. #4

open opened by pdewey.com targeting main from push-wyqtrtsvtyyy
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:hm5f3dnm6jdhrc55qp2npdja/sh.tangled.repo.pull/3mkm2vnrhfi22
+511 -279
Diff #0
+1 -1
internal/web/components/layout.templ
··· 115 115 <link rel="icon" href="/static/favicon.svg" type="image/svg+xml"/> 116 116 <link rel="icon" href="/static/favicon-32.svg" type="image/svg+xml" sizes="32x32"/> 117 117 <link rel="apple-touch-icon" href="/static/icon-192.svg"/> 118 - <link rel="stylesheet" href="/static/css/output.css?v=0.10.0"/> 118 + <link rel="stylesheet" href="/static/css/output.css?v=0.11.0"/> 119 119 <style> 120 120 [x-cloak] { display: none !important; } 121 121 </style>
+23
internal/web/components/shared.templ
··· 290 290 } 291 291 } 292 292 293 + // JournalField renders a ledger-style row: label left, value right, with a faint separator. 294 + templ JournalField(props DetailStackedProps) { 295 + if props.Value != "" { 296 + <div class="journal-field"> 297 + <span class="detail-label"> 298 + if props.Icon != nil { 299 + <span class="inline-flex items-center gap-1"> 300 + @props.Icon 301 + { props.Label } 302 + </span> 303 + } else { 304 + { props.Label } 305 + } 306 + </span> 307 + if props.LinkHref != "" { 308 + <a href={ templ.SafeURL(props.LinkHref) } class="detail-value hover:underline">{ props.Value }</a> 309 + } else { 310 + <span class="detail-value">{ props.Value }</span> 311 + } 312 + </div> 313 + } 314 + } 315 + 293 316 type PageHeaderProps struct { 294 317 Title string 295 318 BackURL string
+72 -58
internal/web/pages/bean_view.templ
··· 10 10 ) 11 11 12 12 type BeanViewProps struct { 13 - Bean *models.Bean 14 - IsOwnProfile bool 15 - IsAuthenticated bool 16 - SubjectURI string 17 - SubjectCID string 18 - IsLiked bool 19 - LikeCount int 20 - CommentCount int 21 - Comments []firehose.IndexedComment 22 - CurrentUserDID string 23 - ShareURL string 24 - IsModerator bool 25 - CanHideRecord bool 26 - CanBlockUser bool 27 - IsRecordHidden bool 13 + Bean *models.Bean 14 + IsOwnProfile bool 15 + IsAuthenticated bool 16 + SubjectURI string 17 + SubjectCID string 18 + IsLiked bool 19 + LikeCount int 20 + CommentCount int 21 + Comments []firehose.IndexedComment 22 + CurrentUserDID string 23 + ShareURL string 24 + IsModerator bool 25 + CanHideRecord bool 26 + CanBlockUser bool 27 + IsRecordHidden bool 28 28 AuthorDID string 29 29 AuthorHandle string 30 30 AuthorDisplayName string ··· 53 53 AuthorDisplay: props.AuthorDisplayName, 54 54 AuthorAvatar: props.AuthorAvatar, 55 55 }) 56 - if props.Bean.Roaster != nil && props.Bean.Roaster.Name != "" { 57 - <div class="detail-stacked mb-6"> 58 - <span class="detail-label"> 56 + <div class="record-label p-4"> 57 + if props.Bean.Roaster != nil && props.Bean.Roaster.Name != "" { 58 + <div class="label-byline"> 59 59 <span class="inline-flex items-center gap-1"> 60 60 @components.IconStore() 61 - Roaster 61 + <a href={ templ.SafeURL(fmt.Sprintf("/roasters/%s?owner=%s", props.Bean.Roaster.RKey, getOwnerFromShareURL(props.ShareURL))) }> 62 + { props.Bean.Roaster.Name } 63 + </a> 64 + if props.Bean.Roaster.Location != "" { 65 + <span style="color: var(--text-faint)">·</span> 66 + <span class="inline-flex items-center gap-1"> 67 + @components.IconMapPin() 68 + { props.Bean.Roaster.Location } 69 + </span> 70 + } 62 71 </span> 63 - </span> 64 - <a 65 - href={ templ.SafeURL(fmt.Sprintf("/roasters/%s?owner=%s", props.Bean.Roaster.RKey, getOwnerFromShareURL(props.ShareURL))) } 66 - class="detail-value-lg hover:underline" 67 - > 68 - { props.Bean.Roaster.Name } 69 - </a> 70 - if props.Bean.Roaster.Location != "" { 71 - <span class="text-sm inline-flex items-center gap-1" style="color: var(--text-muted)"> 72 - @components.IconMapPin() 73 - { props.Bean.Roaster.Location } 72 + </div> 73 + } 74 + if props.Bean.Rating != nil { 75 + <div class="brew-rating-hero mt-4"> 76 + <span class="brew-rating-value">{ fmt.Sprintf("%d", *props.Bean.Rating) }</span> 77 + <span class="brew-rating-max">/10</span> 78 + </div> 79 + } 80 + <div class="label-tags"> 81 + if props.Bean.Origin != "" { 82 + <span class="label-tag"> 83 + <span class="inline-flex items-center gap-1"> 84 + @components.IconMapPin() 85 + { props.Bean.Origin } 86 + </span> 74 87 </span> 75 88 } 89 + if props.Bean.Variety != "" { 90 + <span class="label-tag"> 91 + <span class="inline-flex items-center gap-1"> 92 + @components.IconLeaf() 93 + { props.Bean.Variety } 94 + </span> 95 + </span> 96 + } 97 + if props.Bean.RoastLevel != "" { 98 + <span class="label-tag"> 99 + <span class="inline-flex items-center gap-1"> 100 + @components.IconFlame() 101 + { props.Bean.RoastLevel } 102 + </span> 103 + </span> 104 + } 105 + if props.Bean.Process != "" { 106 + <span class="label-tag"> 107 + <span class="inline-flex items-center gap-1"> 108 + @components.IconSprout() 109 + { props.Bean.Process } 110 + </span> 111 + </span> 112 + } 113 + if props.Bean.Closed { 114 + <span class="label-tag" style="border-color: var(--text-muted)">Closed</span> 115 + } 76 116 </div> 77 - } 78 - if bff.FormatBeanRating(props.Bean.Rating) != "" { 79 - <div class="brew-rating-hero mb-6"> 80 - <span class="brew-rating-value">{ bff.FormatBeanRating(props.Bean.Rating) }</span> 81 - <span class="brew-rating-max">/10</span> 82 - </div> 83 - } 84 - <div class="detail-grid"> 85 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconMapPin(), Label: "Origin", Value: props.Bean.Origin}) 86 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconLeaf(), Label: "Variety", Value: props.Bean.Variety}) 87 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconFlame(), Label: "Roast Level", Value: props.Bean.RoastLevel}) 88 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconSprout(), Label: "Process", Value: props.Bean.Process}) 89 - if props.Bean.Closed { 90 - <div class="detail-stacked"> 91 - <span class="detail-label">Status</span> 92 - <span class="text-sm bg-brown-200 text-brown-700 px-2 py-1 rounded-md font-medium w-fit">Closed</span> 93 - </div> 117 + if props.Bean.Description != "" { 118 + <div class="label-description">{ props.Bean.Description }</div> 94 119 } 95 120 </div> 96 - if props.Bean.Description != "" { 97 - <div class="mt-6"> 98 - <span class="detail-label mb-2 block"> 99 - <span class="inline-flex items-center gap-1"> 100 - @components.IconFileText() 101 - Description 102 - </span> 103 - </span> 104 - <div class="prose-note">{ props.Bean.Description }</div> 105 - </div> 106 - } 107 121 if props.BrewCount > 0 { 108 122 <div class="record-stat-line"> 109 123 <span class="flex items-center gap-1">
+111 -58
internal/web/pages/brew_view.templ
··· 22 22 CurrentUserDID string // DID of the current user (for delete buttons) 23 23 ShareURL string // URL for sharing the brew 24 24 // Moderation state 25 - IsModerator bool // User has moderator role 26 - CanHideRecord bool // User has hide_record permission 27 - CanBlockUser bool // User has blacklist_user permission 28 - IsRecordHidden bool // This record is currently hidden 29 - AuthorDID string // DID of the brew author 30 - AuthorHandle string 31 - AuthorDisplayName string 32 - AuthorAvatar string 25 + IsModerator bool // User has moderator role 26 + CanHideRecord bool // User has hide_record permission 27 + CanBlockUser bool // User has blacklist_user permission 28 + IsRecordHidden bool // This record is currently hidden 29 + AuthorDID string // DID of the brew author 30 + AuthorHandle string 31 + AuthorDisplayName string 32 + AuthorAvatar string 33 33 } 34 34 35 35 // BrewView renders the full brew view page ··· 56 56 AuthorDisplay: props.AuthorDisplayName, 57 57 AuthorAvatar: props.AuthorAvatar, 58 58 }) 59 - if props.Brew.Rating > 0 { 60 - <div class="brew-rating-hero mb-6"> 61 - <span class="brew-rating-value">{ fmt.Sprintf("%d", props.Brew.Rating) }</span> 62 - <span class="brew-rating-max">/10</span> 63 - </div> 64 - } 65 - @BrewBeanSection(props.Brew, getOwnerFromShareURL(props.ShareURL)) 66 - <div class="detail-grid my-6"> 67 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconScale(), Label: "Coffee", Value: getCoffeeAmountDisplay(props.Brew)}) 68 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconCoffee(), Label: "Brew Method", Value: getBrewerName(props.Brew), LinkHref: getBrewerViewURL(props.Brew, getOwnerFromShareURL(props.ShareURL))}) 69 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconGear(), Label: "Grinder", Value: getGrinderName(props.Brew), LinkHref: getGrinderViewURL(props.Brew, getOwnerFromShareURL(props.ShareURL))}) 70 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconDisc(), Label: "Grind Size", Value: getGrindSizeDisplay(props.Brew)}) 71 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconDroplet(), Label: "Water", Value: getWaterAmountDisplay(props.Brew)}) 72 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconThermometer(), Label: "Temperature", Value: getTemperatureDisplay(props.Brew)}) 73 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconClock(), Label: "Brew Time", Value: getBrewTimeDisplay(props.Brew)}) 74 - if props.Brew.EspressoParams != nil { 75 - if props.Brew.EspressoParams.YieldWeight > 0 { 76 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconScale(), Label: "Yield", Value: fmt.Sprintf("%.1fg", props.Brew.EspressoParams.YieldWeight)}) 77 - } 78 - if props.Brew.EspressoParams.Pressure > 0 { 79 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconBarChart(), Label: "Pressure", Value: fmt.Sprintf("%.1f bar", props.Brew.EspressoParams.Pressure)}) 59 + <div class="record-journal p-4"> 60 + @BrewSummary(props.Brew) 61 + @BrewBeanSection(props.Brew, getOwnerFromShareURL(props.ShareURL)) 62 + <div class="my-6"> 63 + @components.JournalField(components.DetailStackedProps{Icon: components.IconScale(), Label: "Coffee", Value: getCoffeeAmountDisplay(props.Brew)}) 64 + @components.JournalField(components.DetailStackedProps{Icon: components.IconCoffee(), Label: "Brew Method", Value: getBrewerName(props.Brew), LinkHref: getBrewerViewURL(props.Brew, getOwnerFromShareURL(props.ShareURL))}) 65 + @components.JournalField(components.DetailStackedProps{Icon: components.IconGear(), Label: "Grinder", Value: getGrinderName(props.Brew), LinkHref: getGrinderViewURL(props.Brew, getOwnerFromShareURL(props.ShareURL))}) 66 + @components.JournalField(components.DetailStackedProps{Icon: components.IconDisc(), Label: "Grind Size", Value: getGrindSizeDisplay(props.Brew)}) 67 + @components.JournalField(components.DetailStackedProps{Icon: components.IconDroplet(), Label: "Water", Value: getWaterAmountDisplay(props.Brew)}) 68 + @components.JournalField(components.DetailStackedProps{Icon: components.IconThermometer(), Label: "Temperature", Value: getTemperatureDisplay(props.Brew)}) 69 + @components.JournalField(components.DetailStackedProps{Icon: components.IconClock(), Label: "Brew Time", Value: getBrewTimeDisplay(props.Brew)}) 70 + if props.Brew.EspressoParams != nil { 71 + if props.Brew.EspressoParams.YieldWeight > 0 { 72 + @components.JournalField(components.DetailStackedProps{Icon: components.IconScale(), Label: "Yield", Value: fmt.Sprintf("%.1fg", props.Brew.EspressoParams.YieldWeight)}) 73 + } 74 + if props.Brew.EspressoParams.Pressure > 0 { 75 + @components.JournalField(components.DetailStackedProps{Icon: components.IconBarChart(), Label: "Pressure", Value: fmt.Sprintf("%.1f bar", props.Brew.EspressoParams.Pressure)}) 76 + } 77 + if props.Brew.EspressoParams.PreInfusionSeconds > 0 { 78 + @components.JournalField(components.DetailStackedProps{Icon: components.IconClock(), Label: "Pre-infusion", Value: fmt.Sprintf("%ds", props.Brew.EspressoParams.PreInfusionSeconds)}) 79 + } 80 80 } 81 - if props.Brew.EspressoParams.PreInfusionSeconds > 0 { 82 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconClock(), Label: "Pre-infusion", Value: fmt.Sprintf("%ds", props.Brew.EspressoParams.PreInfusionSeconds)}) 81 + if props.Brew.PouroverParams != nil { 82 + if props.Brew.PouroverParams.BloomWater > 0 || props.Brew.PouroverParams.BloomSeconds > 0 { 83 + @components.JournalField(components.DetailStackedProps{Icon: components.IconDroplet(), Label: "Bloom", Value: formatBloom(props.Brew.PouroverParams)}) 84 + } 85 + if props.Brew.PouroverParams.DrawdownSeconds > 0 { 86 + @components.JournalField(components.DetailStackedProps{Icon: components.IconClock(), Label: "Drawdown", Value: fmt.Sprintf("%ds", props.Brew.PouroverParams.DrawdownSeconds)}) 87 + } 88 + if props.Brew.PouroverParams.BypassWater > 0 { 89 + @components.JournalField(components.DetailStackedProps{Icon: components.IconDroplet(), Label: "Bypass Water", Value: fmt.Sprintf("%dg", props.Brew.PouroverParams.BypassWater)}) 90 + } 91 + if props.Brew.PouroverParams.Filter != "" { 92 + @components.JournalField(components.DetailStackedProps{Icon: components.IconSliders(), Label: "Filter", Value: props.Brew.PouroverParams.Filter}) 93 + } 83 94 } 84 - } 85 - if props.Brew.PouroverParams != nil { 86 - if props.Brew.PouroverParams.BloomWater > 0 || props.Brew.PouroverParams.BloomSeconds > 0 { 87 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconDroplet(), Label: "Bloom", Value: formatBloom(props.Brew.PouroverParams)}) 95 + </div> 96 + <div class="space-y-6"> 97 + if props.Brew.RecipeObj != nil { 98 + @BrewRecipeSection(props.Brew.RecipeObj, getOwnerFromShareURL(props.ShareURL)) 88 99 } 89 - if props.Brew.PouroverParams.DrawdownSeconds > 0 { 90 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconClock(), Label: "Drawdown", Value: fmt.Sprintf("%ds", props.Brew.PouroverParams.DrawdownSeconds)}) 100 + if props.Brew.Pours != nil && len(props.Brew.Pours) > 0 { 101 + @BrewPoursSection(props.Brew.Pours) 91 102 } 92 - if props.Brew.PouroverParams.BypassWater > 0 { 93 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconDroplet(), Label: "Bypass Water", Value: fmt.Sprintf("%dg", props.Brew.PouroverParams.BypassWater)}) 103 + if props.Brew.TastingNotes != "" { 104 + @BrewTastingNotes(props.Brew.TastingNotes) 94 105 } 95 - if props.Brew.PouroverParams.Filter != "" { 96 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconSliders(), Label: "Filter", Value: props.Brew.PouroverParams.Filter}) 106 + if props.IsOwnProfile && props.Brew.RecipeObj == nil { 107 + @SaveAsRecipeButton(props.Brew.RKey) 97 108 } 98 - } 99 - </div> 100 - <div class="space-y-6"> 101 - if props.Brew.RecipeObj != nil { 102 - @BrewRecipeSection(props.Brew.RecipeObj, getOwnerFromShareURL(props.ShareURL)) 103 - } 104 - if props.Brew.Pours != nil && len(props.Brew.Pours) > 0 { 105 - @BrewPoursSection(props.Brew.Pours) 106 - } 107 - if props.Brew.TastingNotes != "" { 108 - @BrewTastingNotes(props.Brew.TastingNotes) 109 - } 110 - if props.IsOwnProfile && props.Brew.RecipeObj == nil { 111 - @SaveAsRecipeButton(props.Brew.RKey) 112 - } 109 + </div> 113 110 </div> 114 111 <div class="record-view-footer"> 115 112 @components.BackButton() ··· 150 147 }) 151 148 } 152 149 150 + // BrewSummary renders the at-a-glance headline at the top of the journal: 151 + // the rating on the left paired with a compact recipe summary on the right. 152 + templ BrewSummary(brew *models.Brew) { 153 + if brew.Rating > 0 || hasBrewSummaryStats(brew) { 154 + <div class="brew-summary"> 155 + if brew.Rating > 0 { 156 + <div class="brew-rating-hero"> 157 + <span class="brew-rating-value">{ fmt.Sprintf("%d", brew.Rating) }</span> 158 + <span class="brew-rating-max">/10</span> 159 + </div> 160 + } 161 + if hasBrewSummaryStats(brew) { 162 + <dl class="brew-summary-stats"> 163 + if ratio := getBrewRatioDisplay(brew); ratio != "" { 164 + <div class="brew-summary-stat"> 165 + <dt>Ratio</dt> 166 + <dd>{ ratio }</dd> 167 + </div> 168 + } 169 + if t := getBrewTimeDisplay(brew); t != "" { 170 + <div class="brew-summary-stat"> 171 + <dt>Time</dt> 172 + <dd>{ t }</dd> 173 + </div> 174 + } 175 + if m := getBrewerName(brew); m != "" { 176 + <div class="brew-summary-stat"> 177 + <dt>Method</dt> 178 + <dd>{ m }</dd> 179 + </div> 180 + } 181 + </dl> 182 + } 183 + </div> 184 + } 185 + } 186 + 187 + func hasBrewSummaryStats(brew *models.Brew) bool { 188 + return getBrewRatioDisplay(brew) != "" || getBrewTimeDisplay(brew) != "" || getBrewerName(brew) != "" 189 + } 190 + 191 + func getBrewRatioDisplay(brew *models.Brew) string { 192 + water := 0 193 + if brew.WaterAmount > 0 { 194 + water = brew.WaterAmount 195 + } else { 196 + for _, pour := range brew.Pours { 197 + water += pour.WaterAmount 198 + } 199 + } 200 + if brew.CoffeeAmount > 0 && water > 0 { 201 + return fmt.Sprintf("1:%.1f", float64(water)/float64(brew.CoffeeAmount)) 202 + } 203 + return "" 204 + } 205 + 153 206 // BrewBeanSection renders the coffee bean as a prominent reference card 154 207 templ BrewBeanSection(brew *models.Brew, owner string) { 155 208 if brew.Bean != nil { 156 - <div class="brew-bean-ref mb-2"> 209 + <div class="journal-bean-ref mb-2"> 157 210 <span class="detail-label mb-2 block"> 158 211 <span class="inline-flex items-center gap-1"> 159 212 @components.IconBean() ··· 417 470 Tasting Notes 418 471 </span> 419 472 </span> 420 - <div class="prose-note">{ notes }</div> 473 + <div class="journal-prose">{ notes }</div> 421 474 </div> 422 475 }
+28 -27
internal/web/pages/brewer_view.templ
··· 10 10 ) 11 11 12 12 type BrewerViewProps struct { 13 - Brewer *models.Brewer 14 - IsOwnProfile bool 15 - IsAuthenticated bool 16 - SubjectURI string 17 - SubjectCID string 18 - IsLiked bool 19 - LikeCount int 20 - CommentCount int 21 - Comments []firehose.IndexedComment 22 - CurrentUserDID string 23 - ShareURL string 24 - IsModerator bool 25 - CanHideRecord bool 26 - CanBlockUser bool 27 - IsRecordHidden bool 13 + Brewer *models.Brewer 14 + IsOwnProfile bool 15 + IsAuthenticated bool 16 + SubjectURI string 17 + SubjectCID string 18 + IsLiked bool 19 + LikeCount int 20 + CommentCount int 21 + Comments []firehose.IndexedComment 22 + CurrentUserDID string 23 + ShareURL string 24 + IsModerator bool 25 + CanHideRecord bool 26 + CanBlockUser bool 27 + IsRecordHidden bool 28 28 AuthorDID string 29 29 AuthorHandle string 30 30 AuthorDisplayName string ··· 53 53 AuthorDisplay: props.AuthorDisplayName, 54 54 AuthorAvatar: props.AuthorAvatar, 55 55 }) 56 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconCoffee(), Label: "Type", Value: props.Brewer.BrewerType}) 57 - if props.Brewer.Description != "" { 58 - <div class="mt-6"> 59 - <span class="detail-label mb-2 block"> 60 - <span class="inline-flex items-center gap-1"> 61 - @components.IconFileText() 62 - Description 56 + <div class="record-journal p-4"> 57 + @components.JournalField(components.DetailStackedProps{Icon: components.IconCoffee(), Label: "Type", Value: props.Brewer.BrewerType}) 58 + if props.Brewer.Description != "" { 59 + <div class="mt-6"> 60 + <span class="detail-label mb-2 block"> 61 + <span class="inline-flex items-center gap-1"> 62 + @components.IconFileText() 63 + Description 64 + </span> 63 65 </span> 64 - </span> 65 - <div class="prose-note">{ props.Brewer.Description }</div> 66 - </div> 67 - } 66 + <div class="journal-prose">{ props.Brewer.Description }</div> 67 + </div> 68 + } 69 + </div> 68 70 if props.BrewCount > 0 { 69 71 <div class="record-stat-line"> 70 72 <span class="flex items-center gap-1"> ··· 111 113 ViewURL: props.ShareURL, 112 114 }) 113 115 } 114 -
+31 -30
internal/web/pages/grinder_view.templ
··· 10 10 ) 11 11 12 12 type GrinderViewProps struct { 13 - Grinder *models.Grinder 14 - IsOwnProfile bool 15 - IsAuthenticated bool 16 - SubjectURI string 17 - SubjectCID string 18 - IsLiked bool 19 - LikeCount int 20 - CommentCount int 21 - Comments []firehose.IndexedComment 22 - CurrentUserDID string 23 - ShareURL string 24 - IsModerator bool 25 - CanHideRecord bool 26 - CanBlockUser bool 27 - IsRecordHidden bool 13 + Grinder *models.Grinder 14 + IsOwnProfile bool 15 + IsAuthenticated bool 16 + SubjectURI string 17 + SubjectCID string 18 + IsLiked bool 19 + LikeCount int 20 + CommentCount int 21 + Comments []firehose.IndexedComment 22 + CurrentUserDID string 23 + ShareURL string 24 + IsModerator bool 25 + CanHideRecord bool 26 + CanBlockUser bool 27 + IsRecordHidden bool 28 28 AuthorDID string 29 29 AuthorHandle string 30 30 AuthorDisplayName string ··· 53 53 AuthorDisplay: props.AuthorDisplayName, 54 54 AuthorAvatar: props.AuthorAvatar, 55 55 }) 56 - <div class="space-y-4"> 57 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconGear(), Label: "Type", Value: props.Grinder.GrinderType}) 58 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconDisc(), Label: "Burr Type", Value: props.Grinder.BurrType}) 59 - </div> 60 - if props.Grinder.Notes != "" { 61 - <div class="mt-6"> 62 - <span class="detail-label mb-2 block"> 63 - <span class="inline-flex items-center gap-1"> 64 - @components.IconFileText() 65 - Notes 66 - </span> 67 - </span> 68 - <div class="prose-note">{ props.Grinder.Notes }</div> 56 + <div class="record-journal p-4"> 57 + <div> 58 + @components.JournalField(components.DetailStackedProps{Icon: components.IconGear(), Label: "Type", Value: props.Grinder.GrinderType}) 59 + @components.JournalField(components.DetailStackedProps{Icon: components.IconDisc(), Label: "Burr Type", Value: props.Grinder.BurrType}) 69 60 </div> 70 - } 61 + if props.Grinder.Notes != "" { 62 + <div class="mt-6"> 63 + <span class="detail-label mb-2 block"> 64 + <span class="inline-flex items-center gap-1"> 65 + @components.IconFileText() 66 + Notes 67 + </span> 68 + </span> 69 + <div class="journal-prose">{ props.Grinder.Notes }</div> 70 + </div> 71 + } 72 + </div> 71 73 if props.BrewCount > 0 { 72 74 <div class="record-stat-line"> 73 75 <span class="flex items-center gap-1"> ··· 114 116 ViewURL: props.ShareURL, 115 117 }) 116 118 } 117 -
+50 -49
internal/web/pages/recipe_view.templ
··· 66 66 </a> 67 67 </p> 68 68 } 69 - <div class="detail-grid"> 70 - if props.Recipe.CoffeeAmount > 0 { 71 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconCoffee(), Label: "Coffee", Value: fmt.Sprintf("%.1fg", props.Recipe.CoffeeAmount)}) 72 - } 73 - if props.Recipe.WaterAmount > 0 { 74 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconDroplet(), Label: "Water", Value: fmt.Sprintf("%.1fg", props.Recipe.WaterAmount)}) 75 - } 76 - if props.Recipe.Ratio > 0 { 77 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconScale(), Label: "Ratio", Value: fmt.Sprintf("1:%.1f", props.Recipe.Ratio)}) 78 - } 79 - if props.Recipe.BrewerObj != nil { 80 - @components.DetailStacked(components.DetailStackedProps{ 81 - Icon: components.IconBrewer(), 82 - Label: "Brewer", 83 - Value: props.Recipe.BrewerObj.Name, 84 - LinkHref: recipeBrewerLink(props), 85 - }) 86 - } else if props.Recipe.BrewerType != "" { 87 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconBrewer(), Label: "Brewer Type", Value: props.Recipe.BrewerType}) 88 - } 89 - </div> 90 - <div class="space-y-6 mt-6"> 91 - if len(props.Recipe.Pours) > 0 { 92 - <div> 93 - <span class="detail-label mb-3 block"> 94 - <span class="inline-flex items-center gap-1"> 95 - @components.IconDroplet() 96 - Pours 69 + <div class="record-journal p-4"> 70 + <div> 71 + if props.Recipe.CoffeeAmount > 0 { 72 + @components.JournalField(components.DetailStackedProps{Icon: components.IconCoffee(), Label: "Coffee", Value: fmt.Sprintf("%.1fg", props.Recipe.CoffeeAmount)}) 73 + } 74 + if props.Recipe.WaterAmount > 0 { 75 + @components.JournalField(components.DetailStackedProps{Icon: components.IconDroplet(), Label: "Water", Value: fmt.Sprintf("%.1fg", props.Recipe.WaterAmount)}) 76 + } 77 + if props.Recipe.Ratio > 0 { 78 + @components.JournalField(components.DetailStackedProps{Icon: components.IconScale(), Label: "Ratio", Value: fmt.Sprintf("1:%.1f", props.Recipe.Ratio)}) 79 + } 80 + if props.Recipe.BrewerObj != nil { 81 + @components.JournalField(components.DetailStackedProps{ 82 + Icon: components.IconBrewer(), 83 + Label: "Brewer", 84 + Value: props.Recipe.BrewerObj.Name, 85 + LinkHref: recipeBrewerLink(props), 86 + }) 87 + } else if props.Recipe.BrewerType != "" { 88 + @components.JournalField(components.DetailStackedProps{Icon: components.IconBrewer(), Label: "Brewer Type", Value: props.Recipe.BrewerType}) 89 + } 90 + </div> 91 + <div class="space-y-6 mt-6"> 92 + if len(props.Recipe.Pours) > 0 { 93 + <div> 94 + <span class="detail-label mb-3 block"> 95 + <span class="inline-flex items-center gap-1"> 96 + @components.IconDroplet() 97 + Pours 98 + </span> 97 99 </span> 98 - </span> 99 - <div class="space-y-2"> 100 - for _, pour := range props.Recipe.Pours { 101 - <div class="flex gap-4 text-sm py-2" style="border-bottom: 1px solid var(--surface-border)"> 102 - <span class="detail-value">{ fmt.Sprintf("%dg", pour.WaterAmount) }</span> 103 - <span style="color: var(--text-muted)">{ "for " + bff.FormatTime(pour.TimeSeconds) }</span> 104 - </div> 105 - } 100 + <div> 101 + for _, pour := range props.Recipe.Pours { 102 + <div class="flex gap-4 text-sm py-2" style="border-bottom: 1px solid var(--surface-border)"> 103 + <span class="detail-value">{ fmt.Sprintf("%dg", pour.WaterAmount) }</span> 104 + <span style="color: var(--text-muted)">{ "for " + bff.FormatTime(pour.TimeSeconds) }</span> 105 + </div> 106 + } 107 + </div> 106 108 </div> 107 - </div> 108 - } 109 - if props.Recipe.Notes != "" { 110 - <div> 111 - <span class="detail-label mb-2 block"> 112 - <span class="inline-flex items-center gap-1"> 113 - @components.IconFileText() 114 - Notes 109 + } 110 + if props.Recipe.Notes != "" { 111 + <div> 112 + <span class="detail-label mb-2 block"> 113 + <span class="inline-flex items-center gap-1"> 114 + @components.IconFileText() 115 + Notes 116 + </span> 115 117 </span> 116 - </span> 117 - <div class="prose-note">{ props.Recipe.Notes }</div> 118 - </div> 119 - } 118 + <div class="journal-prose">{ props.Recipe.Notes }</div> 119 + </div> 120 + } 121 + </div> 120 122 </div> 121 123 <div class="record-view-footer"> 122 124 <div class="flex items-center gap-3"> ··· 181 183 }) 182 184 } 183 185 184 - 185 186 func recipeBrewerLink(props RecipeViewProps) string { 186 187 if props.Recipe.BrewerObj == nil || props.Recipe.BrewerRKey == "" { 187 188 return ""
+30 -19
internal/web/pages/roaster_view.templ
··· 10 10 ) 11 11 12 12 type RoasterViewProps struct { 13 - Roaster *models.Roaster 14 - IsOwnProfile bool 15 - IsAuthenticated bool 16 - SubjectURI string 17 - SubjectCID string 18 - IsLiked bool 19 - LikeCount int 20 - CommentCount int 21 - Comments []firehose.IndexedComment 22 - CurrentUserDID string 23 - ShareURL string 24 - IsModerator bool 25 - CanHideRecord bool 26 - CanBlockUser bool 27 - IsRecordHidden bool 13 + Roaster *models.Roaster 14 + IsOwnProfile bool 15 + IsAuthenticated bool 16 + SubjectURI string 17 + SubjectCID string 18 + IsLiked bool 19 + LikeCount int 20 + CommentCount int 21 + Comments []firehose.IndexedComment 22 + CurrentUserDID string 23 + ShareURL string 24 + IsModerator bool 25 + CanHideRecord bool 26 + CanBlockUser bool 27 + IsRecordHidden bool 28 28 AuthorDID string 29 29 AuthorHandle string 30 30 AuthorDisplayName string ··· 53 53 AuthorDisplay: props.AuthorDisplayName, 54 54 AuthorAvatar: props.AuthorAvatar, 55 55 }) 56 - <div class="space-y-4"> 57 - @components.DetailStacked(components.DetailStackedProps{Icon: components.IconMapPin(), Label: "Location", Value: props.Roaster.Location, Large: true}) 56 + <div class="record-label p-4"> 57 + <div class="label-detail"> 58 + <span class="detail-label"> 59 + <span class="inline-flex items-center gap-1"> 60 + @components.IconMapPin() 61 + Location 62 + </span> 63 + </span> 64 + if props.Roaster.Location != "" { 65 + <span class="label-origin-hero" style="font-size: 1.5rem">{ props.Roaster.Location }</span> 66 + } else { 67 + <span class="text-sm" style="color: var(--text-faint)">—</span> 68 + } 69 + </div> 58 70 if props.Roaster.Website != "" { 59 - <div class="detail-stacked"> 71 + <div class="label-detail"> 60 72 <span class="detail-label"> 61 73 <span class="inline-flex items-center gap-1"> 62 74 @components.IconLink() ··· 117 129 ViewURL: props.ShareURL, 118 130 }) 119 131 } 120 -
+165 -37
static/css/app.css
··· 34 34 --feed-board-border: #B09470; 35 35 /* Sticky note base — warm off-white */ 36 36 --feed-card-bg: #FFFDF5; 37 - --texture-paper: none; 37 + /* Dot-grid — faint dots on warm paper, for journal pages */ 38 + --texture-dotgrid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='0.7' fill='%23d2bab0' fill-opacity='0.35'/%3E%3C/svg%3E"); 39 + /* Kraft grain — subtle fiber texture for label pages */ 40 + --texture-kraft: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='k'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0.15 0 0 0 0.1 0.1 0 0 0 0.06 0.05 0 0 0 0.02 0 0 0 0.07 0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23k)'/%3E%3C/svg%3E"); 41 + /* Journal paper — warm cream */ 42 + --journal-bg: #FFFDF5; 43 + /* Kraft — slightly warmer/darker than card bg */ 44 + --kraft-bg: #F7F0E8; 38 45 } 39 46 40 47 /* ======================================== ··· 154 161 --surface-bg: rgba(36, 26, 22, 0.6); 155 162 --surface-border: #2E211B; 156 163 164 + /* Journal/label page backgrounds */ 165 + --texture-dotgrid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='0.7' fill='%235A4A40' fill-opacity='0.4'/%3E%3C/svg%3E"); 166 + --texture-kraft: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='k'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0.08 0 0 0 0.04 0.06 0 0 0 0.03 0.03 0 0 0 0.01 0 0 0 0.06 0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23k)'/%3E%3C/svg%3E"); 167 + --journal-bg: #1A1210; 168 + --kraft-bg: #201814; 169 + 157 170 /* Feed board — noticeably lighter than cards for contrast */ 158 171 --feed-board-bg: #3D2D22; 159 172 --feed-board-border: #4A3828; ··· 316 329 --feed-board-bg: #3D2D22; 317 330 --feed-board-border: #4A3828; 318 331 --feed-card-bg: #1A1210; 332 + --texture-dotgrid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='0.7' fill='%235A4A40' fill-opacity='0.4'/%3E%3C/svg%3E"); 333 + --texture-kraft: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='k'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0.08 0 0 0 0.04 0.06 0 0 0 0.03 0.03 0 0 0 0.01 0 0 0 0.06 0'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23k)'/%3E%3C/svg%3E"); 334 + --journal-bg: #1A1210; 335 + --kraft-bg: #201814; 319 336 --header-bg-from: #0F0A08; 320 337 --header-bg-to: #0F0A08; 321 338 --header-border: #2E211B; ··· 561 578 color: var(--text-muted); 562 579 } 563 580 564 - /* Record view — typography-driven detail fields (no container boxes) */ 581 + /* ── Shared record view tokens ── */ 565 582 .detail-label { 566 583 @apply text-xs font-medium uppercase tracking-wider; 567 584 color: var(--text-muted); ··· 577 594 color: var(--text-primary); 578 595 } 579 596 580 - .detail-value a { 581 - color: var(--text-primary); 582 - } 583 - 584 - .detail-value a:hover { 585 - text-decoration: underline; 586 - } 597 + .detail-value a { color: var(--text-primary); } 598 + .detail-value a:hover { text-decoration: underline; } 587 599 588 - /* Stacked: label on top, value below */ 589 600 .detail-stacked { 590 601 @apply flex flex-col gap-1; 591 602 } 592 603 593 - /* Inline: label and value side by side */ 594 604 .detail-inline { 595 605 @apply flex items-baseline gap-2; 596 606 } 597 607 598 - /* Compact parameter grid — tighter than standard, single col on mobile */ 599 - .detail-grid { 600 - @apply grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-4; 608 + .record-view-footer { 609 + @apply flex justify-between items-center pt-4 mt-6; 601 610 } 602 611 603 - /* Journal-entry prose for notes/descriptions */ 604 - .prose-note { 605 - @apply pl-4 whitespace-pre-wrap leading-relaxed; 606 - border-left: 2px solid var(--surface-border); 607 - color: var(--text-primary); 612 + .record-stat-line { 613 + @apply flex items-center gap-3 text-xs pt-3; 614 + color: var(--text-muted); 608 615 } 609 616 610 - /* Bean reference card on brew view */ 611 - .brew-bean-ref { 612 - @apply rounded-lg p-4; 613 - background: var(--surface-bg); 614 - transition: background 0.15s ease; 617 + .brew-rating-hero { 618 + @apply inline-flex items-center gap-2 py-1; 615 619 } 616 620 617 - .brew-bean-ref:hover { 618 - background: var(--type-bean-tint); 621 + .brew-summary { 622 + @apply flex flex-wrap items-center gap-x-8 gap-y-4 mb-6 pb-5; 623 + border-bottom: 1px solid var(--surface-border); 619 624 } 620 625 621 - /* Hero rating for brew view */ 622 - .brew-rating-hero { 623 - @apply inline-flex items-center gap-2 py-1; 626 + .brew-summary-stats { 627 + @apply flex flex-wrap gap-x-6 gap-y-3; 628 + flex: 1 1 auto; 629 + } 630 + 631 + .brew-summary-stat { 632 + @apply flex flex-col gap-0.5; 633 + } 634 + 635 + .brew-summary-stat dt { 636 + @apply uppercase tracking-wider; 637 + font-size: 0.65rem; 638 + color: var(--text-muted); 639 + } 640 + 641 + .brew-summary-stat dd { 642 + @apply text-base font-semibold; 643 + color: var(--text-primary); 644 + font-variant-numeric: tabular-nums; 624 645 } 625 646 626 647 .brew-rating-value { ··· 633 654 color: var(--text-muted); 634 655 } 635 656 636 - /* Clean record footer: back button left, actions right */ 637 - .record-view-footer { 638 - @apply flex justify-between items-center pt-4 mt-6; 639 - border-top: 1px solid var(--surface-border); 657 + /* ── Treatment 1: Brew Journal ── 658 + Dot-grid paper, ledger-style fields, for brews/recipes/equipment */ 659 + 660 + .record-journal { 661 + background-color: var(--journal-bg); 662 + background-image: var(--texture-dotgrid); 663 + @apply rounded-lg; 640 664 } 641 665 642 - /* Stat line at bottom of record (brew count, bean count) */ 643 - .record-stat-line { 644 - @apply flex items-center gap-3 text-xs py-3; 666 + /* Ledger field: label left, value right, separated by faint line */ 667 + .journal-field { 668 + @apply flex items-baseline justify-between py-3; 669 + border-bottom: 1px solid var(--surface-border); 670 + } 671 + 672 + .journal-field:last-child { 673 + border-bottom: none; 674 + } 675 + 676 + .journal-field .detail-label { 677 + @apply flex-shrink-0; 678 + width: 9rem; 679 + } 680 + 681 + .journal-field .detail-value, 682 + .journal-field .detail-value-lg { 683 + @apply text-right flex-1; 684 + } 685 + 686 + .journal-field a.detail-value { 687 + @apply text-right; 688 + } 689 + 690 + /* On mobile, stack journal fields vertically */ 691 + @media (max-width: 480px) { 692 + .journal-field { 693 + @apply flex-col items-start gap-1; 694 + } 695 + .journal-field .detail-value, 696 + .journal-field .detail-value-lg { 697 + @apply text-left; 698 + } 699 + } 700 + 701 + /* Prose section in journal context */ 702 + .journal-prose { 703 + @apply whitespace-pre-wrap leading-relaxed; 704 + color: var(--text-primary); 705 + } 706 + 707 + /* Bean reference inside a journal brew page */ 708 + .journal-bean-ref { 709 + @apply rounded-lg p-4 mb-2; 710 + background: var(--surface-bg); 711 + transition: background 0.15s ease; 712 + } 713 + 714 + .journal-bean-ref:hover { 715 + background: var(--type-bean-tint); 716 + } 717 + 718 + /* ── Treatment 2: Craft Label ── 719 + Kraft paper texture, bold typographic hierarchy, for beans/roasters */ 720 + 721 + .record-label { 722 + background-color: var(--kraft-bg); 723 + background-image: var(--texture-kraft); 724 + @apply rounded-lg; 725 + } 726 + 727 + /* Origin hero — the biggest element on a bean label */ 728 + .label-origin-hero { 729 + @apply text-3xl sm:text-4xl font-bold tracking-tight leading-tight; 730 + color: var(--text-primary); 731 + } 732 + 733 + /* Roaster byline under bean name */ 734 + .label-byline { 735 + @apply text-sm mt-1; 645 736 color: var(--text-muted); 646 - border-top: 1px solid var(--surface-border); 737 + } 738 + 739 + .label-byline a { 740 + color: var(--text-secondary); 741 + } 742 + 743 + .label-byline a:hover { 744 + text-decoration: underline; 745 + color: var(--text-primary); 746 + } 747 + 748 + /* Inline tag strip: variety / process / roast level */ 749 + .label-tags { 750 + @apply flex flex-wrap gap-2 mt-4; 751 + } 752 + 753 + .label-tag { 754 + @apply text-xs font-medium px-2.5 py-1 rounded-full; 755 + background: var(--surface-bg); 756 + color: var(--text-secondary); 757 + border: 1px solid var(--surface-border); 758 + } 759 + 760 + /* Description on a label — flowing paragraph, no indent */ 761 + .label-description { 762 + @apply leading-relaxed mt-6; 763 + color: var(--text-primary); 764 + max-width: 65ch; 765 + } 766 + 767 + /* Stacked detail for label pages (location, website) */ 768 + .label-detail { 769 + @apply flex flex-col gap-1 py-3; 770 + border-bottom: 1px solid var(--surface-border); 771 + } 772 + 773 + .label-detail:last-child { 774 + border-bottom: none; 647 775 } 648 776 649 777 /* Form field groups — semantic clusters within modals */

History

1 round 0 comments
sign up or login to add to the discussion
pdewey.com submitted #0
1 commit
expand
feat: view page redesign cont.
merge conflicts detected
expand
  • internal/web/components/layout.templ:115
  • internal/web/components/shared.templ:218
  • internal/web/pages/bean_view.templ:53
  • internal/web/pages/brew_view.templ:56
  • internal/web/pages/brewer_view.templ:53
  • internal/web/pages/grinder_view.templ:53
  • internal/web/pages/recipe_view.templ:66
  • internal/web/pages/roaster_view.templ:53
  • static/css/app.css:561
expand 0 comments