- 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.
internal/handlers/recipe.go
internal/handlers/recipe.go
This file has not been changed.
+209
-296
internal/web/pages/recipe_explore.templ
+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">×</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">·</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 
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">×</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">×</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 
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
+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
pdewey.com
submitted
#1
1 commit
expand
collapse
feat: slightly improve experience of clicking recipe on explore page
- 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.
merge conflicts detected
expand
collapse
expand
collapse
expand 0 comments
pdewey.com
submitted
#0
1 commit
expand
collapse
feat: slightly improve experience of clicking recipe on explore page
- 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.