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
+211 -299
Interdiff #0 #1
internal/handlers/recipe.go

This file has not been changed.

+209 -296
internal/web/pages/recipe_explore.templ
··· 171 171 </button> 172 172 </div> 173 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> 189 - </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> 199 - </div> 200 - </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> 206 - </div> 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'"> 210 - <template x-for="recipe in recipes" :key="recipe.rkey"> 211 - <div 212 - class="feed-card feed-card-recipe cursor-pointer transition-shadow" 213 - :class="selectedRecipe && selectedRecipe.rkey === recipe.rkey ? 'ring-2 ring-brown-400' : ''" 214 - @click="selectRecipe(recipe)" 215 - > 216 - <!-- Author row --> 217 - <a 218 - :href="'/profile/' + recipe.author_did" 219 - class="flex items-center gap-2 mb-3 group/author" 220 - @click.stop 221 - > 222 - <template x-if="recipe.author_avatar"> 223 - <img :src="recipe.author_avatar" class="w-7 h-7 rounded-full object-cover" :alt="recipe.author_display || recipe.author_handle || ''" loading="lazy" width="28" height="28"/> 224 - </template> 225 - <template x-if="!recipe.author_avatar"> 226 - <div class="w-7 h-7 rounded-full bg-brown-200 flex items-center justify-center text-brown-600 text-xs font-bold" x-text="(recipe.author_display || recipe.author_handle || '?')[0].toUpperCase()"></div> 227 - </template> 228 - <div class="min-w-0 flex-1"> 229 - <template x-if="recipe.author_display"> 230 - <span class="block truncate text-sm font-medium text-brown-700 group-hover/author:text-brown-900 group-hover/author:underline transition-colors" x-text="recipe.author_display"></span> 231 - </template> 232 - <span class="block truncate text-xs text-brown-600 group-hover/author:text-brown-800 transition-colors" x-text="recipe.author_handle || ''"></span> 233 - </div> 234 - </a> 235 - <!-- Recipe name --> 236 - <h3 class="font-semibold text-brown-900 mb-2 truncate" x-text="recipe.name"></h3> 237 - <!-- Brewer --> 238 - <template x-if="getBrewerDisplay(recipe) !== '-'"> 239 - <p class="text-sm text-brown-600 mb-3" x-text="getBrewerDisplay(recipe)"></p> 240 - </template> 241 - <!-- Stats grid --> 242 - <div class="grid grid-cols-3 gap-2 mb-3"> 243 - <div class="text-center bg-brown-50/60 rounded-md py-1.5"> 244 - <span class="block text-[10px] text-brown-500 uppercase tracking-wide">Coffee</span> 245 - <span class="block text-sm font-medium text-brown-900" x-text="recipe.coffee_amount > 0 ? recipe.coffee_amount.toFixed(1) + 'g' : '-'"></span> 246 - </div> 247 - <div class="text-center bg-brown-50/60 rounded-md py-1.5"> 248 - <span class="block text-[10px] text-brown-500 uppercase tracking-wide">Water</span> 249 - <span class="block text-sm font-medium text-brown-900" x-text="recipe.water_amount > 0 ? recipe.water_amount.toFixed(1) + 'g' : '-'"></span> 250 - </div> 251 - <div class="text-center bg-brown-50/60 rounded-md py-1.5"> 252 - <span class="block text-[10px] text-brown-500 uppercase tracking-wide">Ratio</span> 253 - <span class="block text-sm font-medium text-brown-900" x-text="formatRatio(recipe)"></span> 254 - </div> 255 - </div> 256 - <!-- Counts row --> 257 - <template x-if="recipe.brew_count > 0 || recipe.fork_count > 0"> 258 - <div class="flex items-center gap-3 pt-2 border-t border-brown-200/60 text-xs text-brown-500"> 259 - <template x-if="recipe.brew_count > 0"> 260 - <span class="flex items-center gap-1" :title="recipe.brew_count + ' brews'"> 261 - <svg class="w-3.5 h-3.5" 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> 262 - <span x-text="recipe.brew_count + ' brew' + (recipe.brew_count !== 1 ? 's' : '')"></span> 263 - </span> 264 - </template> 265 - <template x-if="recipe.fork_count > 0"> 266 - <span class="flex items-center gap-1" :title="recipe.fork_count + ' forks'"> 267 - <svg class="w-3.5 h-3.5" 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> 268 - <span x-text="recipe.fork_count + ' fork' + (recipe.fork_count !== 1 ? 's' : '')"></span> 269 - </span> 270 - </template> 271 - <template x-if="recipe.forker_avatars && recipe.forker_avatars.length > 0"> 272 - <div class="flex -space-x-1.5 ml-auto"> 273 - <template x-for="(avatar, i) in recipe.forker_avatars.slice(0, 3)" :key="i"> 274 - <img :src="avatar" alt="" class="w-5 h-5 rounded-full object-cover border border-white" loading="lazy" width="20" height="20"/> 275 - </template> 276 - </div> 277 - </template> 278 - </div> 279 - </template> 280 - </div> 281 - </template> 282 - </div> 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> 302 - <div> 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"> 337 - <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"> 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> 339 - </svg> 340 - Share 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> 354 - </div> 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> 357 - </div> 358 - </div> 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> 378 - </div> 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> 393 - </template> 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> 418 - </template> 419 - </span> 420 - </template> 421 - </div> 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> 462 - </div> 463 - </div> 464 - </template> 465 - </div> 466 - <!-- Mobile detail panel (shown below grid on small screens) --> 174 + <!-- Selected recipe detail panel (above grid) --> 467 175 <template x-if="selectedRecipe"> 468 - <div class="lg:hidden mt-4" x-ref="mobileDetail"> 176 + <div class="mb-4" x-ref="recipeDetail"> 469 177 <div class="card card-inner"> 470 178 <div class="flex justify-between items-start mb-4"> 471 179 <div class="min-w-0 flex-1"> ··· 488 196 </div> 489 197 </a> 490 198 </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> 199 + <div class="flex items-center gap-2 shrink-0"> 200 + <!-- Actions dropdown --> 201 + <div class="relative" x-data="{ actionsOpen: false }"> 202 + <button 203 + type="button" 204 + @click="actionsOpen = !actionsOpen" 205 + @click.away="actionsOpen = false" 206 + class="action-btn" 207 + aria-label="More options" 208 + > 209 + <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"> 210 + <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> 211 + </svg> 212 + </button> 213 + <div 214 + x-show="actionsOpen" 215 + x-transition:enter="transition ease-out duration-100" 216 + x-transition:enter-start="transform opacity-0 scale-95" 217 + x-transition:enter-end="transform opacity-100 scale-100" 218 + x-transition:leave="transition ease-in duration-75" 219 + x-transition:leave-start="transform opacity-100 scale-100" 220 + x-transition:leave-end="transform opacity-0 scale-95" 221 + class="action-menu bottom-full mb-1" 222 + x-cloak 223 + > 224 + <!-- Share --> 225 + <button type="button" @click="shareRecipe(); actionsOpen = false" class="action-menu-item"> 226 + <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"> 227 + <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> 228 + </svg> 229 + Share 230 + </button> 231 + <!-- Report (only for authenticated non-owners) --> 232 + <template x-if="isAuthenticated && !isOwner(selectedRecipe)"> 233 + <div> 234 + <div class="action-menu-divider"></div> 235 + <button type="button" @click="openReport(); actionsOpen = false" class="action-menu-item"> 236 + <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" aria-hidden="true"> 237 + <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> 238 + </svg> 239 + Report 240 + </button> 241 + </div> 242 + </template> 243 + </div> 244 + </div> 245 + <button @click="selectedRecipe = null" class="text-brown-500 hover:text-brown-700 text-lg font-bold" aria-label="Close recipe details">&times;</button> 246 + </div> 492 247 </div> 493 248 <template x-if="getBrewerDisplay(selectedRecipe) !== '-'"> 494 249 <div class="mb-4"> ··· 531 286 <p class="text-sm text-brown-800 mt-1 whitespace-pre-wrap" x-text="selectedRecipe.notes"></p> 532 287 </div> 533 288 </template> 534 - <div class="flex flex-col gap-2"> 289 + <template x-if="selectedRecipe.brew_count > 0 || selectedRecipe.fork_count > 0"> 290 + <div class="flex items-center gap-4 mb-4 text-sm text-brown-600"> 291 + <template x-if="selectedRecipe.brew_count > 0"> 292 + <span class="flex items-center gap-1.5"> 293 + <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> 294 + <span x-text="selectedRecipe.brew_count + ' brew' + (selectedRecipe.brew_count !== 1 ? 's' : '')"></span> 295 + </span> 296 + </template> 297 + <template x-if="selectedRecipe.fork_count > 0"> 298 + <span class="flex items-center gap-1.5"> 299 + <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> 300 + <span x-text="selectedRecipe.fork_count + ' fork' + (selectedRecipe.fork_count !== 1 ? 's' : '')"></span> 301 + <template x-if="selectedRecipe.forker_avatars && selectedRecipe.forker_avatars.length > 0"> 302 + <div class="flex -space-x-1.5 ml-1"> 303 + <template x-for="(avatar, i) in selectedRecipe.forker_avatars.slice(0, 5)" :key="i"> 304 + <img :src="avatar" alt="" class="w-5 h-5 rounded-full object-cover border border-white" loading="lazy" width="20" height="20"/> 305 + </template> 306 + </div> 307 + </template> 308 + </span> 309 + </template> 310 + </div> 311 + </template> 312 + <template x-if="selectedRecipe.source_ref"> 313 + <p class="text-sm text-brown-500 mb-3"> 314 + Forked from&#32; 315 + <template x-if="selectedRecipe.source_author_display || selectedRecipe.source_author_handle"> 316 + <a 317 + :href="getSourceRecipeURL(selectedRecipe)" 318 + class="text-brown-700 underline hover:text-brown-900" 319 + @click.stop 320 + x-text="(selectedRecipe.source_author_display || selectedRecipe.source_author_handle) + '\'s recipe'" 321 + ></a> 322 + </template> 323 + <template x-if="!selectedRecipe.source_author_display && !selectedRecipe.source_author_handle"> 324 + <span>another recipe</span> 325 + </template> 326 + </p> 327 + </template> 328 + <div class="flex flex-col sm:flex-row gap-2 sm:gap-3"> 535 329 <a 536 330 :href="'/brews/new?recipe=' + selectedRecipe.rkey + '&recipe_owner=' + (selectedRecipe.author_did || '')" 537 331 class="btn-primary text-sm text-center" ··· 557 351 </div> 558 352 </div> 559 353 </template> 354 + <!-- Recipe grid --> 355 + <div> 356 + <template x-if="loading"> 357 + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> 358 + <div class="feed-card animate-pulse"> 359 + <div class="h-4 bg-brown-200 rounded w-1/3 mb-3"></div> 360 + <div class="h-5 bg-brown-200 rounded w-2/3 mb-2"></div> 361 + <div class="h-4 bg-brown-200 rounded w-1/2 mb-3"></div> 362 + <div class="grid grid-cols-3 gap-2"> 363 + <div class="h-12 bg-brown-100 rounded"></div> 364 + <div class="h-12 bg-brown-100 rounded"></div> 365 + <div class="h-12 bg-brown-100 rounded"></div> 366 + </div> 367 + </div> 368 + <div class="feed-card animate-pulse hidden sm:block"> 369 + <div class="h-4 bg-brown-200 rounded w-1/3 mb-3"></div> 370 + <div class="h-5 bg-brown-200 rounded w-2/3 mb-2"></div> 371 + <div class="h-4 bg-brown-200 rounded w-1/2 mb-3"></div> 372 + <div class="grid grid-cols-3 gap-2"> 373 + <div class="h-12 bg-brown-100 rounded"></div> 374 + <div class="h-12 bg-brown-100 rounded"></div> 375 + <div class="h-12 bg-brown-100 rounded"></div> 376 + </div> 377 + </div> 378 + <div class="feed-card animate-pulse hidden lg:block"> 379 + <div class="h-4 bg-brown-200 rounded w-1/3 mb-3"></div> 380 + <div class="h-5 bg-brown-200 rounded w-2/3 mb-2"></div> 381 + <div class="h-4 bg-brown-200 rounded w-1/2 mb-3"></div> 382 + <div class="grid grid-cols-3 gap-2"> 383 + <div class="h-12 bg-brown-100 rounded"></div> 384 + <div class="h-12 bg-brown-100 rounded"></div> 385 + <div class="h-12 bg-brown-100 rounded"></div> 386 + </div> 387 + </div> 388 + </div> 389 + </template> 390 + <template x-if="!loading && recipes.length === 0"> 391 + <div class="card card-inner text-center py-8"> 392 + <p class="text-brown-700 text-lg font-medium">No recipes found</p> 393 + <p class="text-sm text-brown-600 mt-2">Try adjusting your filters or search terms</p> 394 + </div> 395 + </template> 396 + <template x-if="!loading && recipes.length > 0"> 397 + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> 398 + <template x-for="recipe in recipes" :key="recipe.rkey"> 399 + <div 400 + class="feed-card feed-card-recipe cursor-pointer transition-shadow" 401 + :class="selectedRecipe && selectedRecipe.rkey === recipe.rkey ? 'ring-2 ring-brown-400' : ''" 402 + @click="selectRecipe(recipe)" 403 + > 404 + <!-- Author row --> 405 + <a 406 + :href="'/profile/' + recipe.author_did" 407 + class="flex items-center gap-2 mb-3 group/author" 408 + @click.stop 409 + > 410 + <template x-if="recipe.author_avatar"> 411 + <img :src="recipe.author_avatar" class="w-7 h-7 rounded-full object-cover" :alt="recipe.author_display || recipe.author_handle || ''" loading="lazy" width="28" height="28"/> 412 + </template> 413 + <template x-if="!recipe.author_avatar"> 414 + <div class="w-7 h-7 rounded-full bg-brown-200 flex items-center justify-center text-brown-600 text-xs font-bold" x-text="(recipe.author_display || recipe.author_handle || '?')[0].toUpperCase()"></div> 415 + </template> 416 + <div class="min-w-0 flex-1"> 417 + <template x-if="recipe.author_display"> 418 + <span class="block truncate text-sm font-medium text-brown-700 group-hover/author:text-brown-900 group-hover/author:underline transition-colors" x-text="recipe.author_display"></span> 419 + </template> 420 + <span class="block truncate text-xs text-brown-600 group-hover/author:text-brown-800 transition-colors" x-text="recipe.author_handle || ''"></span> 421 + </div> 422 + </a> 423 + <!-- Recipe name --> 424 + <h3 class="font-semibold text-brown-900 mb-2 truncate" x-text="recipe.name"></h3> 425 + <!-- Brewer --> 426 + <template x-if="getBrewerDisplay(recipe) !== '-'"> 427 + <p class="text-sm text-brown-600 mb-3" x-text="getBrewerDisplay(recipe)"></p> 428 + </template> 429 + <!-- Stats grid --> 430 + <div class="grid grid-cols-3 gap-2 mb-3"> 431 + <div class="text-center bg-brown-50/60 rounded-md py-1.5"> 432 + <span class="block text-[10px] text-brown-500 uppercase tracking-wide">Coffee</span> 433 + <span class="block text-sm font-medium text-brown-900" x-text="recipe.coffee_amount > 0 ? recipe.coffee_amount.toFixed(1) + 'g' : '-'"></span> 434 + </div> 435 + <div class="text-center bg-brown-50/60 rounded-md py-1.5"> 436 + <span class="block text-[10px] text-brown-500 uppercase tracking-wide">Water</span> 437 + <span class="block text-sm font-medium text-brown-900" x-text="recipe.water_amount > 0 ? recipe.water_amount.toFixed(1) + 'g' : '-'"></span> 438 + </div> 439 + <div class="text-center bg-brown-50/60 rounded-md py-1.5"> 440 + <span class="block text-[10px] text-brown-500 uppercase tracking-wide">Ratio</span> 441 + <span class="block text-sm font-medium text-brown-900" x-text="formatRatio(recipe)"></span> 442 + </div> 443 + </div> 444 + <!-- Counts row --> 445 + <template x-if="recipe.brew_count > 0 || recipe.fork_count > 0"> 446 + <div class="flex items-center gap-3 pt-2 border-t border-brown-200/60 text-xs text-brown-500"> 447 + <template x-if="recipe.brew_count > 0"> 448 + <span class="flex items-center gap-1" :title="recipe.brew_count + ' brews'"> 449 + <svg class="w-3.5 h-3.5" 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> 450 + <span x-text="recipe.brew_count + ' brew' + (recipe.brew_count !== 1 ? 's' : '')"></span> 451 + </span> 452 + </template> 453 + <template x-if="recipe.fork_count > 0"> 454 + <span class="flex items-center gap-1" :title="recipe.fork_count + ' forks'"> 455 + <svg class="w-3.5 h-3.5" 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> 456 + <span x-text="recipe.fork_count + ' fork' + (recipe.fork_count !== 1 ? 's' : '')"></span> 457 + </span> 458 + </template> 459 + <template x-if="recipe.forker_avatars && recipe.forker_avatars.length > 0"> 460 + <div class="flex -space-x-1.5 ml-auto"> 461 + <template x-for="(avatar, i) in recipe.forker_avatars.slice(0, 3)" :key="i"> 462 + <img :src="avatar" alt="" class="w-5 h-5 rounded-full object-cover border border-white" loading="lazy" width="20" height="20"/> 463 + </template> 464 + </div> 465 + </template> 466 + </div> 467 + </template> 468 + </div> 469 + </template> 470 + </div> 471 + </template> 472 + </div> 560 473 <!-- Report dialog --> 561 474 <dialog id="recipe-report-modal" class="modal-dialog" x-data="{ reason: '', charCount: 0, submitting: false, error: '', success: false }"> 562 475 <div class="modal-content">
+2 -3
static/js/recipe-explore.js
··· 58 58 59 59 selectRecipe(recipe) { 60 60 this.selectedRecipe = recipe; 61 - // On mobile (no side panel), scroll to the detail panel 62 61 this.$nextTick(() => { 63 - if (window.innerWidth < 1024 && this.$refs.mobileDetail) { 64 - this.$refs.mobileDetail.scrollIntoView({ behavior: "smooth", block: "start" }); 62 + if (this.$refs.recipeDetail) { 63 + this.$refs.recipeDetail.scrollIntoView({ behavior: "smooth", block: "start" }); 65 64 } 66 65 }); 67 66 },

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
1 commit
expand
feat: slightly improve experience of clicking recipe on explore page
expand 0 comments