Compare commits
2 Commits
4aea7ca68c
...
66328f1379
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66328f1379 | ||
|
|
613eb58cba |
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 = [
|
||||
{
|
||||
id: 1,
|
||||
videoUrl: "https://videos.pexels.com/video-files/3253245/3253245-uhd_2560_1440_25fps.mp4",
|
||||
videoId: "Fpn1imb9qZg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
videoUrl: "https://videos.pexels.com/video-files/2086119/2086119-uhd_2560_1440_24fps.mp4",
|
||||
videoId: "qqvBnmUBP8g",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
videoUrl: "https://videos.pexels.com/video-files/3163534/3163534-uhd_2560_1440_30fps.mp4",
|
||||
videoId: "DK3hsi6NBjw",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Hero() {
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
|
||||
// Auto-play
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
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="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">
|
||||
{/* Left Content */}
|
||||
<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="w-4 h-3 bg-brutal-pink brutal-border"></div>
|
||||
<span className="text-black font-black tracking-widest text-xs uppercase">
|
||||
@@ -71,24 +68,21 @@ export default function Hero() {
|
||||
<div className="bg-white p-4 md:p-6 brutal-border shadow-[12px_12px_0_0_#000] lg:shadow-[16px_16px_0_0_#000] rotate-2 hover:rotate-0 transition-transform duration-500 max-w-2xl mx-auto xl:ml-auto">
|
||||
<div className="relative w-full aspect-video bg-black brutal-border overflow-hidden">
|
||||
{SLIDES.map((slide, idx) => (
|
||||
<video
|
||||
<iframe
|
||||
key={`video-${slide.id}`}
|
||||
src={slide.videoUrl}
|
||||
className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full h-full object-cover max-w-none pointer-events-none transition-opacity duration-700 ease-in-out ${
|
||||
src={`https://www.youtube.com/embed/${slide.videoId}?autoplay=1&mute=1&controls=0&loop=1&playlist=${slide.videoId}&modestbranding=1&playsinline=1&rel=0&showinfo=0&disablekb=1&fs=0`}
|
||||
className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[150%] h-[150%] max-w-none pointer-events-none transition-opacity duration-700 ease-in-out ${
|
||||
currentSlide === idx
|
||||
? "opacity-100 z-10"
|
||||
: "opacity-0 z-0"
|
||||
}`}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
allow="autoplay; encrypted-media; fullscreen; picture-in-picture"
|
||||
frameBorder="0"
|
||||
/>
|
||||
))}
|
||||
<div className="absolute inset-0 bg-blue-900 mix-blend-multiply opacity-20 pointer-events-none z-20"></div>
|
||||
</div>
|
||||
|
||||
{/* Bottom detail for polaroid vibe */}
|
||||
<div className="mt-5 flex justify-between items-center px-1">
|
||||
<div>
|
||||
<h3 className="text-xl font-black text-black">
|
||||
@@ -99,7 +93,6 @@ export default function Hero() {
|
||||
ON AIR
|
||||
</p>
|
||||
</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="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 { Link } from "react-router-dom";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
|
||||
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 (
|
||||
<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="flex items-center justify-between h-20">
|
||||
{/* 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">
|
||||
<Ticket className="w-6 h-6 stroke-[3]" />
|
||||
</div>
|
||||
<span className="font-display font-black text-2xl md:text-3xl text-black tracking-tighter uppercase">
|
||||
SMART TICKET
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Centered Navigation Links */}
|
||||
<div className="hidden md:flex flex-1 justify-center space-x-8 items-center">
|
||||
<Link
|
||||
to="/events"
|
||||
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"
|
||||
to="/"
|
||||
className={isHome ? activeClass : inactiveClass}
|
||||
>
|
||||
Tickets
|
||||
</Link>
|
||||
<Link
|
||||
to="/events"
|
||||
className="text-black font-black uppercase text-sm hover:text-[#7cb9ff] transition-colors"
|
||||
className={isExplore ? activeClass : inactiveClass}
|
||||
>
|
||||
Explore
|
||||
</Link>
|
||||
</div>
|
||||
<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
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,6 @@ export default function Checkout() {
|
||||
address: ''
|
||||
});
|
||||
|
||||
// If no ticket state, redirect back to events
|
||||
if (!ticketState) {
|
||||
return (
|
||||
<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) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Construct the payload structure requested by the user
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
address: formData.address,
|
||||
location_id: "98adc52b-966d-39db-809a-55902ee7228f", // hardcoded mock as requested
|
||||
payment_channel_id: "d48a46b6-3a18-3763-951d-66b7fdb284ae", // hardcoded mock
|
||||
location_id: "98adc52b-966d-39db-809a-55902ee7228f",
|
||||
payment_channel_id: "d48a46b6-3a18-3763-951d-66b7fdb284ae",
|
||||
properties: {},
|
||||
products: [
|
||||
{
|
||||
@@ -57,7 +55,6 @@ export default function Checkout() {
|
||||
totalPrice: ticketState.totalPrice
|
||||
};
|
||||
|
||||
// Navigate to payment page with this payload
|
||||
navigate('/payment', { state: { checkoutPayload: payload } });
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import QRCode from 'react-qr-code';
|
||||
import { ArrowLeft, CheckCircle2 } from 'lucide-react';
|
||||
import BrutalAlert, { AlertType } from '../components/BrutalAlert';
|
||||
|
||||
export default function Payment() {
|
||||
const location = useLocation();
|
||||
@@ -9,9 +10,9 @@ export default function Payment() {
|
||||
const checkoutPayload = location.state?.checkoutPayload;
|
||||
const [transactionId, setTransactionId] = useState('');
|
||||
const [isPaid, setIsPaid] = useState(false);
|
||||
const [alert, setAlert] = useState<{show: boolean, message: string, type: AlertType}>({ show: false, message: '', type: 'info' });
|
||||
|
||||
useEffect(() => {
|
||||
// Generate a mock Xendit Transaction ID when component mounts
|
||||
if (checkoutPayload && !transactionId) {
|
||||
const randomStr = Math.random().toString(36).substring(2, 10).toUpperCase();
|
||||
setTransactionId(`XND-QRIS-${randomStr}`);
|
||||
@@ -32,10 +33,15 @@ export default function Payment() {
|
||||
}
|
||||
|
||||
const handleSimulatePayment = () => {
|
||||
setAlert({ show: true, message: 'Validating Payment...', type: 'warning' });
|
||||
|
||||
setTimeout(() => {
|
||||
setAlert({ show: true, message: 'Payment Confirmed! Redirecting...', type: 'success' });
|
||||
setIsPaid(true);
|
||||
setTimeout(() => {
|
||||
navigate('/events');
|
||||
}, 4000);
|
||||
}, 3000);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
if (isPaid) {
|
||||
@@ -69,7 +75,7 @@ export default function Payment() {
|
||||
</button>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{alert.show && (
|
||||
<BrutalAlert
|
||||
message={alert.message}
|
||||
type={alert.type}
|
||||
onClose={() => setAlert(prev => ({ ...prev, show: false }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user