Skip to main content
The platform provides three distinct gallery surfaces: the /media wallpaper download page, the /latest-events events feed with inline photo albums, and the homepage video section. All images are served through Cloudflare Images; videos use either Cloudflare Stream or YouTube embeds.

CDN helpers

All image URLs are constructed using helpers in lib/cdn-assets.ts. Never hard-code image delivery URLs directly.
lib/cdn-assets.ts
const CLOUDFLARE_IMAGES_BASE = 'https://imagedelivery.net/vdFY6FzpM3Q9zi31qlYmGA/'

// Standard quality — used for most gallery images
export const getCloudflareImage = (imageId: string) =>
  `${CLOUDFLARE_IMAGES_BASE}${imageId}/bigger?format=auto&quality=90`

// Mobile wallpaper variant — 9:19.5 portrait aspect ratio
export const getCloudflareImageMobileWp = (imageId: string) =>
  `${CLOUDFLARE_IMAGES_BASE}${imageId}/mobileWP?format=auto&quality=90`

// Highest quality — used where maximum fidelity is required
export const getCloudflareImageBiggest = (imageId: string) =>
  `${CLOUDFLARE_IMAGES_BASE}${imageId}/biggest?format=auto&quality=100`

bigger

Standard gallery variant. format=auto serves WebP or AVIF where supported. Quality 90.

mobileWP

Portrait crop optimized for phone wallpapers. Rendered at aspect-[9/19.5]. Quality 90.

biggest

Full-resolution delivery. Used for archival or print-quality contexts. Quality 100.

R2 static assets

Logos and icons come from Cloudflare R2 at https://cdn.njrajatmahotsav.com. Use CDN_ASSETS constants or getR2Image(filename).

ImageCarouselModal

components/organisms/image-carousel-modal.tsx — a full-screen lightbox carousel used wherever a list of image URLs needs to be browsed. Props:
PropTypeDescription
imagesstring[]Array of image URLs
titlestringUsed for the accessible dialog title and alt text
openbooleanControls dialog visibility
onOpenChange(open: boolean) => voidCalled when the dialog should close
Interactions:
  • Left/right arrow buttons navigate between images
  • Dot indicators at the bottom jump to a specific image
  • Touch drag is supported via Framer Motion’s drag="x" with a swipe power threshold of 10000
const swipePower = (offset: number, velocity: number) =>
  Math.abs(offset) * velocity

// Swipe left (next) or right (previous) when power exceeds threshold
if (swipe < -swipeConfidenceThreshold) paginate(1)
if (swipe >  swipeConfidenceThreshold) paginate(-1)
  • Images use object-contain inside a aspect-video container with an orange-to-red gradient background
  • The active dot expands to w-8; inactive dots are w-2 bg-white/50
The dialog title is visually hidden (className="sr-only") but present for screen readers.

PhotoAlbumModal

components/organisms/photo-album-modal.tsx — a richer lightbox used on the /latest-events page. It extends the carousel pattern with a thumbnail strip and keyboard navigation. Additional features over ImageCarouselModal:
  • Thumbnail strip — a horizontally scrollable row of 64×64px thumbnails below the main image. The active thumbnail scales up and gains an border-orange-500 border.
  • Counter badge — top-left overlay showing {currentIndex + 1} / {photos.length}
  • Keyboard navigationArrowLeft, ArrowRight, and Escape keys are handled via a window keydown listener
  • initialIndex prop — opens the modal at a specific photo, useful when clicking a thumbnail in a grid
  • Reset on openuseEffect resets currentIndex to initialIndex whenever open changes to true
Props:
PropTypeDefaultDescription
photosEventPhoto[]Array of { url, caption? } objects
titlestringDialog title for accessibility
openbooleanControls visibility
onOpenChange(open: boolean) => voidClose callback
initialIndexnumber0Photo index to open at

ResponsiveImageGallery

components/organisms/responsive-image-gallery.tsx — a fixed three-image layout that switches between a CSS grid on desktop and a swipeable carousel on mobile. Props:
PropTypeDescription
images[ImageData, ImageData, ImageData]Exactly three images (tuple)
interface ImageData {
  id: number
  src: string
  alt: string
}
A grid-cols-3 gap-0 h-[50vh] grid. Each image uses object-cover and scales up 5% on hover (group-hover:scale-105). Items animate in with staggered whileInView delays (0s, 0.2s, 0.4s).

Video components

AashirwadVideoPlayer

components/organisms/aashirwad-video-player.tsx — a standalone full-screen section that plays the main Rajat Mahotsav announcement video hosted on Cloudflare Stream.
  • The section has a dark radial gradient background (from-slate-950 via-blue-950 to-slate-900)
  • A Gujarati heading સુવર્ણ યુગ નો રજત મહોત્સવ and English subtitle A Silver Celebration of a Golden Era appear above the player
  • On initial render a click-to-play poster is shown using the Cloudflare Stream animated GIF thumbnail:
    https://customer-kss5h1dwt4mkz0x3.cloudflarestream.com/
      6f4c127cc7b339c9b1b7875c1dc8e745/
      thumbnails/thumbnail.gif?time=95s&duration=4s
    
  • Clicking the play button sets videoLoaded = true and replaces the poster with a Cloudflare Stream <iframe> with autoplay=true
  • The iframe uses padding-top: 56.42633228840125% for exact aspect ratio preservation

