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

Configure Feed

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

feat: add user info to view pages

+305 -71
+11
internal/handlers/brew.go
··· 353 353 AuthorDID: brewOwnerDID, 354 354 } 355 355 356 + // Fetch author profile for display 357 + authorDIDForProfile := brewOwnerDID 358 + if authorDIDForProfile == "" { 359 + authorDIDForProfile = didStr 360 + } 361 + if authorProfile := h.getUserProfile(r.Context(), authorDIDForProfile); authorProfile != nil { 362 + brewViewProps.AuthorHandle = authorProfile.Handle 363 + brewViewProps.AuthorDisplayName = authorProfile.DisplayName 364 + brewViewProps.AuthorAvatar = authorProfile.Avatar 365 + } 366 + 356 367 // Render using templ component 357 368 if err := pages.BrewView(layoutData, brewViewProps).Render(r.Context(), w); err != nil { 358 369 http.Error(w, "Failed to render page", http.StatusInternalServerError)
+71
internal/handlers/entity_views.go
··· 225 225 beanViewProps.IsRecordHidden = sd.IsRecordHidden 226 226 beanViewProps.AuthorDID = entityOwnerDID 227 227 228 + // Fetch author profile for display 229 + var authorProfile *bff.UserProfile 230 + authorDIDForProfile := entityOwnerDID 231 + if authorDIDForProfile == "" { 232 + authorDIDForProfile = didStr 233 + } 234 + authorProfile = h.getUserProfile(r.Context(), authorDIDForProfile) 235 + if authorProfile != nil { 236 + beanViewProps.AuthorHandle = authorProfile.Handle 237 + beanViewProps.AuthorDisplayName = authorProfile.DisplayName 238 + beanViewProps.AuthorAvatar = authorProfile.Avatar 239 + } 240 + 228 241 if h.feedIndex != nil && subjectURI != "" { 229 242 ownerDID := entityOwnerDID 230 243 if ownerDID == "" { ··· 357 370 props.IsRecordHidden = sd.IsRecordHidden 358 371 props.AuthorDID = entityOwnerDID 359 372 373 + // Fetch author profile for display 374 + var authorProfile *bff.UserProfile 375 + authorDIDForProfile := entityOwnerDID 376 + if authorDIDForProfile == "" { 377 + authorDIDForProfile = didStr 378 + } 379 + authorProfile = h.getUserProfile(r.Context(), authorDIDForProfile) 380 + if authorProfile != nil { 381 + props.AuthorHandle = authorProfile.Handle 382 + props.AuthorDisplayName = authorProfile.DisplayName 383 + props.AuthorAvatar = authorProfile.Avatar 384 + } 385 + 360 386 if h.feedIndex != nil && subjectURI != "" { 361 387 ownerDID := entityOwnerDID 362 388 if ownerDID == "" { ··· 489 515 props.IsRecordHidden = sd.IsRecordHidden 490 516 props.AuthorDID = entityOwnerDID 491 517 518 + // Fetch author profile for display 519 + { 520 + var authorProfile *bff.UserProfile 521 + authorDIDForProfile := entityOwnerDID 522 + if authorDIDForProfile == "" { 523 + authorDIDForProfile = didStr 524 + } 525 + authorProfile = h.getUserProfile(r.Context(), authorDIDForProfile) 526 + if authorProfile != nil { 527 + props.AuthorHandle = authorProfile.Handle 528 + props.AuthorDisplayName = authorProfile.DisplayName 529 + props.AuthorAvatar = authorProfile.Avatar 530 + } 531 + } 532 + 492 533 if h.feedIndex != nil && subjectURI != "" { 493 534 ownerDID := entityOwnerDID 494 535 if ownerDID == "" { ··· 620 661 props.CanBlockUser = sd.CanBlockUser 621 662 props.IsRecordHidden = sd.IsRecordHidden 622 663 props.AuthorDID = entityOwnerDID 664 + 665 + // Fetch author profile for display 666 + { 667 + var authorProfile *bff.UserProfile 668 + authorDIDForProfile := entityOwnerDID 669 + if authorDIDForProfile == "" { 670 + authorDIDForProfile = didStr 671 + } 672 + authorProfile = h.getUserProfile(r.Context(), authorDIDForProfile) 673 + if authorProfile != nil { 674 + props.AuthorHandle = authorProfile.Handle 675 + props.AuthorDisplayName = authorProfile.DisplayName 676 + props.AuthorAvatar = authorProfile.Avatar 677 + } 678 + } 623 679 624 680 if h.feedIndex != nil && subjectURI != "" { 625 681 ownerDID := entityOwnerDID ··· 787 843 props.CanBlockUser = sd.CanBlockUser 788 844 props.IsRecordHidden = sd.IsRecordHidden 789 845 props.AuthorDID = entityOwnerDID 846 + 847 + // Fetch author profile for display 848 + { 849 + var authorProfile *bff.UserProfile 850 + authorDIDForProfile := entityOwnerDID 851 + if authorDIDForProfile == "" { 852 + authorDIDForProfile = didStr 853 + } 854 + authorProfile = h.getUserProfile(r.Context(), authorDIDForProfile) 855 + if authorProfile != nil { 856 + props.AuthorHandle = authorProfile.Handle 857 + props.AuthorDisplayName = authorProfile.DisplayName 858 + props.AuthorAvatar = authorProfile.Avatar 859 + } 860 + } 790 861 791 862 // Resolve source recipe provenance if this is a fork 792 863 if props.Recipe.SourceRef != "" {
+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.9.2"/> 118 + <link rel="stylesheet" href="/static/css/output.css?v=0.9.3"/> 119 119 <style> 120 120 [x-cloak] { display: none !important; } 121 121 </style>
+51
internal/web/components/shared.templ
··· 108 108 } 109 109 } 110 110 111 + // RecordViewHeaderProps defines properties for the shared record view header. 112 + type RecordViewHeaderProps struct { 113 + RecordType string // "brew", "bean", "roaster", "grinder", "brewer", "recipe" 114 + Title string 115 + Timestamp string // formatted date string 116 + TimestampISO string // ISO 8601 for <time> element 117 + AuthorDID string 118 + AuthorHandle string 119 + AuthorDisplay string 120 + AuthorAvatar string 121 + } 122 + 123 + // RecordViewHeader renders the tinted header region with author attribution and type badge. 124 + templ RecordViewHeader(props RecordViewHeaderProps) { 125 + <div class={ "record-view-header", "record-view-header-" + props.RecordType }> 126 + if props.AuthorHandle != "" { 127 + <div class="record-view-author"> 128 + <a href={ templ.SafeURL("/profile/" + props.AuthorHandle) } class="flex-shrink-0"> 129 + @Avatar(AvatarProps{ 130 + AvatarURL: props.AuthorAvatar, 131 + DisplayName: props.AuthorDisplay, 132 + Size: "md", 133 + }) 134 + </a> 135 + <div class="record-view-author-info"> 136 + <div class="record-view-author-names"> 137 + if props.AuthorDisplay != "" { 138 + <a href={ templ.SafeURL("/profile/" + props.AuthorHandle) } class="record-view-author-displayname"> 139 + { props.AuthorDisplay } 140 + </a> 141 + } 142 + <a href={ templ.SafeURL("/profile/" + props.AuthorHandle) } class="record-view-author-handle"> 143 + { "@" + props.AuthorHandle } 144 + </a> 145 + </div> 146 + <div class="record-view-meta"> 147 + <time datetime={ props.TimestampISO } data-local="long">{ props.Timestamp }</time> 148 + </div> 149 + </div> 150 + @TypeBadge(props.RecordType) 151 + </div> 152 + } else { 153 + <div class="record-view-meta mb-3"> 154 + @TypeBadge(props.RecordType) 155 + <time datetime={ props.TimestampISO } data-local="long">{ props.Timestamp }</time> 156 + </div> 157 + } 158 + <h2 class="record-view-title">{ props.Title }</h2> 159 + </div> 160 + } 161 + 111 162 // EmptyStateProps defines properties for empty state messaging 112 163 type EmptyStateProps struct { 113 164 Message string
+20 -14
internal/web/pages/bean_view.templ
··· 25 25 CanHideRecord bool 26 26 CanBlockUser bool 27 27 IsRecordHidden bool 28 - AuthorDID string 29 - BrewCount int 28 + AuthorDID string 29 + AuthorHandle string 30 + AuthorDisplayName string 31 + AuthorAvatar string 32 + BrewCount int 30 33 } 31 34 32 35 templ BeanView(layout *components.LayoutData, props BeanViewProps) { ··· 40 43 } 41 44 42 45 templ BeanViewCard(props BeanViewProps) { 43 - @BeanViewHeader(props) 46 + @components.RecordViewHeader(components.RecordViewHeaderProps{ 47 + RecordType: "bean", 48 + Title: beanViewTitle(props.Bean), 49 + Timestamp: props.Bean.CreatedAt.Format("January 2, 2006 at 3:04 PM"), 50 + TimestampISO: bff.FormatISO(props.Bean.CreatedAt), 51 + AuthorDID: props.AuthorDID, 52 + AuthorHandle: props.AuthorHandle, 53 + AuthorDisplay: props.AuthorDisplayName, 54 + AuthorAvatar: props.AuthorAvatar, 55 + }) 44 56 <div class="space-y-6"> 45 57 if props.Bean.Roaster != nil && props.Bean.Roaster.Name != "" { 46 58 <div class="section-box"> ··· 178 190 </div> 179 191 } 180 192 181 - templ BeanViewHeader(props BeanViewProps) { 182 - <div class="mb-6"> 183 - <h2 class="text-2xl font-semibold text-brown-900"> 184 - if props.Bean.Name != "" { 185 - { props.Bean.Name } 186 - } else { 187 - { props.Bean.Origin } 188 - } 189 - </h2> 190 - <p class="text-sm text-brown-600 mt-1"><time datetime={ bff.FormatISO(props.Bean.CreatedAt) } data-local="long">{ props.Bean.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</time></p> 191 - </div> 193 + func beanViewTitle(bean *models.Bean) string { 194 + if bean.Name != "" { 195 + return bean.Name 196 + } 197 + return bean.Origin 192 198 } 193 199 194 200 func getBeanShareTitle(bean *models.Bean) string {
+14 -10
internal/web/pages/brew_view.templ
··· 26 26 CanHideRecord bool // User has hide_record permission 27 27 CanBlockUser bool // User has blacklist_user permission 28 28 IsRecordHidden bool // This record is currently hidden 29 - AuthorDID string // DID of the brew author 29 + AuthorDID string // DID of the brew author 30 + AuthorHandle string 31 + AuthorDisplayName string 32 + AuthorAvatar string 30 33 } 31 34 32 35 // BrewView renders the full brew view page ··· 43 46 44 47 // BrewViewCard renders the brew details card 45 48 templ BrewViewCard(props BrewViewProps) { 46 - @BrewViewHeader(props) 49 + @components.RecordViewHeader(components.RecordViewHeaderProps{ 50 + RecordType: "brew", 51 + Title: "Brew Details", 52 + Timestamp: props.Brew.CreatedAt.Format("January 2, 2006 at 3:04 PM"), 53 + TimestampISO: bff.FormatISO(props.Brew.CreatedAt), 54 + AuthorDID: props.AuthorDID, 55 + AuthorHandle: props.AuthorHandle, 56 + AuthorDisplay: props.AuthorDisplayName, 57 + AuthorAvatar: props.AuthorAvatar, 58 + }) 47 59 <!-- Rating: hero element, generous spacing --> 48 60 if props.Brew.Rating > 0 { 49 61 <div class="mb-8"> ··· 108 120 }, 109 121 ViewURL: props.ShareURL, 110 122 }) 111 - </div> 112 - } 113 - 114 - // BrewViewHeader renders the header with title and timestamp 115 - templ BrewViewHeader(props BrewViewProps) { 116 - <div class="mb-6"> 117 - <h2 class="text-2xl font-semibold text-brown-900">Brew Details</h2> 118 - <p class="text-sm text-brown-600 mt-1"><time datetime={ bff.FormatISO(props.Brew.CreatedAt) } data-local="long">{ props.Brew.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</time></p> 119 123 </div> 120 124 } 121 125
+15 -9
internal/web/pages/brewer_view.templ
··· 25 25 CanHideRecord bool 26 26 CanBlockUser bool 27 27 IsRecordHidden bool 28 - AuthorDID string 29 - BrewCount int 28 + AuthorDID string 29 + AuthorHandle string 30 + AuthorDisplayName string 31 + AuthorAvatar string 32 + BrewCount int 30 33 } 31 34 32 35 templ BrewerView(layout *components.LayoutData, props BrewerViewProps) { ··· 40 43 } 41 44 42 45 templ BrewerViewCard(props BrewerViewProps) { 43 - @BrewerViewHeader(props) 46 + @components.RecordViewHeader(components.RecordViewHeaderProps{ 47 + RecordType: "brewer", 48 + Title: props.Brewer.Name, 49 + Timestamp: props.Brewer.CreatedAt.Format("January 2, 2006 at 3:04 PM"), 50 + TimestampISO: bff.FormatISO(props.Brewer.CreatedAt), 51 + AuthorDID: props.AuthorDID, 52 + AuthorHandle: props.AuthorHandle, 53 + AuthorDisplay: props.AuthorDisplayName, 54 + AuthorAvatar: props.AuthorAvatar, 55 + }) 44 56 <div class="space-y-6"> 45 57 @components.DetailField(components.DetailFieldProps{Icon: components.IconCoffee(), Label: "Type", Value: props.Brewer.BrewerType}) 46 58 if props.Brewer.Description != "" { ··· 104 116 </div> 105 117 } 106 118 107 - templ BrewerViewHeader(props BrewerViewProps) { 108 - <div class="mb-6"> 109 - <h2 class="text-2xl font-semibold text-brown-900">{ props.Brewer.Name }</h2> 110 - <p class="text-sm text-brown-600 mt-1"><time datetime={ bff.FormatISO(props.Brewer.CreatedAt) } data-local="long">{ props.Brewer.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</time></p> 111 - </div> 112 - }
+15 -9
internal/web/pages/grinder_view.templ
··· 25 25 CanHideRecord bool 26 26 CanBlockUser bool 27 27 IsRecordHidden bool 28 - AuthorDID string 29 - BrewCount int 28 + AuthorDID string 29 + AuthorHandle string 30 + AuthorDisplayName string 31 + AuthorAvatar string 32 + BrewCount int 30 33 } 31 34 32 35 templ GrinderView(layout *components.LayoutData, props GrinderViewProps) { ··· 40 43 } 41 44 42 45 templ GrinderViewCard(props GrinderViewProps) { 43 - @GrinderViewHeader(props) 46 + @components.RecordViewHeader(components.RecordViewHeaderProps{ 47 + RecordType: "grinder", 48 + Title: props.Grinder.Name, 49 + Timestamp: props.Grinder.CreatedAt.Format("January 2, 2006 at 3:04 PM"), 50 + TimestampISO: bff.FormatISO(props.Grinder.CreatedAt), 51 + AuthorDID: props.AuthorDID, 52 + AuthorHandle: props.AuthorHandle, 53 + AuthorDisplay: props.AuthorDisplayName, 54 + AuthorAvatar: props.AuthorAvatar, 55 + }) 44 56 <div class="space-y-6"> 45 57 <div class="grid grid-cols-2 gap-4"> 46 58 @components.DetailField(components.DetailFieldProps{Icon: components.IconGear(), Label: "Type", Value: props.Grinder.GrinderType}) ··· 107 119 </div> 108 120 } 109 121 110 - templ GrinderViewHeader(props GrinderViewProps) { 111 - <div class="mb-6"> 112 - <h2 class="text-2xl font-semibold text-brown-900">{ props.Grinder.Name }</h2> 113 - <p class="text-sm text-brown-600 mt-1"><time datetime={ bff.FormatISO(props.Grinder.CreatedAt) } data-local="long">{ props.Grinder.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</time></p> 114 - </div> 115 - }
+25 -19
internal/web/pages/recipe_view.templ
··· 25 25 CanBlockUser bool 26 26 IsRecordHidden bool 27 27 AuthorDID string 28 + AuthorHandle string 29 + AuthorDisplayName string 30 + AuthorAvatar string 28 31 SourceRecipeURL string // view URL for the forked-from recipe 29 32 SourceRecipeAuthor string // display name or handle of the original author 30 33 } ··· 41 44 } 42 45 43 46 templ RecipeViewCard(props RecipeViewProps) { 44 - @RecipeViewHeader(props) 47 + @components.RecordViewHeader(components.RecordViewHeaderProps{ 48 + RecordType: "recipe", 49 + Title: props.Recipe.Name, 50 + Timestamp: props.Recipe.CreatedAt.Format("January 2, 2006 at 3:04 PM"), 51 + TimestampISO: bff.FormatISO(props.Recipe.CreatedAt), 52 + AuthorDID: props.AuthorDID, 53 + AuthorHandle: props.AuthorHandle, 54 + AuthorDisplay: props.AuthorDisplayName, 55 + AuthorAvatar: props.AuthorAvatar, 56 + }) 57 + if props.SourceRecipeURL != "" { 58 + <p class="text-sm text-brown-500 -mt-4 mb-4"> 59 + Forked from&#32; 60 + <a href={ templ.SafeURL(props.SourceRecipeURL) } class="text-brown-700 underline hover:text-brown-900"> 61 + if props.SourceRecipeAuthor != "" { 62 + { props.SourceRecipeAuthor + "'s recipe" } 63 + } else { 64 + original recipe 65 + } 66 + </a> 67 + </p> 68 + } 45 69 <div class="space-y-6"> 46 70 <!-- Main details grid --> 47 71 <div class="grid grid-cols-2 gap-4"> ··· 170 194 </div> 171 195 } 172 196 173 - templ RecipeViewHeader(props RecipeViewProps) { 174 - <div class="mb-6"> 175 - <h2 class="text-2xl font-semibold text-brown-900">{ props.Recipe.Name }</h2> 176 - <p class="text-sm text-brown-600 mt-1"><time datetime={ bff.FormatISO(props.Recipe.CreatedAt) } data-local="long">{ props.Recipe.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</time></p> 177 - if props.SourceRecipeURL != "" { 178 - <p class="text-sm text-brown-500 mt-1"> 179 - Forked from&#32; 180 - <a href={ templ.SafeURL(props.SourceRecipeURL) } class="text-brown-700 underline hover:text-brown-900"> 181 - if props.SourceRecipeAuthor != "" { 182 - { props.SourceRecipeAuthor + "'s recipe" } 183 - } else { 184 - original recipe 185 - } 186 - </a> 187 - </p> 188 - } 189 - </div> 190 - } 191 197 192 198 func recipeBrewerLink(props RecipeViewProps) string { 193 199 if props.Recipe.BrewerObj == nil || props.Recipe.BrewerRKey == "" {
+15 -9
internal/web/pages/roaster_view.templ
··· 25 25 CanHideRecord bool 26 26 CanBlockUser bool 27 27 IsRecordHidden bool 28 - AuthorDID string 29 - BeanCount int 28 + AuthorDID string 29 + AuthorHandle string 30 + AuthorDisplayName string 31 + AuthorAvatar string 32 + BeanCount int 30 33 } 31 34 32 35 templ RoasterView(layout *components.LayoutData, props RoasterViewProps) { ··· 40 43 } 41 44 42 45 templ RoasterViewCard(props RoasterViewProps) { 43 - @RoasterViewHeader(props) 46 + @components.RecordViewHeader(components.RecordViewHeaderProps{ 47 + RecordType: "roaster", 48 + Title: props.Roaster.Name, 49 + Timestamp: props.Roaster.CreatedAt.Format("January 2, 2006 at 3:04 PM"), 50 + TimestampISO: bff.FormatISO(props.Roaster.CreatedAt), 51 + AuthorDID: props.AuthorDID, 52 + AuthorHandle: props.AuthorHandle, 53 + AuthorDisplay: props.AuthorDisplayName, 54 + AuthorAvatar: props.AuthorAvatar, 55 + }) 44 56 <div class="space-y-6"> 45 57 <div class="grid grid-cols-2 gap-4"> 46 58 @components.DetailField(components.DetailFieldProps{Icon: components.IconMapPin(), Label: "Location", Value: props.Roaster.Location}) ··· 112 124 </div> 113 125 } 114 126 115 - templ RoasterViewHeader(props RoasterViewProps) { 116 - <div class="mb-6"> 117 - <h2 class="text-2xl font-semibold text-brown-900">{ props.Roaster.Name }</h2> 118 - <p class="text-sm text-brown-600 mt-1"><time datetime={ bff.FormatISO(props.Roaster.CreatedAt) } data-local="long">{ props.Roaster.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</time></p> 119 - </div> 120 - }
+67
static/css/app.css
··· 494 494 @apply rounded-lg p-4; 495 495 } 496 496 497 + /* Record view — tinted header region */ 498 + .record-view-header { 499 + @apply -mx-6 -mt-6 px-6 pt-5 pb-4 mb-6 rounded-t-xl; 500 + } 501 + 502 + .record-view-header-brew { 503 + background: linear-gradient(to bottom, var(--type-brew-tint), transparent); 504 + } 505 + .record-view-header-bean { 506 + background: linear-gradient(to bottom, var(--type-bean-tint), transparent); 507 + } 508 + .record-view-header-recipe { 509 + background: linear-gradient(to bottom, var(--type-recipe-tint), transparent); 510 + } 511 + .record-view-header-roaster { 512 + background: linear-gradient(to bottom, var(--type-roaster-tint), transparent); 513 + } 514 + .record-view-header-grinder { 515 + background: linear-gradient(to bottom, var(--type-grinder-tint), transparent); 516 + } 517 + .record-view-header-brewer { 518 + background: linear-gradient(to bottom, var(--type-brewer-tint), transparent); 519 + } 520 + 521 + /* Record view — author line */ 522 + .record-view-author { 523 + @apply flex items-center gap-3 mb-4; 524 + } 525 + 526 + .record-view-author-info { 527 + @apply flex-1 min-w-0; 528 + } 529 + 530 + .record-view-author-names { 531 + @apply flex items-center gap-2 flex-wrap; 532 + } 533 + 534 + .record-view-author-displayname { 535 + @apply font-semibold truncate; 536 + color: var(--text-primary); 537 + } 538 + 539 + .record-view-author-handle { 540 + @apply text-sm truncate; 541 + color: var(--text-muted); 542 + } 543 + 544 + .record-view-author-handle:hover { 545 + color: var(--text-secondary); 546 + } 547 + 548 + .record-view-meta { 549 + @apply flex items-center gap-3 text-sm; 550 + color: var(--text-muted); 551 + } 552 + 553 + /* Record view — title area */ 554 + .record-view-title { 555 + @apply text-2xl font-bold tracking-tight; 556 + color: var(--text-primary); 557 + } 558 + 559 + .record-view-timestamp { 560 + @apply text-sm mt-1; 561 + color: var(--text-muted); 562 + } 563 + 497 564 /* Form field groups — semantic clusters within modals */ 498 565 .form-fieldset { 499 566 @apply space-y-3;