|
368 | 368 | (e) => e.title.toLowerCase().replaceAll(' ', '-') == event.getMatchingTags("t").filter((t) => t[1].slice(13)[0])[0][1].slice(13) |
369 | 369 | );*/ |
370 | 370 |
|
| 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 | +
|
371 | 393 | // Deduplicate image tags by URL, use placeholder if no images or all images are empty |
372 | 394 | $: uniqueImages = (() => { |
373 | 395 | const images = event.tags |
|
490 | 512 | </div> |
491 | 513 | </div> |
492 | 514 | </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) --> |
497 | 549 | <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" |
505 | 554 | > |
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> |
511 | 558 | </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} |
532 | 594 | <!-- Reactions and actions --> |
533 | 595 | <div class="flex flex-col gap-1 print:hidden -mt-2"> |
534 | 596 | <RecipeReactionPills {event} /> |
|
923 | 985 | :global(.prose tbody tr) { |
924 | 986 | border-bottom-color: var(--color-input-border); |
925 | 987 | } |
| 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 | + } |
926 | 1001 | </style> |
0 commit comments