diff --git a/assets/image/logo/awh.png b/assets/image/logo/awh.png
new file mode 100644
index 0000000..6aedb31
Binary files /dev/null and b/assets/image/logo/awh.png differ
diff --git a/package-lock.json b/package-lock.json
index 92d5828..6cb0d8f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"lucide-react": "^0.475.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-qr-code": "^2.0.21",
"react-router-dom": "^7.15.1"
},
"devDependencies": {
@@ -2898,7 +2899,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -3269,6 +3269,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3351,6 +3363,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3493,6 +3514,17 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3503,6 +3535,12 @@
"node": ">=6"
}
},
+ "node_modules/qr.js": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
+ "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==",
+ "license": "MIT"
+ },
"node_modules/react": {
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
@@ -3524,6 +3562,25 @@
"react": "^19.2.6"
}
},
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-qr-code": {
+ "version": "2.0.21",
+ "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.21.tgz",
+ "integrity": "sha512-xaywjo0eaF4S3LOz6ns5eoPbM2E+q9HYl4VATYpxK4bBniOhQ9noY2RJ9G4SnZFhUwzx63FUT6KdHzfKgUwyuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.8.1",
+ "qr.js": "0.0.0"
+ },
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
diff --git a/package.json b/package.json
index 0425926..133b098 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"lucide-react": "^0.475.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+ "react-qr-code": "^2.0.21",
"react-router-dom": "^7.15.1"
},
"devDependencies": {
diff --git a/src/App.tsx b/src/App.tsx
index a305aa9..adebf66 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,6 +5,8 @@ import Footer from './components/Footer';
import Home from './pages/Home';
import AllEvents from './pages/AllEvents';
import EventDetail from './pages/EventDetail';
+import Checkout from './pages/Checkout';
+import Payment from './pages/Payment';
function ScrollToTop() {
const { pathname } = useLocation();
@@ -28,6 +30,8 @@ function App() {
} />
} />
} />
+ } />
+ } />
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx
index 3a2b739..4a295f2 100644
--- a/src/components/Hero.tsx
+++ b/src/components/Hero.tsx
@@ -5,15 +5,15 @@ import { Search } from "lucide-react";
const SLIDES = [
{
id: 1,
- videoId: "Fpn1imb9qZg",
+ videoUrl: "https://videos.pexels.com/video-files/3253245/3253245-uhd_2560_1440_25fps.mp4",
},
{
id: 2,
- videoId: "PN7Pv23FrWg",
+ videoUrl: "https://videos.pexels.com/video-files/2086119/2086119-uhd_2560_1440_24fps.mp4",
},
{
id: 3,
- videoId: "YsJqKjD2sXE",
+ videoUrl: "https://videos.pexels.com/video-files/3163534/3163534-uhd_2560_1440_30fps.mp4",
},
];
@@ -71,16 +71,18 @@ export default function Hero() {
{SLIDES.map((slide, idx) => (
-
))}
diff --git a/src/pages/Checkout.tsx b/src/pages/Checkout.tsx
new file mode 100644
index 0000000..c1545cf
--- /dev/null
+++ b/src/pages/Checkout.tsx
@@ -0,0 +1,178 @@
+import { useState } from 'react';
+import { useLocation, useNavigate, Link } from 'react-router-dom';
+import { ArrowLeft } from 'lucide-react';
+
+export default function Checkout() {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const ticketState = location.state;
+
+ const [formData, setFormData] = useState({
+ name: '',
+ email: '',
+ phone: '',
+ address: ''
+ });
+
+ // If no ticket state, redirect back to events
+ if (!ticketState) {
+ return (
+
+
+
No Ticket Selected
+
+ Back to Events
+
+
+
+ );
+ }
+
+ const handleChange = (e: React.ChangeEvent
) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ 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
+ properties: {},
+ products: [
+ {
+ id: ticketState.ticketId,
+ name: ticketState.ticketName,
+ price: ticketState.basePrice,
+ insurance: ticketState.hasInsurance,
+ insurance_price: ticketState.insurancePrice
+ }
+ ],
+ totalPrice: ticketState.totalPrice
+ };
+
+ // Navigate to payment page with this payload
+ navigate('/payment', { state: { checkoutPayload: payload } });
+ };
+
+ return (
+
+
+
+
+ Back to Ticket
+
+
+
+ Checkout
+
+
+
+
+
+
+
+
Order Summary
+
+
+
+
Ticket
+
{ticketState.ticketName}
+
IDR {ticketState.basePrice.toLocaleString('id-ID')}
+
+
+ {ticketState.hasInsurance && (
+
+
Addon
+
Insurance
+
IDR {ticketState.insurancePrice.toLocaleString('id-ID')}
+
+ )}
+
+
+
+
Total Payment
+
+ IDR {ticketState.totalPrice.toLocaleString('id-ID')}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/EventDetail.tsx b/src/pages/EventDetail.tsx
index d6cdc43..4748703 100644
--- a/src/pages/EventDetail.tsx
+++ b/src/pages/EventDetail.tsx
@@ -1,5 +1,5 @@
import { useState } from 'react';
-import { useParams, Link } from 'react-router-dom';
+import { useParams, Link, useNavigate } from 'react-router-dom';
import { ArrowLeft, MapPin, Calendar, Info, FileText, Ticket as TicketIcon } from 'lucide-react';
const MOCK_EVENT_DETAIL = {
@@ -20,26 +20,25 @@ const MOCK_EVENT_DETAIL = {
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',
+ name: 'PRESALE',
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',
+ basePrice: 135000,
+ insurancePrice: 20000,
}
]
};
export default function EventDetail() {
const { id } = useParams();
+ const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<'info' | 'desc' | 'ticket'>('ticket');
+ const [insuranceSelections, setInsuranceSelections] = useState>({});
+
+ const toggleInsurance = (ticketId: string) => {
+ setInsuranceSelections(prev => ({ ...prev, [ticketId]: !prev[ticketId] }));
+ };
// Dalam aplikasi nyata, Anda akan fetch data berdasarkan `id`
const event = MOCK_EVENT_DETAIL;
@@ -164,7 +163,11 @@ export default function EventDetail() {
{/* TICKET TAB */}
{activeTab === 'ticket' && (
- {event.tickets.map((ticket) => (
+ {event.tickets.map((ticket) => {
+ const hasInsurance = insuranceSelections[ticket.id] || false;
+ const totalPrice = ticket.basePrice + (hasInsurance && ticket.insurancePrice ? ticket.insurancePrice : 0);
+
+ return (
{ticket.name}
@@ -182,16 +185,51 @@ export default function EventDetail() {
-
-
- {ticket.price}
+
+ {ticket.insurancePrice && (
+
toggleInsurance(ticket.id)}
+ >
+ {}}
+ className="w-5 h-5 accent-black brutal-border cursor-pointer"
+ />
+
+
+ )}
+
+
+
+ IDR {totalPrice.toLocaleString('id-ID')}
+
+
-
- ))}
+ )})}
)}
diff --git a/src/pages/Payment.tsx b/src/pages/Payment.tsx
new file mode 100644
index 0000000..1a61b8a
--- /dev/null
+++ b/src/pages/Payment.tsx
@@ -0,0 +1,127 @@
+import { useState, useEffect } from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
+import QRCode from 'react-qr-code';
+import { ArrowLeft, CheckCircle2 } from 'lucide-react';
+
+export default function Payment() {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const checkoutPayload = location.state?.checkoutPayload;
+ const [transactionId, setTransactionId] = useState('');
+ const [isPaid, setIsPaid] = useState(false);
+
+ 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}`);
+ }
+ }, [checkoutPayload, transactionId]);
+
+ if (!checkoutPayload) {
+ return (
+
+
+
Invalid Payment Session
+
+
+
+ );
+ }
+
+ const handleSimulatePayment = () => {
+ setIsPaid(true);
+ setTimeout(() => {
+ navigate('/events');
+ }, 4000);
+ };
+
+ if (isPaid) {
+ return (
+
+
+
+
+
+
Payment Success!
+
Your ticket has been booked successfully. Redirecting...
+
+
Transaction ID
+
{transactionId}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {/* Header Xendit Mock */}
+
+
+
Merchant
+
SMART TICKET INC.
+
+
+ QRIS
+
+
+
+
+
+
Total Amount
+
+ IDR {checkoutPayload.totalPrice.toLocaleString('id-ID')}
+
+
TRX ID: {transactionId}
+
+
+
+
+ {transactionId ? (
+
+ ) : (
+
Loading QR...
+ )}
+
+
+
+
+
How to pay:
+
+ - Open your payment app (Gopay, OVO, BCA, dll).
+ - Scan the QR Code above.
+ - Check the details and amount.
+ - Enter your PIN to confirm payment.
+
+
+
+
+
+
+
+
+ );
+}