ok
This commit is contained in:
240
src/Checkout.js
240
src/Checkout.js
@@ -1,6 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import styles from './Checkout.module.css';
|
import styles from './Checkout.module.css';
|
||||||
|
|
||||||
import { QRCodeCanvas } from 'qrcode.react';
|
import { QRCodeCanvas } from 'qrcode.react';
|
||||||
|
|
||||||
function parseJwt(token) {
|
function parseJwt(token) {
|
||||||
@@ -25,36 +24,38 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
|||||||
const [token, setToken] = useState(null);
|
const [token, setToken] = useState(null);
|
||||||
|
|
||||||
const [qrisData, setQrisData] = useState(null);
|
const [qrisData, setQrisData] = useState(null);
|
||||||
|
const [transferData, setTransferData] = useState(null);
|
||||||
const [value, setValue] = useState(null);
|
const [value, setValue] = useState(null);
|
||||||
const [loadingPay, setLoadingPay] = useState(false);
|
const [loadingPay, setLoadingPay] = useState(false);
|
||||||
|
|
||||||
const [redirect_uri, setRedirect_Uri] = useState('');
|
const [redirect_uri, setRedirect_Uri] = useState('');
|
||||||
const [redirect_failed, setRedirect_Failed] = useState('');
|
const [redirect_failed, setRedirect_Failed] = useState('');
|
||||||
|
|
||||||
|
const [paymentMethod, setPaymentMethod] = useState('QRIS');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const tokenParam = urlParams.get('token');
|
const tokenParam = urlParams.get('token');
|
||||||
const itemsIdString = urlParams.get('itemsId');
|
const itemsIdString = urlParams.get('itemsId');
|
||||||
setRedirect_Uri(urlParams.get('redirect_uri'));
|
setRedirect_Uri(urlParams.get('redirect_uri') || '');
|
||||||
setRedirect_Failed(urlParams.get('redirect_failed'));
|
setRedirect_Failed(urlParams.get('redirect_failed') || '');
|
||||||
|
|
||||||
setToken(tokenParam);
|
setToken(tokenParam);
|
||||||
|
|
||||||
if (!itemsIdString) {
|
if (!itemsIdString) {
|
||||||
window.location.href = redirect_failed;
|
window.location.href = urlParams.get('redirect_failed') || '/';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedIds = JSON.parse(itemsIdString);
|
const parsedIds = JSON.parse(itemsIdString);
|
||||||
if (!Array.isArray(parsedIds) || parsedIds.length === 0) {
|
if (!Array.isArray(parsedIds) || parsedIds.length === 0) {
|
||||||
window.location.href = redirect_failed;
|
window.location.href = urlParams.get('redirect_failed') || '/';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setItemIds(parsedIds);
|
setItemIds(parsedIds);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Invalid itemsId format', e);
|
console.error('Invalid itemsId format', e);
|
||||||
window.location.href = redirect_failed;
|
window.location.href = urlParams.get('redirect_failed') || '/';
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -63,18 +64,12 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
|||||||
if (itemIds && Array.isArray(itemIds) && itemIds.length > 0) {
|
if (itemIds && Array.isArray(itemIds) && itemIds.length > 0) {
|
||||||
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
|
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ itemsId: itemIds, noParents: true }),
|
body: JSON.stringify({ itemsId: itemIds, noParents: true }),
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => setProducts(data))
|
||||||
setProducts(data);
|
.catch((err) => console.error('Error fetching products:', err));
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error('Error fetching products:', err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [itemIds]);
|
}, [itemIds]);
|
||||||
|
|
||||||
@@ -102,22 +97,17 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoadingPay(true);
|
setLoadingPay(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const newName = urlParams.get('new_name'); // Ambil dari URL asli
|
const newName = urlParams.get('new_name');
|
||||||
const setName = urlParams.get('set_name'); // Ambil dari URL asli
|
const setName = urlParams.get('set_name');
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
itemIds.forEach((id) => params.append('itemsId', id));
|
itemIds.forEach((id) => params.append('itemsId', id));
|
||||||
params.append('socketId', socketId);
|
params.append('socketId', socketId);
|
||||||
if (newName) {
|
params.append('paymentMethod', paymentMethod);
|
||||||
params.append('newName', newName);
|
if (newName) params.append('newName', newName);
|
||||||
}
|
if (setName) params.append('setName', setName);
|
||||||
if (setName) {
|
|
||||||
params.append('setName', setName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const response = await fetch('https://bot.kediritechnopark.com/webhook/store-production/pay', {
|
const response = await fetch('https://bot.kediritechnopark.com/webhook/store-production/pay', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -129,12 +119,18 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
if (response.ok && result?.qris_dynamic) {
|
if (paymentMethod === 'QRIS' && result?.qris_dynamic) {
|
||||||
setQrisData(result.qris_dynamic);
|
setQrisData(result.qris_dynamic);
|
||||||
setValue(result.total_price);
|
setValue(result.total_price);
|
||||||
|
} else if (paymentMethod === 'Bank Transfer' && result?.bank_account) {
|
||||||
|
setTransferData(result);
|
||||||
|
setValue(result.total_price);
|
||||||
} else {
|
} else {
|
||||||
alert(`Gagal mendapatkan QRIS: ${result?.error || 'Unknown error'}`);
|
alert(`Gagal memproses pembayaran: ${result?.error || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(`Request gagal: ${result?.error || 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Network error:', error);
|
console.error('Network error:', error);
|
||||||
@@ -148,116 +144,146 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
|||||||
if (transactionSuccess) {
|
if (transactionSuccess) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
window.location.href = redirect_uri;
|
window.location.href = redirect_uri;
|
||||||
}, 10000); // 10 detik = 10000 ms
|
}, 10000);
|
||||||
|
|
||||||
// Bersihkan timer kalau komponen unmount atau transactionSuccess berubah
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [transactionSuccess]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div className={styles.page}>
|
||||||
style={{
|
|
||||||
minHeight: '100vh',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: '1rem',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={styles.checkoutCard}>
|
<div className={styles.checkoutCard}>
|
||||||
{/* Product List */}
|
|
||||||
<div className={styles.cartSection}>
|
<div className={styles.cartSection}>
|
||||||
{!qrisData &&
|
{!qrisData && !transferData ? (
|
||||||
<>
|
<>
|
||||||
<h2 className={styles.cartTitle}>Your Cart</h2>
|
<div className={styles.invHeader}>
|
||||||
<ul className={styles.cartList}>
|
|
||||||
{products.map((item, index) => (
|
|
||||||
<li key={index} className={styles.cartItem}>
|
|
||||||
<div className={styles.itemDetails}>
|
|
||||||
<img
|
|
||||||
src={item.image || 'https://via.placeholder.com/60'}
|
|
||||||
alt={item.name}
|
|
||||||
className={styles.productImage}
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<p className={styles.itemText}>{item.name}</p>
|
<h2 className={styles.brand}>
|
||||||
<p className={styles.itemPrice}>Rp{item.price?.toLocaleString('id-ID')}</p>
|
KEDIRI<span className={styles.brandLight}>TECHNOPARK</span>
|
||||||
{item.duration?.hours && (
|
</h2>
|
||||||
<p style={{ fontSize: '0.8rem', color: '#777' }}>
|
<p className={styles.greeting}>
|
||||||
Durasi: {item.duration.hours} jam
|
Hello, {parseJwt(token)?.username || 'User'} <br />
|
||||||
|
Thank you for your order
|
||||||
</p>
|
</p>
|
||||||
)}
|
</div>
|
||||||
|
<div className={styles.orderInfo}>
|
||||||
|
<div className={styles.invoiceLabel}>Invoice</div>
|
||||||
|
<div className={styles.orderMeta}>ORDER #{String(itemIds?.[0] || '').padStart(5, '0')}</div>
|
||||||
|
<div className={styles.orderMeta}>{new Date().toLocaleDateString()}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th className={styles.textRight}>Subtotal</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{products.map((item) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>{item.name}</td>
|
||||||
|
<td>{item.qty ?? 1}</td>
|
||||||
|
<td className={styles.textRight}>
|
||||||
|
Rp{(item.price || 0).toLocaleString('id-ID')}
|
||||||
|
</td>
|
||||||
|
<td className={styles.textRight}>
|
||||||
<button
|
<button
|
||||||
className={styles.removeBtn}
|
className={styles.removeBtn}
|
||||||
onClick={() => handleRemove(item.id)}
|
onClick={() => handleRemove(item.id)}
|
||||||
aria-label={`Remove ${item.name}`}
|
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</td>
|
||||||
|
</tr>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div className={styles.summary}>
|
||||||
|
<div className={styles.summaryRow}>
|
||||||
|
<span>SUBTOTAL</span>
|
||||||
|
<span>Rp{subtotal.toLocaleString('id-ID')}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.summaryRow}>
|
||||||
|
<span>SHIPPING</span>
|
||||||
|
<span>Rp{shipping.toLocaleString('id-ID')}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.summaryRow}>
|
||||||
|
<span>TAX</span>
|
||||||
|
<span>Rp{tax.toLocaleString('id-ID')}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.summaryTotal}>
|
||||||
|
<span>Total</span>
|
||||||
|
<span>Rp{grandTotal.toLocaleString('id-ID')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
) : paymentMethod === 'QRIS' ? (
|
||||||
|
|
||||||
{qrisData && (
|
|
||||||
<>
|
<>
|
||||||
<p>Silahkan scan QRIS ini</p>
|
<p className={styles.qrTitle}>Silakan scan QRIS ini</p>
|
||||||
|
<div className={styles.qrBox}>
|
||||||
<div style={{ marginTop: '2rem', textAlign: 'center', position: 'relative', display: 'inline-block' }}>
|
|
||||||
<QRCodeCanvas value={qrisData} size={256} />
|
<QRCodeCanvas value={qrisData} size={256} />
|
||||||
|
|
||||||
{transactionSuccess && (
|
{transactionSuccess && (
|
||||||
<div className={styles.CheckmarkOverlay}>
|
<div className={styles.CheckmarkOverlay}>
|
||||||
<svg
|
<svg className={styles.Checkmark} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
|
||||||
className={styles.Checkmark}
|
<circle className={styles.CheckmarkCircle} cx="26" cy="26" r="25" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<path className={styles.CheckmarkCheck} d="M14 27l7 7 16-16" />
|
||||||
viewBox="0 0 52 52"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
className={styles.CheckmarkCircle}
|
|
||||||
cx="26"
|
|
||||||
cy="26"
|
|
||||||
r="25"
|
|
||||||
fill="none"
|
|
||||||
stroke="#4BB543"
|
|
||||||
strokeWidth="4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className={styles.CheckmarkCheck}
|
|
||||||
fill="none"
|
|
||||||
stroke="#4BB543"
|
|
||||||
strokeWidth="4"
|
|
||||||
d="M14 27l7 7 16-16"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!transactionSuccess && <h2>Rp{value?.toLocaleString('id-ID')}</h2>}
|
||||||
{!transactionSuccess && (
|
|
||||||
<>
|
|
||||||
<h2>Rp{value?.toLocaleString('id-ID')}</h2>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<div className={styles.transferBox}>
|
||||||
|
<h3 className={styles.transferTitle}>Bank Transfer Information</h3>
|
||||||
|
<div className={styles.transferRow}><span>Bank</span><span>{transferData?.bank_name}</span></div>
|
||||||
|
<div className={styles.transferRow}><span>Account No</span><span>{transferData?.bank_account}</span></div>
|
||||||
|
<div className={styles.transferRow}><span>Account Name</span><span>{transferData?.account_name}</span></div>
|
||||||
|
<div className={styles.transferRow}><span>Total</span><span>Rp{value?.toLocaleString('id-ID')}</span></div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Checkout form */}
|
|
||||||
<div className={styles.checkoutSection}>
|
<div className={styles.checkoutSection}>
|
||||||
<div>
|
<div>
|
||||||
<h2 className={styles.checkoutTitle}>
|
<h2 className={styles.checkoutTitle}>
|
||||||
Rp{qrisData ? value : products.reduce((acc, item) => acc + (item.price || 0), 0).toLocaleString('id-ID')}
|
Rp{(qrisData || transferData ? value : grandTotal).toLocaleString('id-ID')}
|
||||||
</h2>
|
</h2>
|
||||||
<div style={{ marginBottom: '1.5rem' }}>
|
|
||||||
|
<div className={styles.paymentInfo}>
|
||||||
|
<p className={styles.paymentHeading}>PAYMENT INFORMATION</p>
|
||||||
|
<label className={styles.radioLabel}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="payment"
|
||||||
|
value="Bank Transfer"
|
||||||
|
checked={paymentMethod === 'Bank Transfer'}
|
||||||
|
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||||
|
/>
|
||||||
|
Bank Transfer
|
||||||
|
</label>
|
||||||
|
<label className={styles.radioLabel}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="payment"
|
||||||
|
value="QRIS"
|
||||||
|
checked={paymentMethod === 'QRIS'}
|
||||||
|
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||||
|
/>
|
||||||
|
QRIS
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputGroup}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={parseJwt(token)?.username || 'User'}
|
placeholder={parseJwt(token)?.username || 'User'}
|
||||||
@@ -265,18 +291,18 @@ const Checkout = ({ socketId, transactionSuccess }) => {
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={styles.paymentBtn}
|
className={styles.paymentBtn}
|
||||||
onClick={handlePay}
|
onClick={handlePay}
|
||||||
disabled={loadingPay || qrisData !== null}
|
disabled={loadingPay || qrisData !== null || transferData !== null}
|
||||||
>
|
>
|
||||||
{loadingPay ? 'Processing...' : qrisData ? 'Payment QRIS Generated' : 'Complete Payment'}
|
{loadingPay ? 'Processing...' : (qrisData || transferData) ? 'Payment Created' : 'PAY'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className={styles.footerText}>
|
<div className={styles.footerText}>
|
||||||
Powered by <span className={styles.footerHighlight}>KEDIRITECHNOPARK</span> •{' '}
|
Powered by <span className={styles.footerHighlight}>KEDIRITECHNOPARK</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 {
|
.checkoutCard {
|
||||||
border-radius: 1rem; /* rounded-2xl */
|
border-radius: 12px;
|
||||||
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
|
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;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column; /* default mobile: vertical stack */
|
flex-direction: column;
|
||||||
max-width: 28rem; /* max-w-md */
|
max-width: 64rem; /* desktop width */
|
||||||
margin-left: auto;
|
width: 100%;
|
||||||
margin-right: auto;
|
background: #fff;
|
||||||
flex-grow: 1; /* agar bisa melebar dalam container flex */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cartSection, .checkoutSection {
|
/* Sections */
|
||||||
flex: 1; /* agar keduanya bisa melebar seimbang */
|
.cartSection, .checkoutSection { flex: 1; }
|
||||||
}
|
|
||||||
|
|
||||||
.cartSection {
|
.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 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkoutSection {
|
|
||||||
padding: 2rem; /* p-8 */
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-grow: 1;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkoutTitle {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1F2937;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputNote {
|
|
||||||
width: 100%; /* tetap full width agar input memenuhi container */
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border: 1px solid #D1D5DB; /* gray-300 */
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
|
||||||
box-sizing: border-box; /* pastikan padding masuk ke width */
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputNote:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3B82F6; /* blue-500 */
|
|
||||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.paymentBtn {
|
|
||||||
width: 100%; /* tombol harus full lebar */
|
|
||||||
background-color: #2563EB; /* blue-600 */
|
|
||||||
color: white;
|
|
||||||
font-weight: 700;
|
|
||||||
padding: 0.75rem 0;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
font-size: 1.125rem; /* text-lg */
|
|
||||||
cursor: pointer;
|
|
||||||
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 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.footerText {
|
|
||||||
font-size: 0.75rem; /* text-xs */
|
|
||||||
color: #6B7280; /* gray-500 */
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footerLink {
|
|
||||||
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;
|
padding: 2rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkoutSection {
|
.checkoutSection {
|
||||||
padding: 2rem 3rem;
|
padding: 2rem 3rem;
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive layout for small screens */
|
/* Header invoice */
|
||||||
@media (max-width: 640px) {
|
.invHeader {
|
||||||
.checkoutCard {
|
display: flex;
|
||||||
max-width: 100%;
|
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: 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%;
|
||||||
|
padding: .75rem 1rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: .5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.inputNote:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 0 0 2px rgba(59,130,246,.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paymentBtn {
|
||||||
|
width: 100%;
|
||||||
|
background: #8b5cf6;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: .75rem 0;
|
||||||
|
border-radius: .5rem;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
border: none;
|
||||||
|
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: .75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.footerHighlight { font-weight: 700; color: #374151; }
|
||||||
|
|
||||||
|
/* QR view */
|
||||||
|
.qrTitle { font-size: .95rem; color: #374151; }
|
||||||
|
.qrBox {
|
||||||
|
margin-top: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkmark animation (as-is from your logic) */
|
||||||
.CheckmarkOverlay {
|
.CheckmarkOverlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
background: rgba(255, 255, 255, 0.5);
|
background: rgba(255,255,255,.5);
|
||||||
width: 130px;
|
width: 130px;
|
||||||
height: 130px;
|
height: 130px;
|
||||||
display: flex;
|
display: flex; align-items: center; justify-content: center;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
.Checkmark { width: 100px; height: 100px; display: block; }
|
||||||
.CheckmarkSvg {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CheckmarkCircle {
|
.CheckmarkCircle {
|
||||||
fill: none;
|
fill: none; stroke: #4BB543; stroke-width: 4;
|
||||||
stroke: #4BB543;
|
stroke-dasharray: 157; stroke-dashoffset: 157;
|
||||||
stroke-width: 4;
|
transform: rotate(-90deg); transform-origin: center;
|
||||||
stroke-dasharray: 157; /* 2πr where r = 25 */
|
|
||||||
stroke-dashoffset: 157;
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
transform-origin: center;
|
|
||||||
animation: CircleFill 1s ease forwards;
|
animation: CircleFill 1s ease forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CheckmarkCheck {
|
.CheckmarkCheck {
|
||||||
fill: none;
|
fill: none; stroke: #4BB543; stroke-width: 4;
|
||||||
stroke: #4BB543;
|
stroke-dasharray: 48; stroke-dashoffset: 48;
|
||||||
stroke-width: 4;
|
animation: DrawCheck .5s ease forwards; animation-delay: 1s;
|
||||||
stroke-dasharray: 48;
|
|
||||||
stroke-dashoffset: 48;
|
|
||||||
animation: DrawCheck 0.5s ease forwards;
|
|
||||||
animation-delay: 1s;
|
|
||||||
}
|
}
|
||||||
|
@keyframes CircleFill { to { stroke-dashoffset: 0; } }
|
||||||
|
@keyframes DrawCheck { to { stroke-dashoffset: 0; } }
|
||||||
|
|
||||||
/* Circle fills in a clockwise motion */
|
/* Responsive */
|
||||||
@keyframes CircleFill {
|
@media (min-width: 768px) {
|
||||||
to {
|
.checkoutCard { flex-direction: row; }
|
||||||
stroke-dashoffset: 0;
|
.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; }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user