feat: implement checkout and payment flow with navigation and order summary components
This commit is contained in:
50
src/components/BrutalAlert.tsx
Normal file
50
src/components/BrutalAlert.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { AlertCircle, CheckCircle, X, Info } from 'lucide-react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export type AlertType = 'success' | 'error' | 'warning' | 'info';
|
||||||
|
|
||||||
|
interface BrutalAlertProps {
|
||||||
|
message: string;
|
||||||
|
type?: AlertType;
|
||||||
|
onClose: () => void;
|
||||||
|
autoCloseMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BrutalAlert({ message, type = 'info', onClose, autoCloseMs = 3000 }: BrutalAlertProps) {
|
||||||
|
const styles = {
|
||||||
|
success: 'bg-[#4ade80] text-black',
|
||||||
|
error: 'bg-[#ef4444] text-black',
|
||||||
|
warning: 'bg-[#ffd700] text-black',
|
||||||
|
info: 'bg-[#7cb9ff] text-black',
|
||||||
|
};
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
success: <CheckCircle className="w-6 h-6 stroke-[3]" />,
|
||||||
|
error: <AlertCircle className="w-6 h-6 stroke-[3]" />,
|
||||||
|
warning: <AlertCircle className="w-6 h-6 stroke-[3]" />,
|
||||||
|
info: <Info className="w-6 h-6 stroke-[3]" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoCloseMs > 0) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
onClose();
|
||||||
|
}, autoCloseMs);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [autoCloseMs, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`fixed bottom-8 right-8 z-[100] animate-in slide-in-from-right-8 fade-in duration-300`}>
|
||||||
|
<div className={`${styles[type]} border-4 border-black shadow-[8px_8px_0_0_#000] p-5 flex items-center gap-4 min-w-[300px] max-w-md`}>
|
||||||
|
<div className="shrink-0 bg-white border-2 border-black p-1 rounded-full">
|
||||||
|
{icons[type]}
|
||||||
|
</div>
|
||||||
|
<p className="font-black uppercase text-sm flex-1 tracking-tight">{message}</p>
|
||||||
|
<button onClick={onClose} className="shrink-0 hover:-translate-y-1 transition-transform bg-white border-2 border-black p-1">
|
||||||
|
<X className="w-4 h-4 stroke-[3]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,22 +5,21 @@ import { Search } from "lucide-react";
|
|||||||
const SLIDES = [
|
const SLIDES = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
videoId: "Fpn1imb9qZg", // Konser
|
videoId: "Fpn1imb9qZg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
videoId: "qqvBnmUBP8g", // Lari
|
videoId: "qqvBnmUBP8g",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
videoId: "DK3hsi6NBjw", // Seminar
|
videoId: "DK3hsi6NBjw",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Hero() {
|
export default function Hero() {
|
||||||
const [currentSlide, setCurrentSlide] = useState(0);
|
const [currentSlide, setCurrentSlide] = useState(0);
|
||||||
|
|
||||||
// Auto-play
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
setCurrentSlide((prev) => (prev + 1) % SLIDES.length);
|
setCurrentSlide((prev) => (prev + 1) % SLIDES.length);
|
||||||
@@ -32,9 +31,7 @@ export default function Hero() {
|
|||||||
<div className="relative bg-[#ffd700] pt-28 pb-16 lg:pt-36 lg:pb-24 overflow-hidden border-b-4 border-black min-h-[70vh] flex items-center">
|
<div className="relative bg-[#ffd700] pt-28 pb-16 lg:pt-36 lg:pb-24 overflow-hidden border-b-4 border-black min-h-[70vh] flex items-center">
|
||||||
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8 relative z-10 w-full">
|
||||||
<div className="lg:grid lg:grid-cols-12 lg:gap-12 items-center">
|
<div className="lg:grid lg:grid-cols-12 lg:gap-12 items-center">
|
||||||
{/* Left Content */}
|
|
||||||
<div className="lg:col-span-6 mb-16 lg:mb-0 relative z-10">
|
<div className="lg:col-span-6 mb-16 lg:mb-0 relative z-10">
|
||||||
{/* Small Label */}
|
|
||||||
<div className="inline-flex items-center gap-3 bg-white brutal-border brutal-shadow-sm px-4 py-2 mb-8">
|
<div className="inline-flex items-center gap-3 bg-white brutal-border brutal-shadow-sm px-4 py-2 mb-8">
|
||||||
<div className="w-4 h-3 bg-brutal-pink brutal-border"></div>
|
<div className="w-4 h-3 bg-brutal-pink brutal-border"></div>
|
||||||
<span className="text-black font-black tracking-widest text-xs uppercase">
|
<span className="text-black font-black tracking-widest text-xs uppercase">
|
||||||
@@ -86,7 +83,6 @@ export default function Hero() {
|
|||||||
<div className="absolute inset-0 bg-blue-900 mix-blend-multiply opacity-20 pointer-events-none z-20"></div>
|
<div className="absolute inset-0 bg-blue-900 mix-blend-multiply opacity-20 pointer-events-none z-20"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom detail for polaroid vibe */}
|
|
||||||
<div className="mt-5 flex justify-between items-center px-1">
|
<div className="mt-5 flex justify-between items-center px-1">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-black text-black">
|
<h3 className="text-xl font-black text-black">
|
||||||
@@ -97,7 +93,6 @@ export default function Hero() {
|
|||||||
ON AIR
|
ON AIR
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/* Decorative squares from reference */}
|
|
||||||
<div className="grid grid-cols-2 gap-1 brutal-border p-1 bg-white shadow-[2px_2px_0_0_#000]">
|
<div className="grid grid-cols-2 gap-1 brutal-border p-1 bg-white shadow-[2px_2px_0_0_#000]">
|
||||||
<div className="w-3 h-3 bg-black"></div>
|
<div className="w-3 h-3 bg-black"></div>
|
||||||
<div className="w-3 h-3 bg-black"></div>
|
<div className="w-3 h-3 bg-black"></div>
|
||||||
|
|||||||
@@ -1,40 +1,47 @@
|
|||||||
import { Ticket } from "lucide-react";
|
import { Ticket } from "lucide-react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
|
const location = useLocation();
|
||||||
|
const isExplore = location.pathname.startsWith('/event');
|
||||||
|
const isHome = location.pathname === '/';
|
||||||
|
|
||||||
|
const activeClass = "bg-[#7cb9ff] text-black font-black uppercase text-sm px-6 py-2 border-4 border-black brutal-shadow-sm hover:-translate-y-1 hover:shadow-[6px_6px_0_0_#000] transition-all";
|
||||||
|
const inactiveClass = "text-black font-black uppercase text-sm hover:text-[#7cb9ff] transition-colors";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="fixed top-0 w-full z-50 bg-white brutal-border transition-all border-b-[6px]">
|
<nav className="fixed top-0 w-full z-50 bg-white brutal-border transition-all border-b-[6px]">
|
||||||
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex items-center justify-between h-20">
|
<div className="flex items-center justify-between h-20">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex-shrink-0 flex items-center cursor-pointer gap-3">
|
<Link to="/" className="flex-shrink-0 flex items-center cursor-pointer gap-3 hover:opacity-80 transition-opacity">
|
||||||
<div className="p-1 border-4 border-black bg-white">
|
<div className="p-1 border-4 border-black bg-white">
|
||||||
<Ticket className="w-6 h-6 stroke-[3]" />
|
<Ticket className="w-6 h-6 stroke-[3]" />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-display font-black text-2xl md:text-3xl text-black tracking-tighter uppercase">
|
<span className="font-display font-black text-2xl md:text-3xl text-black tracking-tighter uppercase">
|
||||||
SMART TICKET
|
SMART TICKET
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Link>
|
||||||
|
|
||||||
{/* Centered Navigation Links */}
|
{/* Centered Navigation Links */}
|
||||||
<div className="hidden md:flex flex-1 justify-center space-x-8 items-center">
|
<div className="hidden md:flex flex-1 justify-center space-x-8 items-center">
|
||||||
<Link
|
<Link
|
||||||
to="/events"
|
to="/"
|
||||||
className="bg-[#7cb9ff] text-black font-black uppercase text-sm px-6 py-2 border-4 border-black brutal-shadow-sm hover:-translate-y-1 hover:shadow-[6px_6px_0_0_#000] transition-all"
|
className={isHome ? activeClass : inactiveClass}
|
||||||
>
|
>
|
||||||
Tickets
|
Tickets
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/events"
|
to="/events"
|
||||||
className="text-black font-black uppercase text-sm hover:text-[#7cb9ff] transition-colors"
|
className={isExplore ? activeClass : inactiveClass}
|
||||||
>
|
>
|
||||||
Explore
|
Explore
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:flex items-center">
|
<div className="hidden md:flex items-center">
|
||||||
<button className="bg-[#ffd700] text-black font-black text-sm px-6 py-3 brutal-btn">
|
<Link to="/events" className="bg-[#ffd700] text-black font-black text-sm px-6 py-3 brutal-btn inline-block">
|
||||||
Find Your Event
|
Find Your Event
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export default function Checkout() {
|
|||||||
address: ''
|
address: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// If no ticket state, redirect back to events
|
|
||||||
if (!ticketState) {
|
if (!ticketState) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-brutal-bg min-h-screen flex items-center justify-center p-4">
|
<div className="bg-brutal-bg min-h-screen flex items-center justify-center p-4">
|
||||||
@@ -36,14 +35,13 @@ export default function Checkout() {
|
|||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Construct the payload structure requested by the user
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
phone: formData.phone,
|
phone: formData.phone,
|
||||||
address: formData.address,
|
address: formData.address,
|
||||||
location_id: "98adc52b-966d-39db-809a-55902ee7228f", // hardcoded mock as requested
|
location_id: "98adc52b-966d-39db-809a-55902ee7228f",
|
||||||
payment_channel_id: "d48a46b6-3a18-3763-951d-66b7fdb284ae", // hardcoded mock
|
payment_channel_id: "d48a46b6-3a18-3763-951d-66b7fdb284ae",
|
||||||
properties: {},
|
properties: {},
|
||||||
products: [
|
products: [
|
||||||
{
|
{
|
||||||
@@ -57,7 +55,6 @@ export default function Checkout() {
|
|||||||
totalPrice: ticketState.totalPrice
|
totalPrice: ticketState.totalPrice
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigate to payment page with this payload
|
|
||||||
navigate('/payment', { state: { checkoutPayload: payload } });
|
navigate('/payment', { state: { checkoutPayload: payload } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import QRCode from 'react-qr-code';
|
import QRCode from 'react-qr-code';
|
||||||
import { ArrowLeft, CheckCircle2 } from 'lucide-react';
|
import { ArrowLeft, CheckCircle2 } from 'lucide-react';
|
||||||
|
import BrutalAlert, { AlertType } from '../components/BrutalAlert';
|
||||||
|
|
||||||
export default function Payment() {
|
export default function Payment() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -9,9 +10,9 @@ export default function Payment() {
|
|||||||
const checkoutPayload = location.state?.checkoutPayload;
|
const checkoutPayload = location.state?.checkoutPayload;
|
||||||
const [transactionId, setTransactionId] = useState('');
|
const [transactionId, setTransactionId] = useState('');
|
||||||
const [isPaid, setIsPaid] = useState(false);
|
const [isPaid, setIsPaid] = useState(false);
|
||||||
|
const [alert, setAlert] = useState<{show: boolean, message: string, type: AlertType}>({ show: false, message: '', type: 'info' });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Generate a mock Xendit Transaction ID when component mounts
|
|
||||||
if (checkoutPayload && !transactionId) {
|
if (checkoutPayload && !transactionId) {
|
||||||
const randomStr = Math.random().toString(36).substring(2, 10).toUpperCase();
|
const randomStr = Math.random().toString(36).substring(2, 10).toUpperCase();
|
||||||
setTransactionId(`XND-QRIS-${randomStr}`);
|
setTransactionId(`XND-QRIS-${randomStr}`);
|
||||||
@@ -32,10 +33,15 @@ export default function Payment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSimulatePayment = () => {
|
const handleSimulatePayment = () => {
|
||||||
setIsPaid(true);
|
setAlert({ show: true, message: 'Validating Payment...', type: 'warning' });
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate('/events');
|
setAlert({ show: true, message: 'Payment Confirmed! Redirecting...', type: 'success' });
|
||||||
}, 4000);
|
setIsPaid(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/events');
|
||||||
|
}, 3000);
|
||||||
|
}, 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isPaid) {
|
if (isPaid) {
|
||||||
@@ -69,7 +75,7 @@ export default function Payment() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="bg-white brutal-border shadow-[12px_12px_0_0_#000] overflow-hidden">
|
<div className="bg-white brutal-border shadow-[12px_12px_0_0_#000] overflow-hidden">
|
||||||
{/* Header Xendit Mock */}
|
|
||||||
<div className="bg-[#121212] text-white p-6 flex justify-between items-center">
|
<div className="bg-[#121212] text-white p-6 flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">Merchant</p>
|
<p className="text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">Merchant</p>
|
||||||
@@ -122,6 +128,14 @@ export default function Payment() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{alert.show && (
|
||||||
|
<BrutalAlert
|
||||||
|
message={alert.message}
|
||||||
|
type={alert.type}
|
||||||
|
onClose={() => setAlert(prev => ({ ...prev, show: false }))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user