VideoSection

components/organisms/video-section.tsx — a YouTube video carousel section used on the main landing page. Featured videos:
Video IDTitle
6rvGcN4wQCURajat Mahotsav Trailer #1
d0vT6cSVeCYRajat Mahotsav Trailer #2
Jq_mvCRivaESecaucus Temple Drone Footage
Layout:
A grid-cols-3 layout renders all three VideoCard components simultaneously. Only one video can be playing at a time — playingVideoIndex state tracks which card is active.
VideoCard behavior:
  • Before play: shows YouTube maxresdefault.jpg thumbnail with an animated play icon
  • On click: pauses the site background audio via useAudioContext().pause(), then renders the YouTube embed with autoplay=1
  • The play icon pulses (scale: [1, 1.05, 1], opacity: [0.7, 0.9, 0.7]) on a 2.5–3s loop and rotates 360° on hover
The full YouTube playlist is linked from the section: https://www.youtube.com/playlist?list=PLqKpGEY54C-1OklTqKwLh6JWYCDQpn5MC

VideoBackgroundSection

components/organisms/video-background-section.tsx — a hero-style fullscreen video background using Vimeo embeds.
  • Detects device type via useDeviceType() and loads different Vimeo video IDs for mobile vs desktop
  • Desktop: Vimeo 1128421609, Mobile: Vimeo 1128421563
  • Both use autoplay=1&muted=1&loop=1&background=1 for a silent looping background
  • Top and bottom gradient overlays (from-slate-900) fade the video into the surrounding page sections
  • The iframe is centered and sized to always fill the viewport: width: 177.78vh, height: 100vh

Media page — downloadable wallpapers

The /media page (app/media/page.tsx) lets visitors download five exclusive mobile wallpapers. Available wallpapers:
IDTitleVariant
1GM Wallpaper 1mobileWP
2GM Wallpaper 2mobileWP
3GM Wallpaper 3mobileWP
4Prathna Wallpaper 1mobileWP
5Pebbled Wallpaper 1mobileWP
Full-resolution files are served from Cloudflare R2:
https://cdn.njrajatmahotsav.com/wallpapers/rajat_mobile_wallpaper_gm_1.jpeg
The download is triggered via the /api/download route to force a Content-Disposition: attachment header, preventing the browser from opening the image inline:
const handleDownload = (wallpaper: typeof wallpapers[0]) => {
  const filename = `rajat-mahotsav-${wallpaper.title.toLowerCase().replace(/\s+/g, "-")}.jpg`
  const downloadUrl = `/api/download?url=${encodeURIComponent(wallpaper.fullRes)}&filename=${encodeURIComponent(filename)}`
  const link = document.createElement("a")
  link.href = downloadUrl
  link.download = filename
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
}

Latest events feed

The /latest-events page (app/latest-events/page.tsx) displays community events from lib/events-data.ts with filtering and photo browsing. EventData structure:
lib/events-data.ts
interface EventData {
  id: string
  date: string          // ISO format: "2025-03-15"
  title: string
  description: string
  tags: string[]        // "community seva" | "religious" | "cultural" | "youth event"
  youtubeVideoId?: string
  instagramUrl?: string
  photos: EventPhoto[]
}

interface EventPhoto {
  url: string
  caption?: string
}
Filtering:
  • Tag filter — OR logic: an event is shown if it matches any selected tag
  • Date range filter — inclusive start and end dates
  • URL parameter ?tags=community+seva,religious pre-selects tags on load
  • Events are sorted newest-first
Tag color palette:
TagBackgroundText
community sevabg-orange-100text-orange-700
religiousbg-red-100text-red-700
culturalbg-orange-100text-orange-800
youth eventbg-amber-100text-amber-800
Clicking a photo thumbnail on an event card calls handlePhotoClick(photos, eventTitle), which sets selectedPhotos, selectedEventTitle, and opens PhotoAlbumModal.From within the EventDetailsModal (the slide-up detail sheet), clicking a photo calls handlePhotoClickFromDetails(photoIndex), which also sets photoModalInitialIndex so the album opens at the correct photo.
Add a new object to the eventsData array in lib/events-data.ts. Use getCloudflareImage(imageId) for photo URLs. The id field must be a unique string. Events are sorted by date at runtime so insertion order does not matter.

GuruCarousel

components/organisms/guru-carousel.tsx — an Embla Carousel that displays Guru portrait cards, used in sections that highlight the Guruparampara lineage.
  • Auto-advances every 4000ms when the carousel is visible (tracked via IntersectionObserver at threshold: 0.5)
  • Auto-advance resets after a manual swipe or dot click (resetKey state)
  • Dot indicators use orange (bg-orange-500) for the active slide and gray (bg-gray-300) otherwise
  • Each slide renders a GuruCard molecule with an imageId and name
interface Guru {
  imageId: string   // Cloudflare Images image ID
  name: string
}

Build docs developers (and LLMs) love