From ec2eb0d61998c080b7c23accbc20d526e00933ec Mon Sep 17 00:00:00 2001 From: everythingonblack Date: Mon, 18 Aug 2025 13:19:39 +0700 Subject: [PATCH] ok --- src/Checkout.js | 542 +++++++++++++++++++++------------------- src/Checkout.module.css | 336 ++++++++++++------------- 2 files changed, 438 insertions(+), 440 deletions(-) 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} -
    -

    {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')} -

-
- -
+ + + + + + + + + + + {products.map((item) => ( + + + + + + + ))} + +
ItemQuantitySubtotal
{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')} +

+ +
+

PAYMENT INFORMATION

+ + +
+ +
+ +
+ + +
+ +
+ 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; } }