Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
at main 251 lines 7.3 kB view raw
1package components 2 3import ( 4 "arabica/internal/atproto" 5 "arabica/internal/models" 6 "strconv" 7) 8 9// ProfileContentPartialProps defines props for profile content partial 10type ProfileContentPartialProps struct { 11 Brews []*models.Brew 12 Beans []*models.Bean 13 Roasters []*models.Roaster 14 Grinders []*models.Grinder 15 Brewers []*models.Brewer 16 IsOwnProfile bool 17 ProfileHandle string 18 Profile *atproto.Profile 19 // Like state for brews (keyed by brew RKey) 20 BrewLikeCounts map[string]int 21 BrewLikedByUser map[string]bool 22 BrewCIDs map[string]string // CIDs keyed by brew RKey 23 // Comment counts for brews (keyed by brew RKey) 24 BrewCommentCounts map[string]int 25 IsAuthenticated bool 26} 27 28// ProfileContentPartial renders the profile tabs content (for HTMX loading) 29templ ProfileContentPartial(props ProfileContentPartialProps) { 30 <!-- Hidden div with stats data for JavaScript --> 31 <div id="profile-stats-data" class="hidden" data-brews={ strconv.Itoa(len(props.Brews)) } data-beans={ strconv.Itoa(len(props.Beans)) } data-roasters={ strconv.Itoa(len(props.Roasters)) } data-grinders={ strconv.Itoa(len(props.Grinders)) } data-brewers={ strconv.Itoa(len(props.Brewers)) }></div> 32 <!-- Brews Tab --> 33 <div x-show="activeTab === 'brews'"> 34 @ProfileBrewCards(ProfileBrewCardsProps{ 35 Brews: props.Brews, 36 IsOwnProfile: props.IsOwnProfile, 37 ProfileHandle: props.ProfileHandle, 38 Profile: props.Profile, 39 LikeCounts: props.BrewLikeCounts, 40 CommentCounts: props.BrewCommentCounts, 41 LikedByUser: props.BrewLikedByUser, 42 BrewCIDs: props.BrewCIDs, 43 IsAuthenticated: props.IsAuthenticated, 44 }) 45 </div> 46 <!-- Beans Tab --> 47 <div x-show="activeTab === 'beans'"> 48 @ProfileBeansTab(props.Beans, props.Roasters, props.IsOwnProfile, props.ProfileHandle) 49 </div> 50 <!-- Equipment Tab --> 51 <div x-show="activeTab === 'equipment'"> 52 @ProfileEquipmentTab(props.Grinders, props.Brewers, props.IsOwnProfile, props.ProfileHandle) 53 </div> 54} 55 56// ProfileBeansTab renders the beans and roasters tab for profile 57templ ProfileBeansTab(beans []*models.Bean, roasters []*models.Roaster, isOwnProfile bool, profileHandle string) { 58 <div class="space-y-6"> 59 <!-- Open Bags Section --> 60 <div> 61 <h4 class="text-lg font-semibold text-brown-900 mb-3">Open Bags</h4> 62 if len(filterOpenBeans(beans)) == 0 { 63 if isOwnProfile { 64 @EmptyState(EmptyStateProps{ 65 Message: "No open bags yet! Add your first bean.", 66 ActionURL: "/manage", 67 ActionText: "Go to Manage", 68 }) 69 } else { 70 @EmptyState(EmptyStateProps{ 71 Message: "No open bags yet.", 72 }) 73 } 74 } else { 75 @BeanCards(BeanCardsProps{ 76 Beans: filterOpenBeans(beans), 77 ShowActions: false, 78 OwnerHandle: profileHandle, 79 }) 80 } 81 </div> 82 <!-- Roasters Section --> 83 <div> 84 <h4 class="text-lg font-semibold text-brown-900 mb-3">Roasters</h4> 85 if len(roasters) == 0 { 86 if isOwnProfile { 87 @EmptyState(EmptyStateProps{Message: "No roasters added yet."}) 88 } else { 89 @EmptyState(EmptyStateProps{Message: "No roasters yet."}) 90 } 91 } else { 92 @RoastersTable(RoastersTableProps{ 93 Roasters: roasters, 94 ShowActions: false, 95 OwnerHandle: profileHandle, 96 }) 97 } 98 </div> 99 <!-- Closed Bags Section --> 100 <div> 101 <h4 class="text-lg font-semibold text-brown-900 mb-3">Closed Bags</h4> 102 if len(filterClosedBeans(beans)) == 0 { 103 @EmptyState(EmptyStateProps{ 104 Message: "No closed bags.", 105 }) 106 } else { 107 @BeanCards(BeanCardsProps{ 108 Beans: filterClosedBeans(beans), 109 ShowActions: false, 110 OwnerHandle: profileHandle, 111 }) 112 } 113 </div> 114 </div> 115} 116 117// filterOpenBeans returns only beans that are not closed 118func filterOpenBeans(beans []*models.Bean) []*models.Bean { 119 var openBeans []*models.Bean 120 for _, bean := range beans { 121 if !bean.Closed { 122 openBeans = append(openBeans, bean) 123 } 124 } 125 return openBeans 126} 127 128// filterClosedBeans returns only beans that are closed 129func filterClosedBeans(beans []*models.Bean) []*models.Bean { 130 var closedBeans []*models.Bean 131 for _, bean := range beans { 132 if bean.Closed { 133 closedBeans = append(closedBeans, bean) 134 } 135 } 136 return closedBeans 137} 138 139// ProfileBrewCardsProps defines props for the profile brew cards component 140type ProfileBrewCardsProps struct { 141 Brews []*models.Brew 142 IsOwnProfile bool 143 ProfileHandle string 144 Profile *atproto.Profile 145 LikeCounts map[string]int 146 CommentCounts map[string]int 147 LikedByUser map[string]bool 148 IsAuthenticated bool 149 BrewCIDs map[string]string // CIDs keyed by brew RKey 150} 151 152// ProfileBrewCards renders brews as feed-style cards 153templ ProfileBrewCards(props ProfileBrewCardsProps) { 154 if len(props.Brews) == 0 { 155 if props.IsOwnProfile { 156 @EmptyState(EmptyStateProps{ 157 Message: "No brews recorded yet! Start tracking your coffee journey.", 158 ActionURL: "/brews/new", 159 ActionText: "Add Your First Brew", 160 }) 161 } else { 162 @EmptyState(EmptyStateProps{ 163 Message: "No brews yet.", 164 }) 165 } 166 } else { 167 <div class="space-y-4"> 168 for _, brew := range props.Brews { 169 @ProfileBrewCard(ProfileBrewCardProps{ 170 Brew: brew, 171 IsOwnProfile: props.IsOwnProfile, 172 ProfileHandle: props.ProfileHandle, 173 Profile: props.Profile, 174 LikeCount: getLikeCount(props.LikeCounts, brew.RKey), 175 CommentCount: getCommentCount(props.CommentCounts, brew.RKey), 176 IsLiked: isLikedByUser(props.LikedByUser, brew.RKey), 177 IsAuthenticated: props.IsAuthenticated, 178 SubjectCID: getBrewCID(props.BrewCIDs, brew.RKey), 179 }) 180 } 181 </div> 182 } 183} 184 185func getLikeCount(counts map[string]int, rkey string) int { 186 if counts == nil { 187 return 0 188 } 189 return counts[rkey] 190} 191 192func isLikedByUser(liked map[string]bool, rkey string) bool { 193 if liked == nil { 194 return false 195 } 196 return liked[rkey] 197} 198 199func getBrewCID(cids map[string]string, rkey string) string { 200 if cids == nil { 201 return "" 202 } 203 return cids[rkey] 204} 205 206func getCommentCount(counts map[string]int, rkey string) int { 207 if counts == nil { 208 return 0 209 } 210 return counts[rkey] 211} 212 213// ProfileEquipmentTab renders the equipment tab for profile (grinders and brewers only) 214templ ProfileEquipmentTab(grinders []*models.Grinder, brewers []*models.Brewer, isOwnProfile bool, profileHandle string) { 215 <div class="space-y-6"> 216 <!-- Grinders Section --> 217 <div> 218 <h4 class="text-lg font-semibold text-brown-900 mb-3">Grinders</h4> 219 if len(grinders) == 0 { 220 if isOwnProfile { 221 @EmptyState(EmptyStateProps{Message: "No grinders added yet."}) 222 } else { 223 @EmptyState(EmptyStateProps{Message: "No grinders yet."}) 224 } 225 } else { 226 @GrindersTable(GrindersTableProps{ 227 Grinders: grinders, 228 ShowActions: false, 229 OwnerHandle: profileHandle, 230 }) 231 } 232 </div> 233 <!-- Brewers Section --> 234 <div> 235 <h4 class="text-lg font-semibold text-brown-900 mb-3">Brewers</h4> 236 if len(brewers) == 0 { 237 if isOwnProfile { 238 @EmptyState(EmptyStateProps{Message: "No brewing devices added yet."}) 239 } else { 240 @EmptyState(EmptyStateProps{Message: "No brewing devices yet."}) 241 } 242 } else { 243 @BrewersTable(BrewersTableProps{ 244 Brewers: brewers, 245 ShowActions: false, 246 OwnerHandle: profileHandle, 247 }) 248 } 249 </div> 250 </div> 251}