Update Web Paon

This commit is contained in:
2026-07-28 16:47:44 +07:00
parent 5bb0c57c8e
commit 5a95fbe6c4
10 changed files with 446 additions and 2 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

+2
View File
@@ -7,6 +7,7 @@ import PageTransition from './components/PageTransition';
import Home from './pages/Home'; import Home from './pages/Home';
import BhumiAnindha from './pages/BhumiAnindha'; import BhumiAnindha from './pages/BhumiAnindha';
import PAON from './pages/PAON'; import PAON from './pages/PAON';
import PaonMenu from './pages/PaonMenu';
import KaIND from './pages/KaIND'; import KaIND from './pages/KaIND';
import CittaRahayu from './pages/CittaRahayu'; import CittaRahayu from './pages/CittaRahayu';
import PasarBahagia from './pages/PasarBahagia'; import PasarBahagia from './pages/PasarBahagia';
@@ -59,6 +60,7 @@ function AppContent() {
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/bhumi-anindha" element={<BhumiAnindha />} /> <Route path="/bhumi-anindha" element={<BhumiAnindha />} />
<Route path="/paon" element={<PAON />} /> <Route path="/paon" element={<PAON />} />
<Route path="/paon/menu" element={<PaonMenu />} />
<Route path="/kaind" element={<KaIND />} /> <Route path="/kaind" element={<KaIND />} />
<Route path="/citta-rahayu" element={<CittaRahayu />} /> <Route path="/citta-rahayu" element={<CittaRahayu />} />
<Route path="/pasar-bahagia" element={<PasarBahagia />} /> <Route path="/pasar-bahagia" element={<PasarBahagia />} />
+226
View File
@@ -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 (
<motion.img
src={WHOLE_IMG}
alt="Burger PAON utuh"
className="burger-reveal__img"
initial={{ rotateY: 0, scale: 1, y: 0 }}
animate={controls}
/>
);
}
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 (
<motion.img
src={EXPLODED_IMG}
alt="Burger PAON terurai, menampilkan susunan bahan dari alas sampai penutup"
className="burger-reveal__img"
initial={{ rotateY: -100, scale: 0.76 }}
animate={controls}
/>
);
}
interface BurgerRevealProps {
color: string;
}
export default function BurgerReveal({ color }: BurgerRevealProps) {
const ref = useRef<HTMLDivElement>(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 (
<div ref={ref}>
{/* Desktop: gambar di tengah, label bahan melayang di kiri-kanan */}
<div className="burger-reveal">
<div className="burger-reveal__col burger-reveal__col--left">
{leftItems.map(it => (
<div
key={it.number}
className="burger-reveal__callout burger-reveal__callout--left"
style={{
top: `${it.top}%`,
opacity: labelsShown ? 1 : 0,
transform: `translateY(-50%) translateX(${labelsShown ? 0 : 16}px) scale(${labelsShown ? 1 : 0.9})`,
transitionDelay: `${delayFor(it.number)}s`,
}}
>
<span className="burger-reveal__text">{it.name}</span>
<span
className="burger-reveal__line"
style={{ width: labelsShown ? 28 : 0, transitionDelay: `${delayFor(it.number)}s` }}
/>
<span
className="burger-reveal__num"
style={{
background: color,
transform: `scale(${labelsShown ? 1 : 0.4})`,
transitionDelay: `${delayFor(it.number) + 0.1}s`,
}}
>
{it.number}
</span>
</div>
))}
</div>
<div className="burger-reveal__stage">
{!exploded ? (
<WholeBurger play={inView} onSpunAway={() => setExploded(true)} />
) : (
<ExplodedBurger onSettled={() => setLabelsShown(true)} />
)}
</div>
<div className="burger-reveal__col burger-reveal__col--right">
{rightItems.map(it => (
<div
key={it.number}
className="burger-reveal__callout burger-reveal__callout--right"
style={{
top: `${it.top}%`,
opacity: labelsShown ? 1 : 0,
transform: `translateY(-50%) translateX(${labelsShown ? 0 : -16}px) scale(${labelsShown ? 1 : 0.9})`,
transitionDelay: `${delayFor(it.number)}s`,
}}
>
<span
className="burger-reveal__num"
style={{
background: color,
transform: `scale(${labelsShown ? 1 : 0.4})`,
transitionDelay: `${delayFor(it.number) + 0.1}s`,
}}
>
{it.number}
</span>
<span
className="burger-reveal__line"
style={{ width: labelsShown ? 28 : 0, transitionDelay: `${delayFor(it.number)}s` }}
/>
<span className="burger-reveal__text">{it.name}</span>
</div>
))}
</div>
</div>
{/* Mobile: gambar di atas, daftar bahan bertingkat di bawah */}
<div className="burger-reveal-mobile">
<div className="burger-reveal-mobile__stage">
<img
src={exploded ? EXPLODED_IMG : WHOLE_IMG}
alt="Burger PAON"
className="burger-reveal__img"
/>
</div>
<ol className="burger-reveal-mobile__list">
{items.map(it => (
<li
key={it.number}
style={{
opacity: labelsShown ? 1 : 0,
transform: `translateX(${labelsShown ? 0 : -10}px)`,
transitionDelay: `${delayFor(it.number) * 0.6}s`,
}}
>
<span className="burger-reveal__num" style={{ background: color }}>{it.number}</span>
{it.name}
</li>
))}
</ol>
</div>
</div>
);
}
+6 -1
View File
@@ -1,13 +1,16 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ArrowRight, ChevronRight } from 'lucide-react'; import { ArrowRight, ChevronRight } from 'lucide-react';
import type { ReactNode } from 'react';
import type { Module } from '../data/modules'; import type { Module } from '../data/modules';
import EcosystemIcon from './EcosystemIcons'; import EcosystemIcon from './EcosystemIcons';
interface Props { interface Props {
module: Module; 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 ( return (
<> <>
{/* Hero */} {/* Hero */}
@@ -61,6 +64,8 @@ export default function ModulePage({ module: m }: Props) {
</div> </div>
</section> </section>
{afterAbout}
{/* Features */} {/* Features */}
<section className="section module-features" id="fitur"> <section className="section module-features" id="fitur">
<div className="container"> <div className="container">
+65
View File
@@ -838,6 +838,71 @@ html { scroll-behavior: smooth; scroll-padding-top: var(--nav-h); }
.delay-2 { animation-delay: .2s; } .delay-2 { animation-delay: .2s; }
.delay-3 { animation-delay: .3s; } .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 ──────────────────────────────────── */ /* ─── Motion safety net ──────────────────────────────────── */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
*, *::before, *::after { *, *::before, *::after {
+45 -1
View File
@@ -1,8 +1,52 @@
import { Link } from 'react-router-dom';
import { ArrowRight, UtensilsCrossed } from 'lucide-react';
import ModulePage from '../components/ModulePage'; import ModulePage from '../components/ModulePage';
import modules from '../data/modules'; import modules from '../data/modules';
const module = modules.find(m => m.id === 'paon')!; const module = modules.find(m => m.id === 'paon')!;
function MenuCTA() {
return (
<section className="section" style={{ background: 'var(--cream)' }}>
<div className="container">
<div style={{
background: `linear-gradient(140deg, ${module.gradientFrom} 0%, #8B3D1A 55%, ${module.color} 100%)`,
borderRadius: 'var(--r-xl)',
padding: '56px 48px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 32,
position: 'relative',
overflow: 'hidden',
}}>
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 50% 80% at 80% 50%, rgba(255,255,255,.05) 0%, transparent 60%)' }} />
<div style={{ position: 'relative', zIndex: 1, maxWidth: 560 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: '.82rem', color: 'rgba(255,255,255,.7)', marginBottom: 20 }}>
<UtensilsCrossed size={15} /> Bongkar bahan, kenali rasanya
</div>
<h2 style={{ color: '#fff', marginBottom: 12 }}>Intip menu Rasa lebih dekat</h2>
<p style={{ color: 'rgba(255,255,255,.7)', fontSize: '1rem', lineHeight: 1.75 }}>
Setiap hidangan PAON punya cerita di tiap lapisnya. Lihat susunan bahan menu andalan kami, dari alas sampai penutup.
</p>
</div>
<div style={{ position: 'relative', zIndex: 1, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<Link to="/paon/menu" className="btn btn--amber btn--lg">
Lihat Menu <ArrowRight size={16} />
</Link>
<Link to="/kontak" className="btn btn--outline-light">
Tanya Dulu
</Link>
</div>
</div>
</div>
</section>
);
}
export default function PAON() { export default function PAON() {
return <ModulePage module={module} />; return <ModulePage module={module} afterAbout={<MenuCTA />} />;
} }
+102
View File
@@ -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 (
<div className="page-top-pad">
{/* Hero */}
<section
style={{
background: `linear-gradient(150deg, ${module.gradientFrom} 0%, #8B3D1A 60%, ${module.color} 100%)`,
padding: '80px 0 96px',
position: 'relative',
overflow: 'hidden',
}}
>
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 60% 80% at 60% 50%, rgba(255,255,255,.08) 0%, transparent 60%)' }} />
<div className="container" style={{ position: 'relative', zIndex: 1 }}>
<nav className="breadcrumb">
<Link to="/">Beranda</Link>
<ChevronRight size={12} />
<Link to="/paon">PAON</Link>
<ChevronRight size={12} />
<span style={{ color: 'rgba(255,255,255,.8)' }}>Menu</span>
</nav>
<div style={{ maxWidth: 640 }}>
<p className="eyebrow eyebrow--amber" style={{ marginBottom: 16 }}>🍽️ PAON</p>
<h1 style={{ color: '#fff', marginBottom: 20 }}>Menu Rasa</h1>
<p style={{ fontSize: '1.1rem', color: 'rgba(255,255,255,.75)', lineHeight: 1.8 }}>
Good Food, Great Taste — kenali bahan di balik tiap hidangan andalan kami, disusun dari alas sampai penutup.
</p>
</div>
</div>
</section>
{/* Menu items */}
{menuItems.map((item, i) => (
<section
key={item.id}
className="section"
style={{ background: i % 2 === 0 ? 'var(--warm-white)' : 'var(--cream)' }}
>
<div className="container">
<Reveal className="section__header">
<p className="eyebrow" style={{ color: module.color }}>{item.category}</p>
<h2>{item.name}</h2>
<p>{item.description}</p>
</Reveal>
{item.hasReveal ? (
<Reveal delay={0.1}>
<BurgerReveal color={module.color} />
</Reveal>
) : (
<Reveal delay={0.1}>
<div className="card" style={{ maxWidth: 480, margin: '0 auto', padding: '32px 28px', textAlign: 'center' }}>
<p style={{ color: 'var(--text-300)', fontSize: '.9rem' }}>Detail bahan segera hadir.</p>
</div>
</Reveal>
)}
</div>
</section>
))}
{/* More coming */}
<section className="section" style={{ background: 'var(--warm-white)' }}>
<div className="container" style={{ maxWidth: 560, textAlign: 'center' }}>
<p style={{ color: 'var(--text-300)', fontSize: '.9rem', lineHeight: 1.7 }}>
🌱 Menu ini adalah ruang yang sedang tumbuh — hidangan lain akan menyusul. Ingin tahu menu lengkap atau reservasi? Hubungi kami langsung.
</p>
<Link to="/kontak" className="btn btn--outline" style={{ marginTop: 20 }}>
Hubungi Kami
</Link>
</div>
</section>
</div>
);
}