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: slightly improve experience of clicking recipe on explore page #15

open opened by pdewey.com targeting main from push-qkssutqslksz
  • This is mostly a stop-gap until I dedicate more time into making this page better. The side-drawer thing on desktop might go away.
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:hm5f3dnm6jdhrc55qp2npdja/sh.tangled.repo.pull/3mkuqdz4dt222
+365 -207
Diff #0
+22
internal/handlers/recipe.go
··· 476 476 477 477 filtered := models.FilterRecipes(recipes, filter) 478 478 479 + // Sort results (default: popular) 480 + sortBy := r.URL.Query().Get("sort") 481 + if sortBy == "" { 482 + sortBy = "popular" 483 + } 484 + switch sortBy { 485 + case "popular": 486 + sort.Slice(filtered, func(i, j int) bool { 487 + si := filtered[i].BrewCount + filtered[i].ForkCount 488 + sj := filtered[j].BrewCount + filtered[j].ForkCount 489 + return si > sj 490 + }) 491 + case "newest": 492 + sort.Slice(filtered, func(i, j int) bool { 493 + return filtered[i].CreatedAt.After(filtered[j].CreatedAt) 494 + }) 495 + case "most_forked": 496 + sort.Slice(filtered, func(i, j int) bool { 497 + return filtered[i].ForkCount > filtered[j].ForkCount 498 + }) 499 + } 500 + 479 501 w.Header().Set("Content-Type", "application/json") 480 502 if err := json.NewEncoder(w).Encode(filtered); err != nil { 481 503 log.Error().Err(err).Msg("Failed to encode recipe suggestions response")
+330 -207
internal/web/pages/recipe_explore.templ
··· 133 133 </div> 134 134 </div> 135 135 </div> 136 - <!-- Results --> 137 - <div> 138 - <template x-if="loading"> 139 - <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> 140 - <div class="feed-card animate-pulse"> 141 - <div class="h-4 bg-brown-200 rounded w-1/3 mb-3"></div> 142 - <div class="h-5 bg-brown-200 rounded w-2/3 mb-2"></div> 143 - <div class="h-4 bg-brown-200 rounded w-1/2 mb-3"></div> 144 - <div class="grid grid-cols-3 gap-2"> 145 - <div class="h-12 bg-brown-100 rounded"></div> 146 - <div class="h-12 bg-brown-100 rounded"></div> 147 - <div class="h-12 bg-brown-100 rounded"></div> 136 + <!-- Sort + results count bar --> 137 + <div class="flex items-center justify-between mb-3"> 138 + <template x-if="!loading && recipes.length > 0"> 139 + <p class="text-sm text-brown-600"> 140 + <span x-text="recipes.length"></span> recipe(s) found 141 + </p> 142 + </template> 143 + <template x-if="loading || recipes.length === 0"> 144 + <div></div> 145 + </template> 146 + <div class="flex items-center gap-1 text-sm"> 147 + <span class="text-brown-500 mr-1">Sort:</span> 148 + <button 149 + type="button" 150 + class="filter-pill text-xs" 151 + @click="setSort('popular')" 152 + :class="sortBy === 'popular' ? 'filter-pill-active' : 'filter-pill'" 153 + > 154 + Popular 155 + </button> 156 + <button 157 + type="button" 158 + class="filter-pill text-xs" 159 + @click="setSort('newest')" 160 + :class="sortBy === 'newest' ? 'filter-pill-active' : 'filter-pill'" 161 + > 162 + Newest 163 + </button> 164 + <button 165 + type="button" 166 + class="filter-pill text-xs" 167 + @click="setSort('most_forked')" 168 + :class="sortBy === 'most_forked' ? 'filter-pill-active' : 'filter-pill'" 169 + > 170 + Most Forked 171 + </button> 172 + </div> 173 + </div> 174 + <!-- Results + detail side panel --> 175 + <div class="flex gap-6 items-start"> 176 + <!-- Recipe grid (shrinks when detail panel is open on desktop) --> 177 + <div class="min-w-0" :class="selectedRecipe ? 'flex-1 lg:w-3/5' : 'w-full'"> 178 + <template x-if="loading"> 179 + <div class="grid gap-4" :class="selectedRecipe ? 'grid-cols-1 sm:grid-cols-2' : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'"> 180 + <div class="feed-card animate-pulse"> 181 + <div class="h-4 bg-brown-200 rounded w-1/3 mb-3"></div> 182 + <div class="h-5 bg-brown-200 rounded w-2/3 mb-2"></div> 183 + <div class="h-4 bg-brown-200 rounded w-1/2 mb-3"></div> 184 + <div class="grid grid-cols-3 gap-2"> 185 + <div class="h-12 bg-brown-100 rounded"></div> 186 + <div class="h-12 bg-brown-100 rounded"></div> 187 + <div class="h-12 bg-brown-100 rounded"></div> 188 + </div> 148 189 </div> 149 - </div> 150 - <div class="feed-card animate-pulse hidden sm:block"> 151 - <div class="h-4 bg-brown-200 rounded w-1/3 mb-3"></div> 152 - <div class="h-5 bg-brown-200 rounded w-2/3 mb-2"></div> 153 - <div class="h-4 bg-brown-200 rounded w-1/2 mb-3"></div> 154 - <div class="grid grid-cols-3 gap-2"> 155 - <div class="h-12 bg-brown-100 rounded"></div> 156 - <div class="h-12 bg-brown-100 rounded"></div> 157 - <div class="h-12 bg-brown-100 rounded"></div> 190 + <div class="feed-card animate-pulse hidden sm:block"> 191 + <div class="h-4 bg-brown-200 rounded w-1/3 mb-3"></div> 192 + <div class="h-5 bg-brown-200 rounded w-2/3 mb-2"></div> 193 + <div class="h-4 bg-brown-200 rounded w-1/2 mb-3"></div> 194 + <div class="grid grid-cols-3 gap-2"> 195 + <div class="h-12 bg-brown-100 rounded"></div> 196 + <div class="h-12 bg-brown-100 rounded"></div> 197 + <div class="h-12 bg-brown-100 rounded"></div> 198 + </div> 158 199 </div> 159 200 </div> 160 - <div class="feed-card animate-pulse hidden lg:block"> 161 - <div class="h-4 bg-brown-200 rounded w-1/3 mb-3"></div> 162 - <div class="h-5 bg-brown-200 rounded w-2/3 mb-2"></div> 163 - <div class="h-4 bg-brown-200 rounded w-1/2 mb-3"></div> 164 - <div class="grid grid-cols-3 gap-2"> 165 - <div class="h-12 bg-brown-100 rounded"></div> 166 - <div class="h-12 bg-brown-100 rounded"></div> 167 - <div class="h-12 bg-brown-100 rounded"></div> 168 - </div> 201 + </template> 202 + <template x-if="!loading && recipes.length === 0"> 203 + <div class="card card-inner text-center py-8"> 204 + <p class="text-brown-700 text-lg font-medium">No recipes found</p> 205 + <p class="text-sm text-brown-600 mt-2">Try adjusting your filters or search terms</p> 169 206 </div> 170 - </div> 171 - </template> 172 - <template x-if="!loading && recipes.length === 0"> 173 - <div class="card card-inner text-center py-8"> 174 - <p class="text-brown-700 text-lg font-medium">No recipes found</p> 175 - <p class="text-sm text-brown-600 mt-2">Try adjusting your filters or search terms</p> 176 - </div> 177 - </template> 178 - <template x-if="!loading && recipes.length > 0"> 179 - <div> 180 - <p class="text-sm text-brown-600 mb-3"> 181 - <span x-text="recipes.length"></span> recipe(s) found 182 - </p> 183 - <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> 207 + </template> 208 + <template x-if="!loading && recipes.length > 0"> 209 + <div class="grid gap-4" :class="selectedRecipe ? 'grid-cols-1 sm:grid-cols-2' : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'"> 184 210 <template x-for="recipe in recipes" :key="recipe.rkey"> 185 211 <div 186 - class="feed-card feed-card-recipe cursor-pointer" 212 + class="feed-card feed-card-recipe cursor-pointer transition-shadow" 213 + :class="selectedRecipe && selectedRecipe.rkey === recipe.rkey ? 'ring-2 ring-brown-400' : ''" 187 214 @click="selectRecipe(recipe)" 188 215 > 189 216 <!-- Author row --> ··· 253 280 </div> 254 281 </template> 255 282 </div> 256 - </div> 257 - </template> 258 - </div> 259 - <!-- Recipe detail panel --> 260 - <template x-if="selectedRecipe"> 261 - <div class="card card-inner mt-4"> 262 - <div class="flex justify-between items-start mb-4"> 263 - <div> 264 - <h3 class="text-xl font-bold text-brown-900" x-text="selectedRecipe.name"></h3> 265 - <a 266 - :href="'/profile/' + selectedRecipe.author_did" 267 - class="flex items-center gap-2 mt-1 group/author" 268 - > 269 - <template x-if="selectedRecipe.author_avatar"> 270 - <img :src="selectedRecipe.author_avatar" class="w-6 h-6 rounded-full object-cover" :alt="selectedRecipe.author_display || ''" loading="lazy" width="24" height="24"/> 271 - </template> 272 - <template x-if="!selectedRecipe.author_avatar"> 273 - <div class="w-6 h-6 rounded-full bg-brown-200 flex items-center justify-center text-brown-600 text-xs font-bold" x-text="(selectedRecipe.author_display || selectedRecipe.author_handle || '?')[0].toUpperCase()"></div> 274 - </template> 275 - <div> 276 - <template x-if="selectedRecipe.author_display"> 277 - <span class="block text-sm font-medium text-brown-700 group-hover/author:text-brown-900 group-hover/author:underline transition-colors" x-text="selectedRecipe.author_display"></span> 278 - </template> 279 - <span class="block text-xs text-brown-600 group-hover/author:text-brown-800 transition-colors" x-text="selectedRecipe.author_handle || ''"></span> 280 - </div> 281 - </a> 282 - </div> 283 - <div class="flex items-center gap-2"> 284 - <!-- Actions dropdown --> 285 - <div class="relative" x-data="{ actionsOpen: false }"> 286 - <button 287 - type="button" 288 - @click="actionsOpen = !actionsOpen" 289 - @click.away="actionsOpen = false" 290 - class="action-btn" 291 - aria-label="More options" 292 - > 293 - <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"> 294 - <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"></path> 295 - </svg> 296 - </button> 297 - <div 298 - x-show="actionsOpen" 299 - x-transition:enter="transition ease-out duration-100" 300 - x-transition:enter-start="transform opacity-0 scale-95" 301 - x-transition:enter-end="transform opacity-100 scale-100" 302 - x-transition:leave="transition ease-in duration-75" 303 - x-transition:leave-start="transform opacity-100 scale-100" 304 - x-transition:leave-end="transform opacity-0 scale-95" 305 - class="action-menu bottom-full mb-1" 306 - x-cloak 307 - > 308 - <!-- Share --> 309 - <button type="button" @click="shareRecipe(); actionsOpen = false" class="action-menu-item"> 310 - <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"> 311 - <path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z"></path> 312 - </svg> 313 - Share 314 - </button> 315 - <!-- Report (only for authenticated non-owners) --> 316 - <template x-if="isAuthenticated && !isOwner(selectedRecipe)"> 283 + </template> 284 + </div> 285 + <!-- Recipe detail side panel (sticky on desktop, below grid on mobile) --> 286 + <template x-if="selectedRecipe"> 287 + <div class="hidden lg:block lg:w-2/5 lg:sticky lg:top-4"> 288 + <div class="card card-inner"> 289 + <div class="flex justify-between items-start mb-4"> 290 + <div class="min-w-0 flex-1"> 291 + <h3 class="text-xl font-bold text-brown-900 break-words" x-text="selectedRecipe.name"></h3> 292 + <a 293 + :href="'/profile/' + selectedRecipe.author_did" 294 + class="flex items-center gap-2 mt-1 group/author" 295 + > 296 + <template x-if="selectedRecipe.author_avatar"> 297 + <img :src="selectedRecipe.author_avatar" class="w-6 h-6 rounded-full object-cover" :alt="selectedRecipe.author_display || ''" loading="lazy" width="24" height="24"/> 298 + </template> 299 + <template x-if="!selectedRecipe.author_avatar"> 300 + <div class="w-6 h-6 rounded-full bg-brown-200 flex items-center justify-center text-brown-600 text-xs font-bold" x-text="(selectedRecipe.author_display || selectedRecipe.author_handle || '?')[0].toUpperCase()"></div> 301 + </template> 317 302 <div> 318 - <div class="action-menu-divider"></div> 319 - <button type="button" @click="openReport(); actionsOpen = false" class="action-menu-item"> 303 + <template x-if="selectedRecipe.author_display"> 304 + <span class="block text-sm font-medium text-brown-700 group-hover/author:text-brown-900 group-hover/author:underline transition-colors" x-text="selectedRecipe.author_display"></span> 305 + </template> 306 + <span class="block text-xs text-brown-600 group-hover/author:text-brown-800 transition-colors" x-text="selectedRecipe.author_handle || ''"></span> 307 + </div> 308 + </a> 309 + </div> 310 + <div class="flex items-center gap-2 shrink-0"> 311 + <!-- Actions dropdown --> 312 + <div class="relative" x-data="{ actionsOpen: false }"> 313 + <button 314 + type="button" 315 + @click="actionsOpen = !actionsOpen" 316 + @click.away="actionsOpen = false" 317 + class="action-btn" 318 + aria-label="More options" 319 + > 320 + <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"> 321 + <path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"></path> 322 + </svg> 323 + </button> 324 + <div 325 + x-show="actionsOpen" 326 + x-transition:enter="transition ease-out duration-100" 327 + x-transition:enter-start="transform opacity-0 scale-95" 328 + x-transition:enter-end="transform opacity-100 scale-100" 329 + x-transition:leave="transition ease-in duration-75" 330 + x-transition:leave-start="transform opacity-100 scale-100" 331 + x-transition:leave-end="transform opacity-0 scale-95" 332 + class="action-menu bottom-full mb-1" 333 + x-cloak 334 + > 335 + <!-- Share --> 336 + <button type="button" @click="shareRecipe(); actionsOpen = false" class="action-menu-item"> 320 337 <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"> 321 - <path stroke-linecap="round" stroke-linejoin="round" d="M3 3v1.5M3 21v-6m0 0 2.77-.693a9 9 0 0 1 6.208.682l.108.054a9 9 0 0 0 6.086.71l3.114-.732a48.524 48.524 0 0 1-.005-10.499l-3.11.732a9 9 0 0 1-6.085-.711l-.108-.054a9 9 0 0 0-6.208-.682L3 4.5M3 15V4.5"></path> 338 + <path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z"></path> 322 339 </svg> 323 - Report 340 + Share 324 341 </button> 342 + <!-- Report (only for authenticated non-owners) --> 343 + <template x-if="isAuthenticated && !isOwner(selectedRecipe)"> 344 + <div> 345 + <div class="action-menu-divider"></div> 346 + <button type="button" @click="openReport(); actionsOpen = false" class="action-menu-item"> 347 + <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"> 348 + <path stroke-linecap="round" stroke-linejoin="round" d="M3 3v1.5M3 21v-6m0 0 2.77-.693a9 9 0 0 1 6.208.682l.108.054a9 9 0 0 0 6.086.71l3.114-.732a48.524 48.524 0 0 1-.005-10.499l-3.11.732a9 9 0 0 1-6.085-.711l-.108-.054a9 9 0 0 0-6.208-.682L3 4.5M3 15V4.5"></path> 349 + </svg> 350 + Report 351 + </button> 352 + </div> 353 + </template> 325 354 </div> 326 - </template> 355 + </div> 356 + <button @click="selectedRecipe = null" class="text-brown-500 hover:text-brown-700 text-lg font-bold" aria-label="Close recipe details">&times;</button> 327 357 </div> 328 358 </div> 329 - <button @click="selectedRecipe = null" class="text-brown-500 hover:text-brown-700 text-lg font-bold" aria-label="Close recipe details">&times;</button> 330 - </div> 331 - </div> 332 - <template x-if="getBrewerDisplay(selectedRecipe) !== '-'"> 333 - <div class="mb-4"> 334 - <span class="text-xs text-brown-600 uppercase">Brewer</span> 335 - <p class="font-medium text-brown-900" x-text="getBrewerDisplay(selectedRecipe)"></p> 336 - </div> 337 - </template> 338 - <div class="grid grid-cols-3 gap-4 mb-4"> 339 - <div> 340 - <span class="text-xs text-brown-600 uppercase">Coffee</span> 341 - <p class="font-medium text-brown-900" x-text="selectedRecipe.coffee_amount > 0 ? selectedRecipe.coffee_amount.toFixed(1) + 'g' : '-'"></p> 342 - </div> 343 - <div> 344 - <span class="text-xs text-brown-600 uppercase">Water</span> 345 - <p class="font-medium text-brown-900" x-text="selectedRecipe.water_amount > 0 ? selectedRecipe.water_amount.toFixed(1) + 'g' : '-'"></p> 346 - </div> 347 - <div> 348 - <span class="text-xs text-brown-600 uppercase">Ratio</span> 349 - <p class="font-medium text-brown-900" x-text="formatRatio(selectedRecipe)"></p> 350 - </div> 351 - </div> 352 - <template x-if="selectedRecipe.pours && selectedRecipe.pours.length > 0"> 353 - <div class="mb-4"> 354 - <span class="text-xs text-brown-600 uppercase">Pours</span> 355 - <div class="flex flex-wrap gap-2 mt-1"> 356 - <template x-for="(pour, i) in selectedRecipe.pours" :key="i"> 357 - <span class="inline-flex items-center gap-1.5 text-xs bg-brown-50 px-2.5 py-1 rounded-full border border-brown-200"> 358 - <span class="font-medium text-brown-800" x-text="(i+1)"></span> 359 - <span class="text-brown-700" x-text="pour.water_amount + 'g'"></span> 360 - <span class="text-brown-400">&middot;</span> 361 - <span class="text-brown-600" x-text="pour.time_seconds + 's'"></span> 362 - </span> 363 - </template> 359 + <template x-if="getBrewerDisplay(selectedRecipe) !== '-'"> 360 + <div class="mb-4"> 361 + <span class="text-xs text-brown-600 uppercase">Brewer</span> 362 + <p class="font-medium text-brown-900" x-text="getBrewerDisplay(selectedRecipe)"></p> 363 + </div> 364 + </template> 365 + <div class="grid grid-cols-3 gap-4 mb-4"> 366 + <div> 367 + <span class="text-xs text-brown-600 uppercase">Coffee</span> 368 + <p class="font-medium text-brown-900" x-text="selectedRecipe.coffee_amount > 0 ? selectedRecipe.coffee_amount.toFixed(1) + 'g' : '-'"></p> 369 + </div> 370 + <div> 371 + <span class="text-xs text-brown-600 uppercase">Water</span> 372 + <p class="font-medium text-brown-900" x-text="selectedRecipe.water_amount > 0 ? selectedRecipe.water_amount.toFixed(1) + 'g' : '-'"></p> 373 + </div> 374 + <div> 375 + <span class="text-xs text-brown-600 uppercase">Ratio</span> 376 + <p class="font-medium text-brown-900" x-text="formatRatio(selectedRecipe)"></p> 377 + </div> 364 378 </div> 365 - </div> 366 - </template> 367 - <template x-if="selectedRecipe.notes"> 368 - <div class="mb-4"> 369 - <span class="text-xs text-brown-600 uppercase">Notes</span> 370 - <p class="text-sm text-brown-800 mt-1 whitespace-pre-wrap" x-text="selectedRecipe.notes"></p> 371 - </div> 372 - </template> 373 - <template x-if="selectedRecipe.brew_count > 0 || selectedRecipe.fork_count > 0"> 374 - <div class="flex items-center gap-4 mb-4 text-sm text-brown-600"> 375 - <template x-if="selectedRecipe.brew_count > 0"> 376 - <span class="flex items-center gap-1.5"> 377 - <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0 1 12 21 8.25 8.25 0 0 1 6.038 7.047 8.287 8.287 0 0 0 9 9.601a8.983 8.983 0 0 1 3.361-6.867 8.21 8.21 0 0 0 3 2.48Z"></path></svg> 378 - <span x-text="selectedRecipe.brew_count + ' brew' + (selectedRecipe.brew_count !== 1 ? 's' : '')"></span> 379 - </span> 379 + <template x-if="selectedRecipe.pours && selectedRecipe.pours.length > 0"> 380 + <div class="mb-4"> 381 + <span class="text-xs text-brown-600 uppercase">Pours</span> 382 + <div class="flex flex-wrap gap-2 mt-1"> 383 + <template x-for="(pour, i) in selectedRecipe.pours" :key="i"> 384 + <span class="inline-flex items-center gap-1.5 text-xs bg-brown-50 px-2.5 py-1 rounded-full border border-brown-200"> 385 + <span class="font-medium text-brown-800" x-text="(i+1)"></span> 386 + <span class="text-brown-700" x-text="pour.water_amount + 'g'"></span> 387 + <span class="text-brown-400">&middot;</span> 388 + <span class="text-brown-600" x-text="pour.time_seconds + 's'"></span> 389 + </span> 390 + </template> 391 + </div> 392 + </div> 380 393 </template> 381 - <template x-if="selectedRecipe.fork_count > 0"> 382 - <span class="flex items-center gap-1.5"> 383 - <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"></path></svg> 384 - <span x-text="selectedRecipe.fork_count + ' fork' + (selectedRecipe.fork_count !== 1 ? 's' : '')"></span> 385 - <template x-if="selectedRecipe.forker_avatars && selectedRecipe.forker_avatars.length > 0"> 386 - <div class="flex -space-x-1.5 ml-1"> 387 - <template x-for="(avatar, i) in selectedRecipe.forker_avatars.slice(0, 5)" :key="i"> 388 - <img :src="avatar" alt="" class="w-5 h-5 rounded-full object-cover border border-white" loading="lazy" width="20" height="20"/> 394 + <template x-if="selectedRecipe.notes"> 395 + <div class="mb-4"> 396 + <span class="text-xs text-brown-600 uppercase">Notes</span> 397 + <p class="text-sm text-brown-800 mt-1 whitespace-pre-wrap" x-text="selectedRecipe.notes"></p> 398 + </div> 399 + </template> 400 + <template x-if="selectedRecipe.brew_count > 0 || selectedRecipe.fork_count > 0"> 401 + <div class="flex items-center gap-4 mb-4 text-sm text-brown-600"> 402 + <template x-if="selectedRecipe.brew_count > 0"> 403 + <span class="flex items-center gap-1.5"> 404 + <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0 1 12 21 8.25 8.25 0 0 1 6.038 7.047 8.287 8.287 0 0 0 9 9.601a8.983 8.983 0 0 1 3.361-6.867 8.21 8.21 0 0 0 3 2.48Z"></path></svg> 405 + <span x-text="selectedRecipe.brew_count + ' brew' + (selectedRecipe.brew_count !== 1 ? 's' : '')"></span> 406 + </span> 407 + </template> 408 + <template x-if="selectedRecipe.fork_count > 0"> 409 + <span class="flex items-center gap-1.5"> 410 + <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"></path></svg> 411 + <span x-text="selectedRecipe.fork_count + ' fork' + (selectedRecipe.fork_count !== 1 ? 's' : '')"></span> 412 + <template x-if="selectedRecipe.forker_avatars && selectedRecipe.forker_avatars.length > 0"> 413 + <div class="flex -space-x-1.5 ml-1"> 414 + <template x-for="(avatar, i) in selectedRecipe.forker_avatars.slice(0, 5)" :key="i"> 415 + <img :src="avatar" alt="" class="w-5 h-5 rounded-full object-cover border border-white" loading="lazy" width="20" height="20"/> 416 + </template> 417 + </div> 389 418 </template> 390 - </div> 419 + </span> 391 420 </template> 392 - </span> 421 + </div> 393 422 </template> 423 + <template x-if="selectedRecipe.source_ref"> 424 + <p class="text-sm text-brown-500 mb-3"> 425 + Forked from&#32; 426 + <template x-if="selectedRecipe.source_author_display || selectedRecipe.source_author_handle"> 427 + <a 428 + :href="getSourceRecipeURL(selectedRecipe)" 429 + class="text-brown-700 underline hover:text-brown-900" 430 + @click.stop 431 + x-text="(selectedRecipe.source_author_display || selectedRecipe.source_author_handle) + '\'s recipe'" 432 + ></a> 433 + </template> 434 + <template x-if="!selectedRecipe.source_author_display && !selectedRecipe.source_author_handle"> 435 + <span>another recipe</span> 436 + </template> 437 + </p> 438 + </template> 439 + <div class="flex flex-col gap-2"> 440 + <a 441 + :href="'/brews/new?recipe=' + selectedRecipe.rkey + '&recipe_owner=' + (selectedRecipe.author_did || '')" 442 + class="btn-primary text-sm text-center" 443 + > 444 + Use in Brew 445 + </a> 446 + <template x-if="isAuthenticated && !isOwner(selectedRecipe)"> 447 + <button 448 + type="button" 449 + @click="forkRecipe()" 450 + class="btn-secondary text-sm" 451 + > 452 + Copy Recipe 453 + </button> 454 + </template> 455 + <a 456 + :href="'/recipes/' + selectedRecipe.rkey + '?owner=' + encodeURIComponent(selectedRecipe.author_handle || selectedRecipe.author_did)" 457 + class="btn-secondary text-sm text-center" 458 + > 459 + View Recipe 460 + </a> 461 + </div> 394 462 </div> 395 - </template> 396 - <template x-if="selectedRecipe.source_ref"> 397 - <p class="text-sm text-brown-500 mb-3"> 398 - Forked from&#32; 399 - <template x-if="selectedRecipe.source_author_display || selectedRecipe.source_author_handle"> 463 + </div> 464 + </template> 465 + </div> 466 + <!-- Mobile detail panel (shown below grid on small screens) --> 467 + <template x-if="selectedRecipe"> 468 + <div class="lg:hidden mt-4" x-ref="mobileDetail"> 469 + <div class="card card-inner"> 470 + <div class="flex justify-between items-start mb-4"> 471 + <div class="min-w-0 flex-1"> 472 + <h3 class="text-xl font-bold text-brown-900 break-words" x-text="selectedRecipe.name"></h3> 400 473 <a 401 - :href="getSourceRecipeURL(selectedRecipe)" 402 - class="text-brown-700 underline hover:text-brown-900" 403 - @click.stop 404 - x-text="(selectedRecipe.source_author_display || selectedRecipe.source_author_handle) + '\'s recipe'" 405 - ></a> 406 - </template> 407 - <template x-if="!selectedRecipe.source_author_display && !selectedRecipe.source_author_handle"> 408 - <span>another recipe</span> 474 + :href="'/profile/' + selectedRecipe.author_did" 475 + class="flex items-center gap-2 mt-1 group/author" 476 + > 477 + <template x-if="selectedRecipe.author_avatar"> 478 + <img :src="selectedRecipe.author_avatar" class="w-6 h-6 rounded-full object-cover" :alt="selectedRecipe.author_display || ''" loading="lazy" width="24" height="24"/> 479 + </template> 480 + <template x-if="!selectedRecipe.author_avatar"> 481 + <div class="w-6 h-6 rounded-full bg-brown-200 flex items-center justify-center text-brown-600 text-xs font-bold" x-text="(selectedRecipe.author_display || selectedRecipe.author_handle || '?')[0].toUpperCase()"></div> 482 + </template> 483 + <div> 484 + <template x-if="selectedRecipe.author_display"> 485 + <span class="block text-sm font-medium text-brown-700 group-hover/author:text-brown-900 group-hover/author:underline transition-colors" x-text="selectedRecipe.author_display"></span> 486 + </template> 487 + <span class="block text-xs text-brown-600 group-hover/author:text-brown-800 transition-colors" x-text="selectedRecipe.author_handle || ''"></span> 488 + </div> 489 + </a> 490 + </div> 491 + <button @click="selectedRecipe = null" class="text-brown-500 hover:text-brown-700 text-lg font-bold shrink-0" aria-label="Close recipe details">&times;</button> 492 + </div> 493 + <template x-if="getBrewerDisplay(selectedRecipe) !== '-'"> 494 + <div class="mb-4"> 495 + <span class="text-xs text-brown-600 uppercase">Brewer</span> 496 + <p class="font-medium text-brown-900" x-text="getBrewerDisplay(selectedRecipe)"></p> 497 + </div> 498 + </template> 499 + <div class="grid grid-cols-3 gap-4 mb-4"> 500 + <div> 501 + <span class="text-xs text-brown-600 uppercase">Coffee</span> 502 + <p class="font-medium text-brown-900" x-text="selectedRecipe.coffee_amount > 0 ? selectedRecipe.coffee_amount.toFixed(1) + 'g' : '-'"></p> 503 + </div> 504 + <div> 505 + <span class="text-xs text-brown-600 uppercase">Water</span> 506 + <p class="font-medium text-brown-900" x-text="selectedRecipe.water_amount > 0 ? selectedRecipe.water_amount.toFixed(1) + 'g' : '-'"></p> 507 + </div> 508 + <div> 509 + <span class="text-xs text-brown-600 uppercase">Ratio</span> 510 + <p class="font-medium text-brown-900" x-text="formatRatio(selectedRecipe)"></p> 511 + </div> 512 + </div> 513 + <template x-if="selectedRecipe.pours && selectedRecipe.pours.length > 0"> 514 + <div class="mb-4"> 515 + <span class="text-xs text-brown-600 uppercase">Pours</span> 516 + <div class="flex flex-wrap gap-2 mt-1"> 517 + <template x-for="(pour, i) in selectedRecipe.pours" :key="i"> 518 + <span class="inline-flex items-center gap-1.5 text-xs bg-brown-50 px-2.5 py-1 rounded-full border border-brown-200"> 519 + <span class="font-medium text-brown-800" x-text="(i+1)"></span> 520 + <span class="text-brown-700" x-text="pour.water_amount + 'g'"></span> 521 + <span class="text-brown-400">&middot;</span> 522 + <span class="text-brown-600" x-text="pour.time_seconds + 's'"></span> 523 + </span> 524 + </template> 525 + </div> 526 + </div> 527 + </template> 528 + <template x-if="selectedRecipe.notes"> 529 + <div class="mb-4"> 530 + <span class="text-xs text-brown-600 uppercase">Notes</span> 531 + <p class="text-sm text-brown-800 mt-1 whitespace-pre-wrap" x-text="selectedRecipe.notes"></p> 532 + </div> 533 + </template> 534 + <div class="flex flex-col gap-2"> 535 + <a 536 + :href="'/brews/new?recipe=' + selectedRecipe.rkey + '&recipe_owner=' + (selectedRecipe.author_did || '')" 537 + class="btn-primary text-sm text-center" 538 + > 539 + Use in Brew 540 + </a> 541 + <template x-if="isAuthenticated && !isOwner(selectedRecipe)"> 542 + <button 543 + type="button" 544 + @click="forkRecipe()" 545 + class="btn-secondary text-sm" 546 + > 547 + Copy Recipe 548 + </button> 409 549 </template> 410 - </p> 411 - </template> 412 - <div class="flex flex-col sm:flex-row gap-2 sm:gap-3"> 413 - <a 414 - :href="'/brews/new?recipe=' + selectedRecipe.rkey + '&recipe_owner=' + (selectedRecipe.author_did || '')" 415 - class="btn-primary text-sm text-center" 416 - > 417 - Use in Brew 418 - </a> 419 - <template x-if="isAuthenticated && !isOwner(selectedRecipe)"> 420 - <button 421 - type="button" 422 - @click="forkRecipe()" 423 - class="btn-secondary text-sm" 550 + <a 551 + :href="'/recipes/' + selectedRecipe.rkey + '?owner=' + encodeURIComponent(selectedRecipe.author_handle || selectedRecipe.author_did)" 552 + class="btn-secondary text-sm text-center" 424 553 > 425 - Copy Recipe 426 - </button> 427 - </template> 428 - <a 429 - :href="'/recipes/' + selectedRecipe.rkey + '?owner=' + encodeURIComponent(selectedRecipe.author_handle || selectedRecipe.author_did)" 430 - class="btn-secondary text-sm text-center" 431 - > 432 - View Recipe 433 - </a> 554 + View Recipe 555 + </a> 556 + </div> 434 557 </div> 435 558 </div> 436 559 </template>
+13
static/js/recipe-explore.js
··· 9 9 brewerType: "", 10 10 minCoffee: "", 11 11 maxCoffee: "", 12 + sortBy: "popular", 12 13 loading: false, 13 14 recipes: [], 14 15 selectedRecipe: null, ··· 24 25 this.search(); 25 26 }, 26 27 28 + setSort(sort) { 29 + this.sortBy = sort; 30 + this.search(); 31 + }, 32 + 27 33 async search() { 28 34 this.loading = true; 29 35 try { ··· 33 39 if (this.brewerType) params.set("brewer_type", this.brewerType); 34 40 if (this.minCoffee) params.set("min_coffee", this.minCoffee); 35 41 if (this.maxCoffee) params.set("max_coffee", this.maxCoffee); 42 + if (this.sortBy) params.set("sort", this.sortBy); 36 43 37 44 const resp = await fetch(`/api/recipes/suggestions?${params}`, { 38 45 credentials: "same-origin", ··· 51 58 52 59 selectRecipe(recipe) { 53 60 this.selectedRecipe = recipe; 61 + // On mobile (no side panel), scroll to the detail panel 62 + this.$nextTick(() => { 63 + if (window.innerWidth < 1024 && this.$refs.mobileDetail) { 64 + this.$refs.mobileDetail.scrollIntoView({ behavior: "smooth", block: "start" }); 65 + } 66 + }); 54 67 }, 55 68 56 69 formatRatio(recipe) {

History

2 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
feat: slightly improve experience of clicking recipe on explore page
merge conflicts detected
expand
expand 0 comments
pdewey.com submitted #0
1 commit
expand
feat: slightly improve experience of clicking recipe on explore page
expand 0 comments