feat: implement event card component and event detail page with mock data rendering
This commit is contained in:
@@ -4,6 +4,7 @@ import Navbar from './components/Navbar';
|
||||
import Footer from './components/Footer';
|
||||
import Home from './pages/Home';
|
||||
import AllEvents from './pages/AllEvents';
|
||||
import EventDetail from './pages/EventDetail';
|
||||
|
||||
function ScrollToTop() {
|
||||
const { pathname } = useLocation();
|
||||
@@ -26,6 +27,7 @@ function App() {
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/events" element={<AllEvents />} />
|
||||
<Route path="/event/:id" element={<EventDetail />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { MapPin, Calendar, ArrowRight } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
interface EventCardProps {
|
||||
id: string;
|
||||
title: string;
|
||||
image: string;
|
||||
date: string;
|
||||
@@ -12,6 +14,7 @@ interface EventCardProps {
|
||||
}
|
||||
|
||||
export default function EventCard({
|
||||
id,
|
||||
title,
|
||||
image,
|
||||
date,
|
||||
@@ -79,16 +82,22 @@ export default function EventCard({
|
||||
<p className="text-xs font-bold text-black mt-1 uppercase">BY: {organizer}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`flex items-center gap-2 px-5 py-3 text-sm uppercase ${
|
||||
isAvailable
|
||||
? 'bg-brutal-blue brutal-btn'
|
||||
: 'bg-gray-200 border-4 border-black font-bold text-gray-500 cursor-not-allowed'
|
||||
}`}
|
||||
{isAvailable ? (
|
||||
<Link
|
||||
to={`/event/${id}`}
|
||||
className="flex items-center gap-2 px-5 py-3 text-sm uppercase bg-brutal-blue brutal-btn"
|
||||
>
|
||||
{isAvailable ? 'BELI TIKET' : 'CLOSE'}
|
||||
BELI TIKET
|
||||
<ArrowRight className="w-5 h-5 stroke-[3]" />
|
||||
</Link>
|
||||
) : (
|
||||
<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>
|
||||
|
||||
204
src/pages/EventDetail.tsx
Normal file
204
src/pages/EventDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user