Update Web Paon
This commit is contained in:
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 |
@@ -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() {
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/bhumi-anindha" element={<BhumiAnindha />} />
|
||||
<Route path="/paon" element={<PAON />} />
|
||||
<Route path="/paon/menu" element={<PaonMenu />} />
|
||||
<Route path="/kaind" element={<KaIND />} />
|
||||
<Route path="/citta-rahayu" element={<CittaRahayu />} />
|
||||
<Route path="/pasar-bahagia" element={<PasarBahagia />} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{afterAbout}
|
||||
|
||||
{/* Features */}
|
||||
<section className="section module-features" id="fitur">
|
||||
<div className="container">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+45
-1
@@ -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 (
|
||||
<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() {
|
||||
return <ModulePage module={module} />;
|
||||
return <ModulePage module={module} afterAbout={<MenuCTA />} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user