Skip to content

Commit da8388e

Browse files
spe1020claude
authored andcommitted
Add recipe image carousel and zapstore metadata
- Recipe images now display in a horizontal carousel with smooth scrolling - Added navigation arrows (desktop), dot indicators (≤5 images), and slide counter - Matches community feed carousel styling for consistency - Added zapstore.yaml metadata: repository, app name, description, homepage, and MIT license Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 998ced7 commit da8388e

File tree

2 files changed

+127
-36
lines changed

2 files changed

+127
-36
lines changed

src/components/Recipe/Recipe.svelte

Lines changed: 111 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,28 @@
368368
(e) => e.title.toLowerCase().replaceAll(' ', '-') == event.getMatchingTags("t").filter((t) => t[1].slice(13)[0])[0][1].slice(13)
369369
);*/
370370
371+
// Carousel: track element and current index for dots/arrows
372+
let carouselEl: HTMLDivElement;
373+
let carouselIndex = 0;
374+
function updateCarouselIndex() {
375+
if (!carouselEl || uniqueImages.length <= 1) return;
376+
const width = carouselEl.clientWidth;
377+
if (width <= 0) return;
378+
const index = Math.round(carouselEl.scrollLeft / width);
379+
const clamped = Math.min(index, uniqueImages.length - 1);
380+
if (clamped !== carouselIndex) carouselIndex = clamped;
381+
}
382+
function carouselPrev() {
383+
if (!carouselEl || uniqueImages.length <= 1) return;
384+
const width = carouselEl.clientWidth;
385+
carouselEl.scrollBy({ left: -width, behavior: 'smooth' });
386+
}
387+
function carouselNext() {
388+
if (!carouselEl || uniqueImages.length <= 1) return;
389+
const width = carouselEl.clientWidth;
390+
carouselEl.scrollBy({ left: width, behavior: 'smooth' });
391+
}
392+
371393
// Deduplicate image tags by URL, use placeholder if no images or all images are empty
372394
$: uniqueImages = (() => {
373395
const images = event.tags
@@ -490,45 +512,85 @@
490512
</div>
491513
</div>
492514
</div>
493-
{#each uniqueImages as image, i}
494-
{#if i === 0}
495-
<!-- First image - clickable to open modal -->
496-
<div class="rounded-3xl overflow-hidden">
515+
<!-- Image carousel: same style as community feed for consistency -->
516+
{#if uniqueImages.length > 0}
517+
<div class="recipe-carousel-wrapper relative rounded-3xl overflow-hidden">
518+
<div
519+
bind:this={carouselEl}
520+
on:scroll={updateCarouselIndex}
521+
class="recipe-image-carousel flex overflow-x-auto overflow-y-hidden snap-x snap-mandatory -mx-1 scrollbar-hide"
522+
style="touch-action: pan-y pan-x; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch;"
523+
role="region"
524+
aria-label="Recipe images"
525+
>
526+
{#each uniqueImages as image, i}
527+
<div class="recipe-carousel-slide flex-shrink-0 w-full min-w-full snap-center flex items-center justify-center">
528+
<button
529+
on:click={() =>
530+
openImageModal(
531+
image[1],
532+
uniqueImages.map((img) => img[1]),
533+
i
534+
)}
535+
class="block w-full cursor-pointer rounded-3xl overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
536+
type="button"
537+
>
538+
<img
539+
class="rounded-3xl aspect-video object-cover w-full"
540+
src={image[1]}
541+
alt="Recipe image {i + 1}"
542+
/>
543+
</button>
544+
</div>
545+
{/each}
546+
</div>
547+
{#if uniqueImages.length > 1}
548+
<!-- Arrows: hidden on mobile, visible on sm+ (match community feed) -->
497549
<button
498-
on:click={() =>
499-
openImageModal(
500-
image[1],
501-
uniqueImages.map((img) => img[1]),
502-
i
503-
)}
504-
class="w-full cursor-pointer"
550+
type="button"
551+
on:click={(e) => { e.stopPropagation(); carouselPrev(); }}
552+
class="hidden sm:block absolute left-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full p-2 transition-colors z-10"
553+
aria-label="Previous image"
505554
>
506-
<img
507-
class="rounded-3xl aspect-video object-cover w-full"
508-
src={image[1]}
509-
alt="Recipe image {i + 1}"
510-
/>
555+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
556+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
557+
</svg>
511558
</button>
512-
</div>
513-
{:else}
514-
<!-- Other images - clickable to open modal -->
515-
<button
516-
on:click={() =>
517-
openImageModal(
518-
image[1],
519-
uniqueImages.map((img) => img[1]),
520-
i
521-
)}
522-
class="w-full cursor-pointer"
523-
>
524-
<img
525-
class="rounded-3xl aspect-video object-cover w-full"
526-
src={image[1]}
527-
alt="Recipe image {i + 1}"
528-
/>
529-
</button>
530-
{/if}
531-
{/each}
559+
<button
560+
type="button"
561+
on:click={(e) => { e.stopPropagation(); carouselNext(); }}
562+
class="hidden sm:block absolute right-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full p-2 transition-colors z-10"
563+
aria-label="Next image"
564+
>
565+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
566+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
567+
</svg>
568+
</button>
569+
<!-- Slide counter (match community feed) -->
570+
<div class="absolute top-2 right-2 bg-black/50 text-white text-xs px-2 py-1 rounded z-10">
571+
{carouselIndex + 1} / {uniqueImages.length}
572+
</div>
573+
<!-- Dot indicators: only when ≤5 images (match community feed) -->
574+
{#if uniqueImages.length <= 5}
575+
<div class="absolute bottom-2 left-1/2 -translate-x-1/2 flex space-x-1.5 z-10" aria-hidden="true">
576+
{#each uniqueImages as _, i}
577+
<button
578+
type="button"
579+
class="w-2 h-2 rounded-full transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-1 focus-visible:ring-offset-transparent {carouselIndex === i ? 'bg-white' : 'bg-white/50'}"
580+
on:click={(e) => {
581+
e.stopPropagation();
582+
if (carouselEl) {
583+
carouselEl.scrollTo({ left: carouselEl.clientWidth * i, behavior: 'smooth' });
584+
}
585+
}}
586+
aria-label="Go to image {i + 1}"
587+
/>
588+
{/each}
589+
</div>
590+
{/if}
591+
{/if}
592+
</div>
593+
{/if}
532594
<!-- Reactions and actions -->
533595
<div class="flex flex-col gap-1 print:hidden -mt-2">
534596
<RecipeReactionPills {event} />
@@ -923,4 +985,17 @@
923985
:global(.prose tbody tr) {
924986
border-bottom-color: var(--color-input-border);
925987
}
988+
989+
/* Recipe image carousel: horizontal scroll, one image at a time */
990+
.recipe-image-carousel {
991+
scroll-behavior: smooth;
992+
-webkit-overflow-scrolling: touch;
993+
}
994+
.recipe-image-carousel::-webkit-scrollbar {
995+
display: none;
996+
}
997+
.recipe-carousel-slide {
998+
scroll-snap-align: start;
999+
scroll-snap-stop: always;
1000+
}
9261001
</style>

zapstore.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,18 @@
1+
repository: https://github.com/zapcooking/frontend
2+
3+
name: Zap Cooking
4+
summary: Nostr-native recipes, culture, and Bitcoin zaps
5+
6+
description: >
7+
Zap Cooking is a Nostr-native food culture platform.
8+
Discover and share recipes, publish food and culture writing,
9+
and zap creators directly without algorithms or gatekeepers.
10+
11+
The app includes a built-in self-custodial Bitcoin wallet
12+
with Lightning and on-chain support, designed for long-term ownership.
13+
14+
homepage: https://zap.cooking
15+
license: MIT
16+
117
assets:
218
- android/app/release/.*\.apk

0 commit comments

Comments
 (0)