This commit is contained in:
everythingonblack
2025-08-18 13:19:39 +07:00
parent d4073506bd
commit ec2eb0d619
2 changed files with 438 additions and 440 deletions

View File

@@ -1,287 +1,313 @@
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) {
try { try {
const base64Url = token.split('.')[1]; const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent( const jsonPayload = decodeURIComponent(
atob(base64) atob(base64)
.split('') .split('')
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('') .join('')
); );
return JSON.parse(jsonPayload); return JSON.parse(jsonPayload);
} catch (e) { } catch (e) {
return null; return null;
} }
} }
const Checkout = ({ socketId, transactionSuccess }) => { const Checkout = ({ socketId, transactionSuccess }) => {
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
const [itemIds, setItemIds] = useState(null); const [itemIds, setItemIds] = useState(null);
const [token, setToken] = useState(null); const [token, setToken] = useState(null);
const [qrisData, setQrisData] = useState(null); const [qrisData, setQrisData] = useState(null);
const [value, setValue] = useState(null); const [transferData, setTransferData] = useState(null);
const [loadingPay, setLoadingPay] = useState(false); const [value, setValue] = useState(null);
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('');
useEffect(() => { const [paymentMethod, setPaymentMethod] = useState('QRIS');
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); 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) { if (!itemsIdString) {
window.location.href = redirect_failed; window.location.href = urlParams.get('redirect_failed') || '/';
return; 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 { useEffect(() => {
const parsedIds = JSON.parse(itemsIdString); if (transactionSuccess) {
if (!Array.isArray(parsedIds) || parsedIds.length === 0) { const timer = setTimeout(() => {
window.location.href = redirect_failed; window.location.href = redirect_uri;
return; }, 10000);
} return () => clearTimeout(timer);
setItemIds(parsedIds); }
} catch (e) { }, [transactionSuccess, redirect_uri]);
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 (
<div
style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '1rem',
boxSizing: 'border-box',
}}
>
<div className={styles.checkoutCard}>
{/* Product List */}
<div className={styles.cartSection}>
{!qrisData &&
<>
<h2 className={styles.cartTitle}>Your Cart</h2>
<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>
<p className={styles.itemText}>{item.name}</p>
<p className={styles.itemPrice}>Rp{item.price?.toLocaleString('id-ID')}</p>
{item.duration?.hours && (
<p style={{ fontSize: '0.8rem', color: '#777' }}>
Durasi: {item.duration.hours} jam
</p>
)}
</div>
</div>
<button
className={styles.removeBtn}
onClick={() => handleRemove(item.id)}
aria-label={`Remove ${item.name}`}
>
&times;
</button>
</li>
))}
</ul>
</>
}
{qrisData && (
<>
<p>Silahkan scan QRIS ini</p>
<div style={{ marginTop: '2rem', textAlign: 'center', position: 'relative', display: 'inline-block' }}>
<QRCodeCanvas value={qrisData} size={256} />
{transactionSuccess && (
<div className={styles.CheckmarkOverlay}>
<svg
className={styles.Checkmark}
xmlns="http://www.w3.org/2000/svg"
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>
</div>
)}
{!transactionSuccess && (
<>
<h2>Rp{value?.toLocaleString('id-ID')}</h2>
</>
)}
</div>
</>
)}
const subtotal = products.reduce((acc, item) => acc + (item.price || 0), 0);
const shipping = 0;
const tax = 0;
const grandTotal = subtotal + shipping + tax;
return (
<div className={styles.page}>
<div className={styles.checkoutCard}>
<div className={styles.cartSection}>
{!qrisData && !transferData ? (
<>
<div className={styles.invHeader}>
<div>
<h2 className={styles.brand}>
KEDIRI<span className={styles.brandLight}>TECHNOPARK</span>
</h2>
<p className={styles.greeting}>
Hello, {parseJwt(token)?.username || 'User'} <br />
Thank you for your order
</p>
</div> </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>
{/* Checkout form */} <table className={styles.table}>
<div className={styles.checkoutSection}> <thead>
<div> <tr>
<h2 className={styles.checkoutTitle}> <th>Item</th>
Rp{qrisData ? value : products.reduce((acc, item) => acc + (item.price || 0), 0).toLocaleString('id-ID')} <th>Quantity</th>
</h2> <th className={styles.textRight}>Subtotal</th>
<div style={{ marginBottom: '1.5rem' }}> <th></th>
<input </tr>
type="text" </thead>
placeholder={parseJwt(token)?.username || 'User'} <tbody>
className={styles.inputNote} {products.map((item) => (
readOnly <tr key={item.id}>
/> <td>{item.name}</td>
</div> <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.paymentBtn} className={styles.removeBtn}
onClick={handlePay} onClick={() => handleRemove(item.id)}
disabled={loadingPay || qrisData !== null}
> >
{loadingPay ? 'Processing...' : qrisData ? 'Payment QRIS Generated' : 'Complete Payment'} &times;
</button> </button>
</div> </td>
</tr>
))}
</tbody>
</table>
{/* Footer */} <div className={styles.summary}>
<div className={styles.footerText}> <div className={styles.summaryRow}>
Powered by <span className={styles.footerHighlight}>KEDIRITECHNOPARK</span> {' '} <span>SUBTOTAL</span>
</div> <span>Rp{subtotal.toLocaleString('id-ID')}</span>
</div> </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' ? (
<>
<p className={styles.qrTitle}>Silakan scan QRIS ini</p>
<div className={styles.qrBox}>
<QRCodeCanvas value={qrisData} size={256} />
{transactionSuccess && (
<div className={styles.CheckmarkOverlay}>
<svg className={styles.Checkmark} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<circle className={styles.CheckmarkCircle} cx="26" cy="26" r="25" />
<path className={styles.CheckmarkCheck} d="M14 27l7 7 16-16" />
</svg>
</div>
)}
{!transactionSuccess && <h2>Rp{value?.toLocaleString('id-ID')}</h2>}
</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> </div>
);
<div className={styles.checkoutSection}>
<div>
<h2 className={styles.checkoutTitle}>
Rp{(qrisData || transferData ? value : grandTotal).toLocaleString('id-ID')}
</h2>
<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
type="text"
placeholder={parseJwt(token)?.username || 'User'}
className={styles.inputNote}
readOnly
/>
</div>
<button
className={styles.paymentBtn}
onClick={handlePay}
disabled={loadingPay || qrisData !== null || transferData !== null}
>
{loadingPay ? 'Processing...' : (qrisData || transferData) ? 'Payment Created' : 'PAY'}
</button>
</div>
<div className={styles.footerText}>
Powered by <span className={styles.footerHighlight}>KEDIRITECHNOPARK</span>
</div>
</div>
</div>
</div>
);
}; };
export default Checkout; export default Checkout;

