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 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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
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