feat: implement event card component and event detail page with mock data rendering

This commit is contained in:
Kevin Satria D
2026-05-20 14:15:03 +07:00
parent a3ae3bfcfe
commit 1914b41941
3 changed files with 225 additions and 10 deletions

View File

@@ -4,6 +4,7 @@ import Navbar from './components/Navbar';
import Footer from './components/Footer'; import Footer from './components/Footer';
import Home from './pages/Home'; import Home from './pages/Home';
import AllEvents from './pages/AllEvents'; import AllEvents from './pages/AllEvents';
import EventDetail from './pages/EventDetail';
function ScrollToTop() { function ScrollToTop() {
const { pathname } = useLocation(); const { pathname } = useLocation();
@@ -26,6 +27,7 @@ function App() {
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/events" element={<AllEvents />} /> <Route path="/events" element={<AllEvents />} />
<Route path="/event/:id" element={<EventDetail />} />
</Routes> </Routes>
</main> </main>

View File

@@ -1,6 +1,8 @@
import { MapPin, Calendar, ArrowRight } from 'lucide-react'; import { MapPin, Calendar, ArrowRight } from 'lucide-react';
import { Link } from 'react-router-dom';
interface EventCardProps { interface EventCardProps {
id: string;
title: string; title: string;
image: string; image: string;
date: string; date: string;
@@ -12,6 +14,7 @@ interface EventCardProps {
} }
export default function EventCard({ export default function EventCard({
id,
title, title,
image, image,
date, date,
@@ -79,16 +82,22 @@ export default function EventCard({
<p className="text-xs font-bold text-black mt-1 uppercase">BY: {organizer}</p> <p className="text-xs font-bold text-black mt-1 uppercase">BY: {organizer}</p>
</div> </div>
<button {isAvailable ? (
className={`flex items-center gap-2 px-5 py-3 text-sm uppercase ${ <Link
isAvailable to={`/event/${id}`}
? 'bg-brutal-blue brutal-btn' className="flex items-center gap-2 px-5 py-3 text-sm uppercase bg-brutal-blue brutal-btn"
: 'bg-gray-200 border-4 border-black font-bold text-gray-500 cursor-not-allowed' >
}`} BELI TIKET
> <ArrowRight className="w-5 h-5 stroke-[3]" />
{isAvailable ? 'BELI TIKET' : 'CLOSE'} </Link>
<ArrowRight className="w-5 h-5 stroke-[3]" /> ) : (
</button> <button
className="flex items-center gap-2 px-5 py-3 text-sm uppercase bg-gray-200 border-4 border-black font-bold text-gray-500 cursor-not-allowed"
>
CLOSE
<ArrowRight className="w-5 h-5 stroke-[3]" />
</button>
)}
</div> </div>
</div> </div>
</div> </div>

204
src/pages/EventDetail.tsx Normal file
View File

@@ -0,0 +1,204 @@
import { useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { ArrowLeft, MapPin, Calendar, Info, FileText, Ticket as TicketIcon } from 'lucide-react';
const MOCK_EVENT_DETAIL = {
id: '1',
title: 'ADVENTURE FEST 2026 MUTIARA GADING CITY',
image: 'https://images.unsplash.com/photo-1540039155732-6761b54f6cce?q=80&w=1200&auto=format&fit=crop',
date: 'Minggu, 26 April 2026',
time: 'Pk. 06.00 - 12.00 WIB',
location: 'South Lake Park, Kabupaten Bekasi',
fullAddress: 'Blk. M 09 - M 10 No.15, Setia Asih, Kec. Tarumajaya, Kabupaten Bekasi, Jawa Barat 17215',
organizer: 'Harmoni Kreasi',
description: [
'Tiket hanya berlaku pada hari H',
'Acara dimulai pukul 06.00 WIB hingga selesai.',
'Peserta wajib membawa kartu identitas (KTP/SIM/Kartu Pelajar) saat acara berlangsung.',
'Pendaftaran bersifat final, tidak dapat dialihkan kepada pihak lain, tidak dapat mengubah kategori lomba.'
],
tickets: [
{
id: 't1',
name: 'PRESALE INCLUDE INSURANCE',
benefits: 'Include: Jersey Peserta + Insurance + Finisher Medal + Best Finisher + Waterstation + Refreshment + Tiket Konser UTOPIA Harga Sudah Termasuk Pajak',
category: 'PELARI',
validDate: '24 Mei 2026',
price: 'IDR 155.000',
},
{
id: 't2',
name: 'PRESALE NON INSURANCE',
benefits: 'Include: Jersey Peserta + Finisher Medal + Best Finisher + Waterstation + Refreshment + Tiket Konser UTOPIA Harga Sudah Termasuk Pajak',
category: 'PELARI',
validDate: '24 Mei 2026',
price: 'IDR 135.000',
}
]
};
export default function EventDetail() {
const { id } = useParams();
const [activeTab, setActiveTab] = useState<'info' | 'desc' | 'ticket'>('ticket');
// Dalam aplikasi nyata, Anda akan fetch data berdasarkan `id`
const event = MOCK_EVENT_DETAIL;
return (
<div className="bg-brutal-bg min-h-screen pt-24 pb-20">
<div className="max-w-[1000px] mx-auto px-4 sm:px-6 lg:px-8">
{/* Back Button */}
<Link
to="/events"
className="inline-flex items-center gap-2 mb-6 text-black font-black uppercase hover:-translate-x-1 transition-transform text-sm"
>
<ArrowLeft className="w-5 h-5 stroke-[3]" />
KEMBALI KE EVENTS
</Link>
{/* Hero Banner */}
<div className="relative w-full h-64 md:h-80 lg:h-96 brutal-border shadow-[8px_8px_0_0_#000] mb-8 overflow-hidden bg-black">
<img
src={event.image}
alt={event.title}
className="w-full h-full object-cover opacity-60 mix-blend-overlay"
/>
<div className="absolute inset-0 p-6 md:p-10 flex flex-col justify-end bg-gradient-to-t from-black/80 to-transparent">
<h1 className="text-3xl md:text-4xl lg:text-5xl font-display font-black text-white uppercase tracking-tight leading-tight max-w-3xl brutal-text-shadow">
{event.title}
</h1>
<p className="text-brutal-yellow font-black text-lg md:text-xl mt-2 uppercase tracking-widest brutal-text-shadow">
{event.date}
</p>
</div>
</div>
{/* Info Highlights */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
<div className="bg-white p-4 brutal-border shadow-[4px_4px_0_0_#000] flex items-center gap-4">
<div className="w-10 h-10 bg-brutal-blue brutal-border flex items-center justify-center shrink-0">
<Calendar className="w-5 h-5 text-white stroke-[3]" />
</div>
<div>
<p className="text-xs font-bold text-gray-500 uppercase">Waktu Pelaksanaan</p>
<p className="text-sm font-black text-black uppercase">{event.time}</p>
</div>
</div>
<div className="bg-white p-4 brutal-border shadow-[4px_4px_0_0_#000] flex items-center gap-4">
<div className="w-10 h-10 bg-brutal-pink brutal-border flex items-center justify-center shrink-0">
<MapPin className="w-5 h-5 text-black stroke-[3]" />
</div>
<div>
<p className="text-xs font-bold text-gray-500 uppercase">Lokasi</p>
<p className="text-sm font-black text-black uppercase truncate">{event.location}</p>
</div>
</div>
</div>
{/* Tabs System */}
<div className="bg-white brutal-border shadow-[8px_8px_0_0_#000]">
{/* Tab Headers */}
<div className="flex border-b-4 border-black overflow-x-auto hide-scrollbar">
<button
onClick={() => setActiveTab('info')}
className={`flex-1 flex items-center justify-center gap-2 py-4 px-6 font-black uppercase text-sm border-r-4 border-black transition-colors min-w-[120px] ${
activeTab === 'info' ? 'bg-brutal-yellow text-black' : 'bg-white text-gray-500 hover:bg-gray-100'
}`}
>
<Info className="w-4 h-4 stroke-[3]" /> INFO
</button>
<button
onClick={() => setActiveTab('desc')}
className={`flex-1 flex items-center justify-center gap-2 py-4 px-6 font-black uppercase text-sm border-r-4 border-black transition-colors min-w-[140px] ${
activeTab === 'desc' ? 'bg-brutal-yellow text-black' : 'bg-white text-gray-500 hover:bg-gray-100'
}`}
>
<FileText className="w-4 h-4 stroke-[3]" /> DESCRIPTION
</button>
<button
onClick={() => setActiveTab('ticket')}
className={`flex-1 flex items-center justify-center gap-2 py-4 px-6 font-black uppercase text-sm transition-colors min-w-[120px] ${
activeTab === 'ticket' ? 'bg-brutal-blue text-white' : 'bg-white text-gray-500 hover:bg-gray-100'
}`}
>
<TicketIcon className="w-4 h-4 stroke-[3]" /> TICKET
</button>
</div>
{/* Tab Content */}
<div className="p-6 md:p-8">
{/* INFO TAB */}
{activeTab === 'info' && (
<div className="animate-in fade-in duration-300">
<h3 className="text-xl font-black text-black uppercase mb-4 border-b-4 border-black inline-block pb-1">Venue Detail</h3>
<div className="bg-brutal-bg p-4 brutal-border mb-4">
<h4 className="font-black text-lg text-black mb-1">{event.location}</h4>
<p className="text-sm font-bold text-gray-700 leading-relaxed mb-4">
{event.fullAddress}
</p>
<button className="px-4 py-2 bg-black text-white font-black text-sm uppercase hover:bg-gray-800 transition-colors brutal-border shadow-[4px_4px_0_0_#ffd700]">
Check in Google Map
</button>
</div>
</div>
)}
{/* DESCRIPTION TAB */}
{activeTab === 'desc' && (
<div className="animate-in fade-in duration-300">
<h3 className="text-xl font-black text-black uppercase mb-4 border-b-4 border-black inline-block pb-1">Syarat & Ketentuan</h3>
<ul className="space-y-3">
{event.description.map((desc, idx) => (
<li key={idx} className="flex gap-3 text-sm font-bold text-gray-800">
<span className="w-1.5 h-1.5 bg-brutal-pink brutal-border mt-1.5 shrink-0" />
{desc}
</li>
))}
</ul>
</div>
)}
{/* TICKET TAB */}
{activeTab === 'ticket' && (
<div className="animate-in fade-in duration-300 space-y-6">
{event.tickets.map((ticket) => (
<div key={ticket.id} className="bg-white brutal-border p-5 relative group hover:-translate-y-1 hover:shadow-[6px_6px_0_0_#000] transition-all">
<h4 className="text-lg font-black text-black uppercase mb-2">
{ticket.name}
</h4>
<p className="text-sm font-medium text-gray-600 mb-3 leading-relaxed">
{ticket.benefits}
</p>
<div className="flex gap-2 mb-4">
<span className="px-3 py-1 bg-brutal-bg brutal-border text-xs font-black uppercase">
{ticket.category}
</span>
<span className="px-3 py-1 bg-gray-100 brutal-border text-xs font-bold text-gray-600 flex items-center gap-1">
<Calendar className="w-3 h-3" /> Valid: {ticket.validDate}
</span>
</div>
<div className="border-t-2 border-dashed border-gray-300 pt-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4 mt-4">
<div className="text-xl font-display font-black text-black">
{ticket.price}
</div>
<button className="px-6 py-3 bg-brutal-blue text-white font-black text-sm uppercase brutal-btn shrink-0">
Buy Ticket
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
}