View File

@@ -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 */ padding: 2rem;
background-color: #ececec; /* gray-50 */ background: #ffffff;
border-bottom: 1px solid #F3F4F6; /* border-gray-100 */ border-bottom: 1px solid #f3f4f6;
}
.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 { .checkoutSection {
padding: 2rem; /* p-8 */ padding: 2rem 3rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; 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 { .checkoutTitle {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 700;
color: #1F2937; color: #111827;
margin-bottom: 1.5rem; 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 { .inputNote {
width: 100%; /* tetap full width agar input memenuhi container */ width: 100%;
padding: 0.75rem 1rem; padding: .75rem 1rem;
border: 1px solid #D1D5DB; /* gray-300 */ border: 1px solid #d1d5db;
border-radius: 0.5rem; border-radius: .5rem;
font-size: 1rem; font-size: 1rem;
transition: box-shadow 0.2s ease, border-color 0.2s ease; box-sizing: border-box;
box-sizing: border-box; /* pastikan padding masuk ke width */
} }
.inputNote:focus { .inputNote:focus {
outline: none; outline: none;
border-color: #3B82F6; /* blue-500 */ border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5); box-shadow: 0 0 0 2px rgba(59,130,246,.4);
} }
.paymentBtn { .paymentBtn {
width: 100%; /* tombol harus full lebar */ width: 100%;
background-color: #2563EB; /* blue-600 */ background: #8b5cf6;
color: white; color: #fff;
font-weight: 700; font-weight: 700;
padding: 0.75rem 0; padding: .75rem 0;
border-radius: 0.5rem; border-radius: .5rem;
font-size: 1.125rem; /* text-lg */ font-size: 1.05rem;
cursor: pointer;
border: none; border: none;
box-shadow: 0 4px 6px rgba(37, 99, 235, 0.5); cursor: pointer;
transition: background-color 0.2s ease; box-shadow: 0 4px 6px rgba(139,92,246,.45);
} transition: background-color .2s ease;
.paymentBtn:hover {
background-color: #1D4ED8; /* blue-700 */
} }
.paymentBtn:hover { background: #7c3aed; }
/* Footer */
.footerText { .footerText {
font-size: 0.75rem; /* text-xs */ font-size: .75rem;
color: #6B7280; /* gray-500 */ color: #6b7280;
text-align: center; text-align: center;
margin-top: 2rem; 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; 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 { .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; }
}
} }
@media (max-width: 767px) {
/* Checkmark is drawn after circle is full */ .checkoutCard { flex-direction: column; }
@keyframes DrawCheck {
to {
stroke-dashoffset: 0;
}
} }