diff --git a/src/Checkout.js b/src/Checkout.js
index 6287e2d..9fb207a 100644
--- a/src/Checkout.js
+++ b/src/Checkout.js
@@ -1,287 +1,313 @@
import React, { useEffect, useState } from 'react';
import styles from './Checkout.module.css';
-
import { QRCodeCanvas } from 'qrcode.react';
function parseJwt(token) {
- try {
- const base64Url = token.split('.')[1];
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
- const jsonPayload = decodeURIComponent(
- atob(base64)
- .split('')
- .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
- .join('')
- );
- return JSON.parse(jsonPayload);
- } catch (e) {
- return null;
- }
+ try {
+ const base64Url = token.split('.')[1];
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
+ const jsonPayload = decodeURIComponent(
+ atob(base64)
+ .split('')
+ .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
+ .join('')
+ );
+ return JSON.parse(jsonPayload);
+ } catch (e) {
+ return null;
+ }
}
const Checkout = ({ socketId, transactionSuccess }) => {
- const [products, setProducts] = useState([]);
- const [itemIds, setItemIds] = useState(null);
- const [token, setToken] = useState(null);
+ const [products, setProducts] = useState([]);
+ const [itemIds, setItemIds] = useState(null);
+ const [token, setToken] = useState(null);
- const [qrisData, setQrisData] = useState(null);
- const [value, setValue] = useState(null);
- const [loadingPay, setLoadingPay] = useState(false);
+ const [qrisData, setQrisData] = useState(null);
+ const [transferData, setTransferData] = useState(null);
+ const [value, setValue] = useState(null);
+ const [loadingPay, setLoadingPay] = useState(false);
- const [redirect_uri, setRedirect_Uri] = useState('');
- const [redirect_failed, setRedirect_Failed] = useState('');
+ const [redirect_uri, setRedirect_Uri] = useState('');
+ const [redirect_failed, setRedirect_Failed] = useState('');
- useEffect(() => {
- const urlParams = new URLSearchParams(window.location.search);
- const tokenParam = urlParams.get('token');
- const itemsIdString = urlParams.get('itemsId');
- setRedirect_Uri(urlParams.get('redirect_uri'));
- setRedirect_Failed(urlParams.get('redirect_failed'));
+ const [paymentMethod, setPaymentMethod] = useState('QRIS');
- setToken(tokenParam);
+ useEffect(() => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const tokenParam = urlParams.get('token');
+ const itemsIdString = urlParams.get('itemsId');
+ setRedirect_Uri(urlParams.get('redirect_uri') || '');
+ setRedirect_Failed(urlParams.get('redirect_failed') || '');
+ setToken(tokenParam);
- if (!itemsIdString) {
- window.location.href = redirect_failed;
- return;
+ if (!itemsIdString) {
+ window.location.href = urlParams.get('redirect_failed') || '/';
+ return;
+ }
+
+ try {
+ const parsedIds = JSON.parse(itemsIdString);
+ if (!Array.isArray(parsedIds) || parsedIds.length === 0) {
+ window.location.href = urlParams.get('redirect_failed') || '/';
+ return;
+ }
+ setItemIds(parsedIds);
+ } catch (e) {
+ console.error('Invalid itemsId format', e);
+ window.location.href = urlParams.get('redirect_failed') || '/';
+ }
+ }, []);
+
+ // Fetch products
+ useEffect(() => {
+ if (itemIds && Array.isArray(itemIds) && itemIds.length > 0) {
+ fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ itemsId: itemIds, noParents: true }),
+ })
+ .then((res) => res.json())
+ .then((data) => setProducts(data))
+ .catch((err) => console.error('Error fetching products:', err));
+ }
+ }, [itemIds]);
+
+ const handleRemove = (id) => {
+ const updatedItemIds = itemIds.filter((itemId) => itemId !== id);
+ const updatedProducts = products.filter((product) => product.id !== id);
+
+ if (updatedItemIds.length === 0) {
+ window.location.href = redirect_failed;
+ return;
+ }
+
+ setItemIds(updatedItemIds);
+ setProducts(updatedProducts);
+
+ const url = new URL(window.location);
+ url.searchParams.set('itemsId', JSON.stringify(updatedItemIds));
+ window.history.replaceState(null, '', url.toString());
+ };
+
+ const handlePay = async () => {
+ if (!itemIds || !token) {
+ alert('Token atau itemsId tidak ditemukan.');
+ return;
+ }
+
+ setLoadingPay(true);
+ try {
+ const urlParams = new URLSearchParams(window.location.search);
+ const newName = urlParams.get('new_name');
+ const setName = urlParams.get('set_name');
+
+ const params = new URLSearchParams();
+ itemIds.forEach((id) => params.append('itemsId', id));
+ params.append('socketId', socketId);
+ params.append('paymentMethod', paymentMethod);
+ if (newName) params.append('newName', newName);
+ if (setName) params.append('setName', setName);
+
+ const response = await fetch('https://bot.kediritechnopark.com/webhook/store-production/pay', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ Authorization: `Bearer ${token}`,
+ },
+ body: params.toString(),
+ });
+
+ const result = await response.json();
+ if (response.ok) {
+ if (paymentMethod === 'QRIS' && result?.qris_dynamic) {
+ setQrisData(result.qris_dynamic);
+ setValue(result.total_price);
+ } else if (paymentMethod === 'Bank Transfer' && result?.bank_account) {
+ setTransferData(result);
+ setValue(result.total_price);
+ } else {
+ alert(`Gagal memproses pembayaran: ${result?.error || 'Unknown error'}`);
}
+ } else {
+ alert(`Request gagal: ${result?.error || 'Unknown error'}`);
+ }
+ } catch (error) {
+ console.error('Network error:', error);
+ alert('Terjadi kesalahan jaringan.');
+ } finally {
+ setLoadingPay(false);
+ }
+ };
- try {
- const parsedIds = JSON.parse(itemsIdString);
- if (!Array.isArray(parsedIds) || parsedIds.length === 0) {
- window.location.href = redirect_failed;
- return;
- }
- setItemIds(parsedIds);
- } catch (e) {
- console.error('Invalid itemsId format', e);
- window.location.href = redirect_failed;
- }
- }, []);
-
- // Fetch products
- useEffect(() => {
- if (itemIds && Array.isArray(itemIds) && itemIds.length > 0) {
- fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ itemsId: itemIds, noParents: true }),
- })
- .then((res) => res.json())
- .then((data) => {
- setProducts(data);
- })
- .catch((err) => {
- console.error('Error fetching products:', err);
- });
- }
- }, [itemIds]);
-
- const handleRemove = (id) => {
- const updatedItemIds = itemIds.filter((itemId) => itemId !== id);
- const updatedProducts = products.filter((product) => product.id !== id);
-
- if (updatedItemIds.length === 0) {
- window.location.href = redirect_failed;
- return;
- }
-
- setItemIds(updatedItemIds);
- setProducts(updatedProducts);
-
- const url = new URL(window.location);
- url.searchParams.set('itemsId', JSON.stringify(updatedItemIds));
- window.history.replaceState(null, '', url.toString());
- };
-
- const handlePay = async () => {
- if (!itemIds || !token) {
- alert('Token atau itemsId tidak ditemukan.');
- return;
- }
-
- setLoadingPay(true);
-
- try {
- const urlParams = new URLSearchParams(window.location.search);
- const newName = urlParams.get('new_name'); // Ambil dari URL asli
- const setName = urlParams.get('set_name'); // Ambil dari URL asli
-
- const params = new URLSearchParams();
- itemIds.forEach((id) => params.append('itemsId', id));
- params.append('socketId', socketId);
- if (newName) {
- params.append('newName', newName);
- }
- if (setName) {
- params.append('setName', setName);
- }
-
-
- const response = await fetch('https://bot.kediritechnopark.com/webhook/store-production/pay', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- Authorization: `Bearer ${token}`,
- },
- body: params.toString(),
- });
-
- const result = await response.json();
-
- if (response.ok && result?.qris_dynamic) {
- setQrisData(result.qris_dynamic);
- setValue(result.total_price);
- } else {
- alert(`Gagal mendapatkan QRIS: ${result?.error || 'Unknown error'}`);
- }
- } catch (error) {
- console.error('Network error:', error);
- alert('Terjadi kesalahan jaringan.');
- } finally {
- setLoadingPay(false);
- }
- };
-
- useEffect(() => {
- if (transactionSuccess) {
- const timer = setTimeout(() => {
- window.location.href = redirect_uri;
- }, 10000); // 10 detik = 10000 ms
-
- // Bersihkan timer kalau komponen unmount atau transactionSuccess berubah
- return () => clearTimeout(timer);
- }
- }, [transactionSuccess]);
-
-
- return (
-
-
- {/* Product List */}
-
- {!qrisData &&
- <>
-
Your Cart
-
- {products.map((item, index) => (
- -
-
-

-
-
{item.name}
-
Rp{item.price?.toLocaleString('id-ID')}
- {item.duration?.hours && (
-
- Durasi: {item.duration.hours} jam
-
- )}
-
-
-
-
- ))}
-
- >
- }
-
- {qrisData && (
- <>
-
Silahkan scan QRIS ini
-
-
-
-
- {transactionSuccess && (
-
- )}
-
- {!transactionSuccess && (
- <>
-
Rp{value?.toLocaleString('id-ID')}
- >
- )}
-
- >
- )}
+ useEffect(() => {
+ if (transactionSuccess) {
+ const timer = setTimeout(() => {
+ window.location.href = redirect_uri;
+ }, 10000);
+ return () => clearTimeout(timer);
+ }
+ }, [transactionSuccess, redirect_uri]);
+ const subtotal = products.reduce((acc, item) => acc + (item.price || 0), 0);
+ const shipping = 0;
+ const tax = 0;
+ const grandTotal = subtotal + shipping + tax;
+ return (
+
+
+
+ {!qrisData && !transferData ? (
+ <>
+
+
+
+ KEDIRITECHNOPARK
+
+
+ Hello, {parseJwt(token)?.username || 'User'}
+ Thank you for your order
+
+
+
Invoice
+
ORDER #{String(itemIds?.[0] || '').padStart(5, '0')}
+
{new Date().toLocaleDateString()}
+
+
- {/* Checkout form */}
-
-
-
- Rp{qrisData ? value : products.reduce((acc, item) => acc + (item.price || 0), 0).toLocaleString('id-ID')}
-
-
-
-
+
+
+
+ | Item |
+ Quantity |
+ Subtotal |
+ |
+
+
+
+ {products.map((item) => (
+
+ | {item.name} |
+ {item.qty ?? 1} |
+
+ Rp{(item.price || 0).toLocaleString('id-ID')}
+ |
+
-
+ |
+
+ ))}
+
+
- {/* Footer */}
-
- Powered by KEDIRITECHNOPARK •{' '}
-
+
+
+ SUBTOTAL
+ Rp{subtotal.toLocaleString('id-ID')}
+
+ SHIPPING
+ Rp{shipping.toLocaleString('id-ID')}
+
+
+ TAX
+ Rp{tax.toLocaleString('id-ID')}
+
+
+ Total
+ Rp{grandTotal.toLocaleString('id-ID')}
+
+
+ >
+ ) : paymentMethod === 'QRIS' ? (
+ <>
+
Silakan scan QRIS ini
+
+
+ {transactionSuccess && (
+
+ )}
+ {!transactionSuccess &&
Rp{value?.toLocaleString('id-ID')}
}
+
+ >
+ ) : (
+
+
Bank Transfer Information
+
Bank{transferData?.bank_name}
+
Account No{transferData?.bank_account}
+
Account Name{transferData?.account_name}
+
TotalRp{value?.toLocaleString('id-ID')}
+ )}
- );
+
+
+
+
+ Rp{(qrisData || transferData ? value : grandTotal).toLocaleString('id-ID')}
+
+
+
+
+
+
+
+
+
+
+
+
+ Powered by KEDIRITECHNOPARK
+
+
+
+
+ );
};
export default Checkout;
diff --git a/src/Checkout.module.css b/src/Checkout.module.css
index fd4fb7d..3a13e54 100644
--- a/src/Checkout.module.css
+++ b/src/Checkout.module.css
@@ -1,240 +1,212 @@
+/* Layout wrapper */
+.page {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem;
+ box-sizing: border-box;
+}
+
.checkoutCard {
- border-radius: 1rem; /* rounded-2xl */
- box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
+ border-radius: 12px;
+ border: 1px solid #e5e7eb;
+ box-shadow: 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.1);
overflow: hidden;
display: flex;
- flex-direction: column; /* default mobile: vertical stack */
- max-width: 28rem; /* max-w-md */
- margin-left: auto;
- margin-right: auto;
- flex-grow: 1; /* agar bisa melebar dalam container flex */
+ flex-direction: column;
+ max-width: 64rem; /* desktop width */
+ width: 100%;
+ background: #fff;
}
-.cartSection, .checkoutSection {
- flex: 1; /* agar keduanya bisa melebar seimbang */
-}
+/* Sections */
+.cartSection, .checkoutSection { flex: 1; }
.cartSection {
- padding: 1.5rem 1.5rem; /* p-6 */
- background-color: #ececec; /* gray-50 */
- border-bottom: 1px solid #F3F4F6; /* border-gray-100 */
-}
-
-.cartTitle {
- font-size: 1.25rem; /* text-xl */
- font-weight: 600; /* font-semibold */
- color: #1F2937; /* gray-800 */
- margin-bottom: 1rem;
-}
-
-.cartList {
- list-style: none;
- padding: 0;
- margin: 0;
- display: flex;
- flex-direction: column;
- gap: 1rem; /* space-y-4 */
-}
-
-.cartItem {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 1rem;
-}
-
-.itemDetails {
- display: flex;
- align-items: center;
- gap: 1rem;
-}
-
-.productImage {
- width: 3.5rem; /* 14 */
- height: 3.5rem;
- border-radius: 0.5rem;
- object-fit: cover;
-}
-
-.itemText {
- font-weight: 500;
- color: #1F2937;
- margin: 0;
-}
-
-.itemPrice {
- font-size: 0.875rem; /* text-sm */
- color: #6B7280; /* gray-500 */
- margin: 0;
-}
-
-.removeBtn {
- font-weight: 700;
- font-size: 1.25rem;
- color: #EF4444; /* red-500 */
- background: none;
- border: none;
- cursor: pointer;
- transition: color 0.2s ease-in-out;
-}
-
-.removeBtn:hover {
- color: #B91C1C; /* red-700 */
+ padding: 2rem;
+ background: #ffffff;
+ border-bottom: 1px solid #f3f4f6;
}
.checkoutSection {
- padding: 2rem; /* p-8 */
+ padding: 2rem 3rem;
display: flex;
flex-direction: column;
justify-content: space-between;
- flex-grow: 1;
- background-color: white;
}
+/* Header invoice */
+.invHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ border-bottom: 1px solid #e5e7eb;
+ padding-bottom: 1rem;
+ margin-bottom: 1rem;
+}
+.brand {
+ font-size: 1.25rem;
+ font-weight: 800;
+ color: #2563eb;
+}
+.brandLight { font-weight: 500; }
+.greeting { margin-top: .5rem; font-size: .875rem; color: #374151; }
+.orderInfo { text-align: right; font-size: .875rem; color: #6b7280; }
+.invoiceLabel { font-size: 1rem; color: #4b5563; }
+.orderMeta { margin-top: .125rem; }
+
+/* Table */
+.table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: .5rem;
+ margin-bottom: 1rem;
+ font-size: .9rem;
+}
+.table th, .table td { padding: .75rem 0; }
+.table th {
+ border-bottom: 1px solid #d1d5db;
+ text-align: left;
+ color: #374151;
+ font-weight: 600;
+}
+.table td { color: #1f2937; border-bottom: 1px solid #f3f4f6; }
+.textRight { text-align: right; }
+.actionsCol { width: 2rem; }
+
+/* Remove button (small) */
+.removeBtn {
+ font-weight: 700;
+ font-size: 1.25rem;
+ line-height: 1;
+ color: #ef4444;
+ background: none;
+ border: none;
+ cursor: pointer;
+}
+.removeBtn:hover { color: #b91c1c; }
+
+/* Summary */
+.summary { margin-top: 1rem; text-align: left; }
+.summaryRow, .summaryTotal {
+ display: flex;
+ justify-content: space-between;
+ font-size: .9rem;
+ color: #374151;
+ margin-top: .25rem;
+}
+.summaryTotal {
+ font-weight: 700;
+ font-size: 1.125rem;
+ margin-top: .5rem;
+}
+
+/* Right side */
.checkoutTitle {
font-size: 1.25rem;
- font-weight: 600;
- color: #1F2937;
- margin-bottom: 1.5rem;
+ font-weight: 700;
+ color: #111827;
+ margin-bottom: 1rem;
}
+.paymentInfo { margin-bottom: 1rem; }
+.paymentHeading {
+ font-size: .75rem;
+ font-weight: 700;
+ color: #374151;
+ letter-spacing: .04em;
+ margin-bottom: .5rem;
+}
+.radioLabel {
+ display: block;
+ margin-bottom: .4rem;
+ font-size: .95rem;
+ color: #374151;
+}
+
+.inputGroup { margin-bottom: 1rem; }
.inputNote {
- width: 100%; /* tetap full width agar input memenuhi container */
- padding: 0.75rem 1rem;
- border: 1px solid #D1D5DB; /* gray-300 */
- border-radius: 0.5rem;
+ width: 100%;
+ padding: .75rem 1rem;
+ border: 1px solid #d1d5db;
+ border-radius: .5rem;
font-size: 1rem;
- transition: box-shadow 0.2s ease, border-color 0.2s ease;
- box-sizing: border-box; /* pastikan padding masuk ke width */
+ box-sizing: border-box;
}
-
.inputNote:focus {
outline: none;
- border-color: #3B82F6; /* blue-500 */
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
+ border-color: #3b82f6;
+ box-shadow: 0 0 0 2px rgba(59,130,246,.4);
}
.paymentBtn {
- width: 100%; /* tombol harus full lebar */
- background-color: #2563EB; /* blue-600 */
- color: white;
+ width: 100%;
+ background: #8b5cf6;
+ color: #fff;
font-weight: 700;
- padding: 0.75rem 0;
- border-radius: 0.5rem;
- font-size: 1.125rem; /* text-lg */
- cursor: pointer;
+ padding: .75rem 0;
+ border-radius: .5rem;
+ font-size: 1.05rem;
border: none;
- box-shadow: 0 4px 6px rgba(37, 99, 235, 0.5);
- transition: background-color 0.2s ease;
-}
-
-.paymentBtn:hover {
- background-color: #1D4ED8; /* blue-700 */
+ cursor: pointer;
+ box-shadow: 0 4px 6px rgba(139,92,246,.45);
+ transition: background-color .2s ease;
}
+.paymentBtn:hover { background: #7c3aed; }
+/* Footer */
.footerText {
- font-size: 0.75rem; /* text-xs */
- color: #6B7280; /* gray-500 */
+ font-size: .75rem;
+ color: #6b7280;
text-align: center;
margin-top: 2rem;
}
+.footerHighlight { font-weight: 700; color: #374151; }
-.footerLink {
+/* QR view */
+.qrTitle { font-size: .95rem; color: #374151; }
+.qrBox {
+ margin-top: 2rem;
+ text-align: center;
+ position: relative;
display: inline-block;
- padding: 0.25rem 0.5rem;
- border-radius: 0.375rem;
- color: inherit;
- text-decoration: none;
- transition: background-color 0.2s ease;
-}
-
-.footerLink:hover {
- background-color: #E5E7EB; /* gray-200 */
-}
-
-.footerHighlight {
- font-weight: 600;
- color: #374151; /* gray-700 */
-}
-
-/* Responsive layout for desktop */
-@media (min-width: 641px) {
- .checkoutCard {
- flex-direction: row; /* dua kolom berdampingan */
- max-width: 64rem; /* lebar container desktop */
- }
-
- .cartSection {
- border-bottom: none; /* hilangkan border bawah */
- border-right: 1px solid #F3F4F6; /* border kanan */
- padding: 2rem;
- }
-
- .checkoutSection {
- padding: 2rem 3rem;
- }
-}
-
-/* Responsive layout for small screens */
-@media (max-width: 640px) {
- .checkoutCard {
- max-width: 100%;
- }
}
+/* Checkmark animation (as-is from your logic) */
.CheckmarkOverlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
- background: rgba(255, 255, 255, 0.5);
+ background: rgba(255,255,255,.5);
width: 130px;
height: 130px;
- display: flex;
- align-items: center;
- justify-content: center;
+ display: flex; align-items: center; justify-content: center;
border-radius: 50%;
pointer-events: none;
z-index: 10;
}
-
-.CheckmarkSvg {
- width: 100px;
- height: 100px;
- display: block;
-}
-
+.Checkmark { width: 100px; height: 100px; display: block; }
.CheckmarkCircle {
- fill: none;
- stroke: #4BB543;
- stroke-width: 4;
- stroke-dasharray: 157; /* 2πr where r = 25 */
- stroke-dashoffset: 157;
- transform: rotate(-90deg);
- transform-origin: center;
+ fill: none; stroke: #4BB543; stroke-width: 4;
+ stroke-dasharray: 157; stroke-dashoffset: 157;
+ transform: rotate(-90deg); transform-origin: center;
animation: CircleFill 1s ease forwards;
}
-
.CheckmarkCheck {
- fill: none;
- stroke: #4BB543;
- stroke-width: 4;
- stroke-dasharray: 48;
- stroke-dashoffset: 48;
- animation: DrawCheck 0.5s ease forwards;
- animation-delay: 1s;
+ fill: none; stroke: #4BB543; stroke-width: 4;
+ stroke-dasharray: 48; stroke-dashoffset: 48;
+ animation: DrawCheck .5s ease forwards; animation-delay: 1s;
}
+@keyframes CircleFill { to { stroke-dashoffset: 0; } }
+@keyframes DrawCheck { to { stroke-dashoffset: 0; } }
-/* Circle fills in a clockwise motion */
-@keyframes CircleFill {
- to {
- stroke-dashoffset: 0;
- }
+/* Responsive */
+@media (min-width: 768px) {
+ .checkoutCard { flex-direction: row; }
+ .cartSection { border-right: 1px solid #f3f4f6; border-bottom: none; }
}
-
-/* Checkmark is drawn after circle is full */
-@keyframes DrawCheck {
- to {
- stroke-dashoffset: 0;
- }
+@media (max-width: 767px) {
+ .checkoutCard { flex-direction: column; }
}