diff --git a/public/assets/images/ekosistem/paon/animate/burger_pecah.png b/public/assets/images/ekosistem/paon/animate/burger_pecah.png new file mode 100644 index 0000000..488a726 Binary files /dev/null and b/public/assets/images/ekosistem/paon/animate/burger_pecah.png differ diff --git a/public/assets/images/ekosistem/paon/animate/burger_pecah_transparent.png b/public/assets/images/ekosistem/paon/animate/burger_pecah_transparent.png new file mode 100644 index 0000000..d2ed065 Binary files /dev/null and b/public/assets/images/ekosistem/paon/animate/burger_pecah_transparent.png differ diff --git a/public/assets/images/ekosistem/paon/animate/burger_utuh.png b/public/assets/images/ekosistem/paon/animate/burger_utuh.png new file mode 100644 index 0000000..f74ef4f Binary files /dev/null and b/public/assets/images/ekosistem/paon/animate/burger_utuh.png differ diff --git a/public/assets/images/ekosistem/paon/animate/burger_utuh_transparent.png b/public/assets/images/ekosistem/paon/animate/burger_utuh_transparent.png new file mode 100644 index 0000000..aefaeba Binary files /dev/null and b/public/assets/images/ekosistem/paon/animate/burger_utuh_transparent.png differ diff --git a/src/App.tsx b/src/App.tsx index bb02624..69d68fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import PageTransition from './components/PageTransition'; import Home from './pages/Home'; import BhumiAnindha from './pages/BhumiAnindha'; import PAON from './pages/PAON'; +import PaonMenu from './pages/PaonMenu'; import KaIND from './pages/KaIND'; import CittaRahayu from './pages/CittaRahayu'; import PasarBahagia from './pages/PasarBahagia'; @@ -59,6 +60,7 @@ function AppContent() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/BurgerReveal.tsx b/src/components/BurgerReveal.tsx new file mode 100644 index 0000000..eebcdf0 --- /dev/null +++ b/src/components/BurgerReveal.tsx @@ -0,0 +1,226 @@ +import { useEffect, useRef, useState } from 'react'; +import { motion, useAnimationControls, useInView, useReducedMotion } from 'framer-motion'; + +const WHOLE_IMG = '/assets/images/ekosistem/paon/animate/burger_utuh_transparent.png'; +const EXPLODED_IMG = '/assets/images/ekosistem/paon/animate/burger_pecah_transparent.png'; + +const EASE_SETTLE = [0.34, 1.56, 0.64, 1] as const; + +/** Urutan bahan dari alas (1) sampai penutup (12), sesuai susunan burger sungguhan. */ +const INGREDIENTS = [ + 'Pumpkin Bun', + 'Tomato Concasse', + 'Mango Chutney', + 'Iceberg Lettuce', + 'Patty', + 'Cheddar Cheese', + 'Patty', + 'Cheddar Cheese', + 'Mayo Nori', + 'Coconut Sauce', + 'Pesto Basil', + 'Pumpkin Bun', +]; + +function WholeBurger({ play, onSpunAway }: { play: boolean; onSpunAway: () => void }) { + const controls = useAnimationControls(); + const prefersReduced = useReducedMotion(); + + useEffect(() => { + if (!play) return; + let cancelled = false; + + async function sequence() { + if (prefersReduced) { + onSpunAway(); + return; + } + // Sedikit "hidup" dulu — melayang pelan, mengundang perhatian. + await controls.start({ y: [0, -10, 0], transition: { duration: 0.9, ease: 'easeInOut' } }); + if (cancelled) return; + // Ancang-ancang — sedikit menciut sebelum berputar. + await controls.start({ scale: 0.94, transition: { duration: 0.16, ease: 'easeIn' } }); + if (cancelled) return; + // Berputar pergi. + await controls.start({ rotateY: 100, scale: 0.76, transition: { duration: 0.45, ease: [0.55, 0, 0.85, 0.2] } }); + if (cancelled) return; + onSpunAway(); + } + sequence(); + return () => { cancelled = true; }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [play]); + + return ( + + ); +} + +function ExplodedBurger({ onSettled }: { onSettled: () => void }) { + const controls = useAnimationControls(); + const prefersReduced = useReducedMotion(); + + useEffect(() => { + let cancelled = false; + + async function sequence() { + if (prefersReduced) { + onSettled(); + return; + } + // Berputar masuk dengan sedikit overshoot supaya terasa "mendarat". + await controls.start({ rotateY: 0, scale: 1, transition: { duration: 0.6, ease: EASE_SETTLE } }); + if (cancelled) return; + await new Promise(r => setTimeout(r, 380)); + if (cancelled) return; + onSettled(); + } + sequence(); + return () => { cancelled = true; }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + ); +} + +interface BurgerRevealProps { + color: string; +} + +export default function BurgerReveal({ color }: BurgerRevealProps) { + const ref = useRef(null); + const inView = useInView(ref, { once: true, margin: '-120px' }); + const [exploded, setExploded] = useState(false); + const [labelsShown, setLabelsShown] = useState(false); + + const items = INGREDIENTS.map((name, i) => ({ + name, + number: i + 1, + top: ((11.5 - i) / 12) * 100, + side: (i % 2 === 0 ? 'left' : 'right') as 'left' | 'right', + })); + const leftItems = items.filter(it => it.side === 'left'); + const rightItems = items.filter(it => it.side === 'right'); + + function delayFor(number: number) { + // Sedikit tidak beraturan (bukan interval seragam) supaya terasa lebih organik. + return 0.08 + number * 0.085 + (number % 3 === 0 ? 0.05 : 0); + } + + return ( +
+ {/* Desktop: gambar di tengah, label bahan melayang di kiri-kanan */} +
+
+ {leftItems.map(it => ( +
+ {it.name} + + + {it.number} + +
+ ))} +
+ +
+ {!exploded ? ( + setExploded(true)} /> + ) : ( + setLabelsShown(true)} /> + )} +
+ +
+ {rightItems.map(it => ( +
+ + {it.number} + + + {it.name} +
+ ))} +
+
+ + {/* Mobile: gambar di atas, daftar bahan bertingkat di bawah */} +
+
+ Burger PAON +
+
    + {items.map(it => ( +
  1. + {it.number} + {it.name} +
  2. + ))} +
+
+
+ ); +} diff --git a/src/components/ModulePage.tsx b/src/components/ModulePage.tsx index 52d12e9..1646367 100644 --- a/src/components/ModulePage.tsx +++ b/src/components/ModulePage.tsx @@ -1,13 +1,16 @@ import { Link } from 'react-router-dom'; import { ArrowRight, ChevronRight } from 'lucide-react'; +import type { ReactNode } from 'react'; import type { Module } from '../data/modules'; import EcosystemIcon from './EcosystemIcons'; interface Props { module: Module; + /** Konten tambahan disisipkan tepat setelah section "Tentang" dan sebelum "Program & Layanan". */ + afterAbout?: ReactNode; } -export default function ModulePage({ module: m }: Props) { +export default function ModulePage({ module: m, afterAbout }: Props) { return ( <> {/* Hero */} @@ -61,6 +64,8 @@ export default function ModulePage({ module: m }: Props) { + {afterAbout} + {/* Features */}
diff --git a/src/index.css b/src/index.css index f7ef5f1..3ee4f9c 100644 --- a/src/index.css +++ b/src/index.css @@ -838,6 +838,71 @@ html { scroll-behavior: smooth; scroll-padding-top: var(--nav-h); } .delay-2 { animation-delay: .2s; } .delay-3 { animation-delay: .3s; } +/* ─── Burger Reveal (PAON) ───────────────────────────────── */ +.burger-reveal { + display: flex; + align-items: stretch; + justify-content: center; + max-width: 980px; + margin: 0 auto; +} +.burger-reveal__col { position: relative; flex: 1 1 0; min-width: 0; } +.burger-reveal__stage { + position: relative; + flex: 0 0 auto; + width: min(320px, 30vw); + aspect-ratio: 761 / 1395; + perspective: 1400px; +} +.burger-reveal__img { + position: absolute; inset: 0; + width: 100%; height: 100%; + object-fit: contain; + filter: drop-shadow(0 22px 28px rgba(30,18,22,.22)); + backface-visibility: hidden; + transform-style: preserve-3d; +} +.burger-reveal__callout { + position: absolute; left: 0; right: 0; + display: flex; align-items: center; gap: 8px; + transition: opacity .5s var(--ease-organic), transform .5s var(--ease-organic); +} +.burger-reveal__callout--left { justify-content: flex-end; } +.burger-reveal__callout--right { justify-content: flex-start; } +.burger-reveal__num { + flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%; + display: flex; align-items: center; justify-content: center; + color: #fff; font-family: var(--font-mono); font-size: .68rem; font-weight: 600; + transition: transform .35s var(--ease-bounce); +} +.burger-reveal__line { + height: 1.5px; width: 0; background: var(--earth-200); flex-shrink: 0; + transition: width .3s var(--ease-organic); +} +.burger-reveal__text { font-size: .82rem; font-weight: 600; color: var(--text-700); white-space: nowrap; } + +.burger-reveal-mobile { display: none; } + +@media (max-width: 980px) { + .burger-reveal { display: none; } + .burger-reveal-mobile { display: block; } + .burger-reveal-mobile__stage { + position: relative; + max-width: 240px; margin: 0 auto 28px; + aspect-ratio: 761 / 1395; + } + .burger-reveal-mobile__stage .burger-reveal__img { position: static; } + .burger-reveal-mobile__list { + display: grid; grid-template-columns: 1fr 1fr; gap: 10px 20px; + max-width: 520px; margin: 0 auto; list-style: none; + } + .burger-reveal-mobile__list li { + display: flex; align-items: center; gap: 8px; + font-size: .82rem; font-weight: 600; color: var(--text-700); + transition: opacity .4s var(--ease-organic), transform .4s var(--ease-organic); + } +} + /* ─── Motion safety net ──────────────────────────────────── */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { diff --git a/src/pages/PAON.tsx b/src/pages/PAON.tsx index 22fb826..6a404d5 100644 --- a/src/pages/PAON.tsx +++ b/src/pages/PAON.tsx @@ -1,8 +1,52 @@ +import { Link } from 'react-router-dom'; +import { ArrowRight, UtensilsCrossed } from 'lucide-react'; import ModulePage from '../components/ModulePage'; import modules from '../data/modules'; const module = modules.find(m => m.id === 'paon')!; +function MenuCTA() { + return ( +
+
+
+
+ +
+
+ Bongkar bahan, kenali rasanya +
+

Intip menu Rasa lebih dekat

+

+ Setiap hidangan PAON punya cerita di tiap lapisnya. Lihat susunan bahan menu andalan kami, dari alas sampai penutup. +

+
+ +
+ + Lihat Menu + + + Tanya Dulu + +
+
+
+
+ ); +} + export default function PAON() { - return ; + return } />; } diff --git a/src/pages/PaonMenu.tsx b/src/pages/PaonMenu.tsx new file mode 100644 index 0000000..aee4d40 --- /dev/null +++ b/src/pages/PaonMenu.tsx @@ -0,0 +1,102 @@ +import { Link } from 'react-router-dom'; +import { ChevronRight } from 'lucide-react'; +import modules from '../data/modules'; +import Reveal from '../components/Reveal'; +import BurgerReveal from '../components/BurgerReveal'; + +const module = modules.find(m => m.id === 'paon')!; + +interface MenuItemData { + id: string; + name: string; + category: string; + description: string; + /** Item dengan animasi bongkar-bahan (burger utuh → terurai). Tambah item baru tanpa ini akan tampil sebagai kartu biasa. */ + hasReveal?: boolean; +} + +/** Tambah entri baru di sini untuk menambah menu — setiap item otomatis dapat section sendiri di bawah. */ +const menuItems: MenuItemData[] = [ + { + id: 'pumpkin-cheddar-burger', + name: 'Pumpkin Cheddar Burger', + category: 'Signature Burger', + description: 'Bun labu panggang, double patty, cheddar leleh, mango chutney, dan saus racikan PAON — 12 lapis rasa dalam satu genggaman.', + hasReveal: true, + }, +]; + +export default function PaonMenu() { + return ( +
+ {/* Hero */} +
+
+
+ +
+

🍽️ PAON

+

Menu Rasa

+

+ Good Food, Great Taste — kenali bahan di balik tiap hidangan andalan kami, disusun dari alas sampai penutup. +

+
+
+
+ + {/* Menu items */} + {menuItems.map((item, i) => ( +
+
+ +

{item.category}

+

{item.name}

+

{item.description}

+
+ + {item.hasReveal ? ( + + + + ) : ( + +
+

Detail bahan segera hadir.

+
+
+ )} +
+
+ ))} + + {/* More coming */} +
+
+

+ 🌱 Menu ini adalah ruang yang sedang tumbuh — hidangan lain akan menyusul. Ingin tahu menu lengkap atau reservasi? Hubungi kami langsung. +

+ + Hubungi Kami + +
+
+
+ ); +}