Compare commits
10 Commits
8ff57862fc
...
2ea7ce8bf3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ea7ce8bf3 | ||
|
|
aa75247bd0 | ||
|
|
ec2eb0d619 | ||
|
|
d4073506bd | ||
|
|
d89c5a52b8 | ||
|
|
ff0f45456b | ||
|
|
c70b7e1d3a | ||
|
|
ad5e3c7a1c | ||
|
|
659e25dd74 | ||
|
|
8f9ba4eafe |
28
package-lock.json
generated
28
package-lock.json
generated
@@ -18,6 +18,9 @@
|
|||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"cross-env": "^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
@@ -2381,6 +2384,13 @@
|
|||||||
"postcss-selector-parser": "^6.0.10"
|
"postcss-selector-parser": "^6.0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@epic-web/invariant": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||||
@@ -5986,6 +5996,24 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@epic-web/invariant": "^1.0.0",
|
||||||
|
"cross-spawn": "^7.0.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "dist/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "cross-env PORT=3002 react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
@@ -37,5 +37,8 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"cross-env": "^10.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
src/App.css
10
src/App.css
@@ -1,5 +1,15 @@
|
|||||||
.App {
|
.App {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #dfdfdf;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-logo {
|
.App-logo {
|
||||||
|
|||||||
327
src/Checkout.css
327
src/Checkout.css
@@ -1,327 +0,0 @@
|
|||||||
.checkout-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 0 20px rgba(0,0,0,0.2);
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 40px auto;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
|
||||||
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-panel {
|
|
||||||
background-color: #2e2a4a;
|
|
||||||
color: white;
|
|
||||||
flex: 1;
|
|
||||||
padding: 40px 30px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-arrow {
|
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-name {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-name {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-price {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 32px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-image-container {
|
|
||||||
background-color: #b7c4f9;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 300px;
|
|
||||||
max-height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-image-container img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-panel {
|
|
||||||
background-color: white;
|
|
||||||
flex: 1;
|
|
||||||
padding: 40px 30px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apple-pay-button {
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 18px;
|
|
||||||
padding: 15px 0;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
color: #999;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator .line {
|
|
||||||
flex: 1;
|
|
||||||
border: none;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shipping-info h3,
|
|
||||||
.payment-methods h3 {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shipping-info input,
|
|
||||||
.shipping-info select {
|
|
||||||
width: 100%;
|
|
||||||
margin: 8px 0;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.manual-address {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
text-decoration: underline;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 4px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment-methods label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment-methods input[type="radio"] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment-methods .icon {
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 14px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #eee;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment-methods .klarna {
|
|
||||||
background-color: #ff4f8b;
|
|
||||||
color: white;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.payment-methods .ideal {
|
|
||||||
background-color: #f9a825;
|
|
||||||
color: white;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.klarna-subtext {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
margin-left: 30px;
|
|
||||||
margin-top: -6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-button {
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 15px 0;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-button:hover {
|
|
||||||
background-color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: auto;
|
|
||||||
padding-top: 20px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
/* Responsive styles */
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.checkout-container {
|
|
||||||
flex-direction: column;
|
|
||||||
max-width: 100vw;
|
|
||||||
min-height: 100vh;
|
|
||||||
border-radius: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
.left-panel, .right-panel {
|
|
||||||
padding: 32px 16px;
|
|
||||||
min-width: 0;
|
|
||||||
max-width: 100vw;
|
|
||||||
}
|
|
||||||
.left-panel {
|
|
||||||
border-radius: 0 0 12px 12px;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.right-panel {
|
|
||||||
border-radius: 12px 12px 0 0;
|
|
||||||
}
|
|
||||||
.product-image-container {
|
|
||||||
max-width: 220px;
|
|
||||||
max-height: 220px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.checkout-container {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.left-panel, .right-panel {
|
|
||||||
padding: 20px 6vw;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
.product-name {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
.product-price {
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
.apple-pay-button, .pay-button {
|
|
||||||
font-size: 15px;
|
|
||||||
padding: 12px 0;
|
|
||||||
}
|
|
||||||
.shipping-info input,
|
|
||||||
.shipping-info select {
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
}
|
|
||||||
.payment-methods label {
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 8px 8px;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
font-size: 10px;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.checkmark-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
animation: fadeIn 0.5s ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkmark {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke: #4CAF50;
|
|
||||||
stroke-miterlimit: 10;
|
|
||||||
animation: scaleIn 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkmark-circle {
|
|
||||||
stroke-dasharray: 166;
|
|
||||||
stroke-dashoffset: 166;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke: #4CAF50;
|
|
||||||
fill: none;
|
|
||||||
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkmark-check {
|
|
||||||
transform-origin: 50% 50%;
|
|
||||||
stroke-dasharray: 48;
|
|
||||||
stroke-dashoffset: 48;
|
|
||||||
stroke: #4CAF50;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
animation: stroke 0.3s 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes stroke {
|
|
||||||
to {
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scaleIn {
|
|
||||||
0% {
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
479
src/Checkout.js
479
src/Checkout.js
@@ -1,220 +1,271 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import './Checkout.css';
|
import styles from './Checkout.module.css';
|
||||||
import { QRCodeCanvas } from 'qrcode.react';
|
import { QRCodeCanvas } from 'qrcode.react';
|
||||||
|
|
||||||
const Checkout = ({ socketId, transactionSuccess }) => {
|
function parseJwt(token) {
|
||||||
const [qrisData, setQrisData] = useState(null); // QRIS string
|
try {
|
||||||
const [value, setValue] = useState(null); // QRIS value (optional)
|
const base64Url = token.split('.')[1];
|
||||||
const [products, setProducts] = useState([]); // Produk dari itemsId
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
const [loadingProducts, setLoadingProducts] = useState(false);
|
const jsonPayload = decodeURIComponent(
|
||||||
|
atob(base64)
|
||||||
// Helper get cookie value
|
.split('')
|
||||||
const getCookie = (name) => {
|
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
|
||||||
const value = `; ${document.cookie}`;
|
.join('')
|
||||||
const parts = value.split(`; ${name}=`);
|
|
||||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.cookie = "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjMsInVzZXJuYW1lIjoia2VkaXJpdGVjaG5vcGFyayIsImVtYWlsIjoia2VkaXJpdGVjaG5vcGFya0BnbWFpbC5jb20iLCJwcm9maWxlX2RhdGEiOnt9LCJzdWJzY3JpcHRpb25zIjp7InByb2R1Y3RzIjpbeyJpZCI6InByb2RfMDAxIiwibmFtZSI6IktFRElSSVRFQ0hOT1BBUkstQURNSU4ifV19LCJpYXQiOjE3NTMzNTE4MjN9.PyZwtRseT5BLZm82vEdjIF4fgbUO7Mv4G01iJtVS2qg; path=/";
|
|
||||||
document.cookie = "itemsId=" + JSON.stringify([1, 2]) + "; path=/";
|
|
||||||
|
|
||||||
const fetchProducts = async () => {
|
|
||||||
const itemsIdRaw = getCookie('itemsId');
|
|
||||||
if (!itemsIdRaw) return;
|
|
||||||
|
|
||||||
let itemsId = [];
|
|
||||||
try {
|
|
||||||
itemsId = JSON.parse(itemsIdRaw);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Gagal parse itemsId dari cookie:', e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemsId.length === 0) return;
|
|
||||||
|
|
||||||
setLoadingProducts(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const token = getCookie('token');
|
|
||||||
if (!token) {
|
|
||||||
console.warn('Token tidak ditemukan');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
itemsId.forEach(id => params.append('itemsId', id));
|
|
||||||
|
|
||||||
const res = await fetch(`https://bot.kediritechnopark.com/webhook/store-dev/products`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
body: params.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
setProducts(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching products:', error);
|
|
||||||
} finally {
|
|
||||||
setLoadingProducts(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchProducts();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
const handlePay = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
let itemsIdRaw = getCookie('itemsId');
|
|
||||||
let token = getCookie('token');
|
|
||||||
|
|
||||||
if (!itemsIdRaw || !token) {
|
|
||||||
alert("Token atau itemsId tidak ditemukan di cookies.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let itemsId = [];
|
|
||||||
try {
|
|
||||||
itemsId = JSON.parse(itemsIdRaw);
|
|
||||||
} catch (e) {
|
|
||||||
alert("Gagal parsing itemsId.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
itemsId.forEach(id => params.append('itemsId', id));
|
|
||||||
params.append('socketId', socketId);
|
|
||||||
|
|
||||||
|
|
||||||
const response = await fetch('https://bot.kediritechnopark.com/webhook/store-dev/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[0].qris_dynamic) {
|
|
||||||
setQrisData(result[0].qris_dynamic);
|
|
||||||
setValue(result[0].total_price);
|
|
||||||
} else {
|
|
||||||
alert(`Gagal mendapatkan QRIS: ${result?.error || 'Unknown error'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Network error:', error);
|
|
||||||
alert("Terjadi kesalahan jaringan.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="checkout-container">
|
|
||||||
<div className="left-panel">
|
|
||||||
<div className="back-arrow">←</div>
|
|
||||||
<div className="brand-name">Powdur</div>
|
|
||||||
<div className="product-name">Pure kit</div>
|
|
||||||
<div className="product-price">$65.00</div>
|
|
||||||
<div className="product-image-container">
|
|
||||||
<img src="path/to/image.jpg" alt="Powdur Product" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="right-panel">
|
|
||||||
{!qrisData ? (
|
|
||||||
<>
|
|
||||||
<h3>Cart Items</h3>
|
|
||||||
{loadingProducts ? (
|
|
||||||
<p>Loading products...</p>
|
|
||||||
) : products.length === 0 ? (
|
|
||||||
<p>No products found</p>
|
|
||||||
) : (
|
|
||||||
<ul>
|
|
||||||
{products.map((product) => (
|
|
||||||
<li key={product.id || product._id}>
|
|
||||||
{product.name} - ${product.price}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form className="shipping-info" onSubmit={handlePay}>
|
|
||||||
<h3>Shipping information</h3>
|
|
||||||
<input type="email" placeholder="Email" />
|
|
||||||
<input type="text" placeholder="Name" />
|
|
||||||
<select >
|
|
||||||
<option value="United States">United States</option>
|
|
||||||
</select>
|
|
||||||
<input type="text" placeholder="Address" />
|
|
||||||
<a href="#" className="manual-address">Enter address manually</a>
|
|
||||||
|
|
||||||
<div className="payment-methods">
|
|
||||||
<h3>Payment method</h3>
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="payment" value="card" />
|
|
||||||
<span className="icon">💳</span> Card
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="payment" value="bank" />
|
|
||||||
<span className="icon">🏦</span> Bank
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="payment" value="klarna" />
|
|
||||||
<span className="icon klarna">K</span> Klarna
|
|
||||||
<div className="klarna-subtext">Buy now pay later</div>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" name="payment" value="ideal" />
|
|
||||||
<span className="icon ideal">iD</span> iDEAL
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" className="pay-button">Pay</button>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{transactionSuccess ? (
|
|
||||||
<div className="success-section">
|
|
||||||
<div className="checkmark-container">
|
|
||||||
<svg className="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
|
|
||||||
<circle className="checkmark-circle" cx="26" cy="26" r="25" fill="none" />
|
|
||||||
<path className="checkmark-check" fill="none" d="M14 27l7 7 16-16" />
|
|
||||||
</svg>
|
|
||||||
<h2>Payment Successful!</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="qris-section">
|
|
||||||
<h3>Scan QRIS to Pay</h3>
|
|
||||||
<QRCodeCanvas value={qrisData} size={256} />
|
|
||||||
<p className="qris-string">{qrisData}</p>
|
|
||||||
</div>
|
|
||||||
<h1>{value}</h1>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="footer">
|
|
||||||
Powered by <strong>stripe</strong> | Terms | Privacy
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
return JSON.parse(jsonPayload);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimeLeft(ms) {
|
||||||
|
const totalSeconds = Math.max(Math.floor(ms / 1000), 0);
|
||||||
|
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
|
||||||
|
const seconds = String(totalSeconds % 60).padStart(2, '0');
|
||||||
|
return `${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Checkout = ({ socketId, transactionSuccess }) => {
|
||||||
|
const [products, setProducts] = useState([]);
|
||||||
|
const [itemIds, setItemIds] = useState(null);
|
||||||
|
const [token, setToken] = useState(null);
|
||||||
|
|
||||||
|
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 [paymentMethod, setPaymentMethod] = useState('QRIS');
|
||||||
|
|
||||||
|
const [payTimeout, setPayTimeout] = useState(null);
|
||||||
|
const [timeLeft, setTimeLeft] = useState(null);
|
||||||
|
|
||||||
|
const [activeAccordion, setActiveAccordion] = useState('QRIS');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socketId) return;
|
||||||
|
|
||||||
|
let urlParams = new URLSearchParams(window.location.search);
|
||||||
|
let tokenParam = urlParams.get('token');
|
||||||
|
let itemsIdString = urlParams.get('itemsId');
|
||||||
|
|
||||||
|
const handlePay = async () => {
|
||||||
|
setLoadingPay(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newName = urlParams.get('new_name');
|
||||||
|
const setName = urlParams.get('set_name');
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
itemsIdString.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 ${tokenParam}`,
|
||||||
|
},
|
||||||
|
body: params.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
if (result.pay_timeout && result.time_now) {
|
||||||
|
const timeout = new Date(result.pay_timeout).getTime();
|
||||||
|
const now = new Date(result.time_now).getTime();
|
||||||
|
setPayTimeout(timeout);
|
||||||
|
setTimeLeft(timeout - now);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(result.total_price);
|
||||||
|
setQrisData(result.qris_dynamic || null);
|
||||||
|
setTransferData(result);
|
||||||
|
} else {
|
||||||
|
alert(`Request gagal: ${result?.error || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Network error:', error);
|
||||||
|
alert('Terjadi kesalahan jaringan.');
|
||||||
|
} finally {
|
||||||
|
setLoadingPay(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
itemsIdString = JSON.parse(urlParams.get('itemsId'));
|
||||||
|
setRedirect_Uri(urlParams.get('redirect_uri') || '');
|
||||||
|
setRedirect_Failed(urlParams.get('redirect_failed') || '');
|
||||||
|
setToken(tokenParam);
|
||||||
|
|
||||||
|
if (!itemsIdString || !Array.isArray(itemsIdString) || itemsIdString.length === 0) {
|
||||||
|
window.location.href = urlParams.get('redirect_failed') || '/';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setItemIds(itemsIdString);
|
||||||
|
handlePay();
|
||||||
|
}, [socketId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (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]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (transactionSuccess) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
window.location.href = redirect_uri;
|
||||||
|
}, 10000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [transactionSuccess, redirect_uri]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!payTimeout) return;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const remaining = payTimeout - now;
|
||||||
|
setTimeLeft(remaining);
|
||||||
|
if (remaining <= 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
alert('Waktu pembayaran habis.');
|
||||||
|
window.location.href = redirect_failed;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [payTimeout]);
|
||||||
|
|
||||||
|
const subtotal = products.reduce((acc, item) => acc + (item.price || 0), 0);
|
||||||
|
const grandTotal = subtotal;
|
||||||
|
const tax = 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<div className={styles.checkoutCard}>
|
||||||
|
<div className={styles.cartSection}>
|
||||||
|
<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 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>
|
||||||
|
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item</th>
|
||||||
|
<th className={styles.textRight}>Subtotal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{products.map((item) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>{item.name}</td>
|
||||||
|
<td className={styles.textRight}>
|
||||||
|
Rp{(item.price || 0).toLocaleString('id-ID')}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</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>TAX</span>
|
||||||
|
<span>Rp{tax.toLocaleString('id-ID')}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.summaryTotal}>
|
||||||
|
<span>Total</span>
|
||||||
|
<span>Rp{value?.toLocaleString('id-ID')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.checkoutSection}>
|
||||||
|
{(qrisData || transferData) && (
|
||||||
|
<div className="mt-4">
|
||||||
|
{qrisData && (
|
||||||
|
<div className={styles.accordion}>
|
||||||
|
<div
|
||||||
|
className={styles.accordionHeader}
|
||||||
|
onClick={() =>
|
||||||
|
setActiveAccordion(activeAccordion === 'QRIS' ? '' : 'QRIS')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
QRIS Payment
|
||||||
|
</div>
|
||||||
|
{activeAccordion === 'QRIS' && (
|
||||||
|
<div className={styles.accordionBody}>
|
||||||
|
<QRCodeCanvas value={qrisData} size={200} />
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{transferData && (
|
||||||
|
<div className={styles.accordion}>
|
||||||
|
<div
|
||||||
|
className={styles.accordionHeader}
|
||||||
|
onClick={() =>
|
||||||
|
setActiveAccordion(activeAccordion === 'Bank' ? '' : 'Bank')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Bank Transfer
|
||||||
|
</div>
|
||||||
|
{activeAccordion === 'Bank' && (
|
||||||
|
<div className={styles.accordionBody}>
|
||||||
|
<div><strong>Bank:</strong> {transferData?.bank_name}</div>
|
||||||
|
<div><strong>Account No:</strong> {transferData?.bank_account}</div>
|
||||||
|
<div><strong>Account Name:</strong> {transferData?.account_name}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.footerText}>
|
||||||
|
Powered by <span className={styles.footerHighlight}>KEDIRITECHNOPARK</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Checkout;
|
export default Checkout;
|
||||||
|
|||||||
297
src/Checkout.module.css
Normal file
297
src/Checkout.module.css
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
/* Layout wrapper */
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkoutCard {
|
||||||
|
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;
|
||||||
|
max-width: 64rem; /* desktop width */
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.cartSection, .checkoutSection { flex: 1; }
|
||||||
|
|
||||||
|
.cartSection {
|
||||||
|
padding: 2rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkoutSection {
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
.brandLight { font-weight: 500; }
|
||||||
|
.greeting { text-align: left; 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 {
|
||||||
|
text-align: left;
|
||||||
|
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;
|
||||||
|
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 {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: rgba(255,255,255,.5);
|
||||||
|
width: 130px;
|
||||||
|
height: 130px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.Checkmark { width: 100px; height: 100px; display: block; }
|
||||||
|
.CheckmarkCircle {
|
||||||
|
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 .5s ease forwards; animation-delay: 1s;
|
||||||
|
}
|
||||||
|
@keyframes CircleFill { to { stroke-dashoffset: 0; } }
|
||||||
|
@keyframes DrawCheck { to { stroke-dashoffset: 0; } }
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.checkoutCard { flex-direction: row; }
|
||||||
|
.cartSection { border-right: 1px solid #f3f4f6; border-bottom: none; }
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.checkoutCard { flex-direction: column; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectPayment {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectPaymentModern {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml;utf8,<svg fill='gray' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/></svg>");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 1rem center;
|
||||||
|
background-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.selectPaymentModern:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transferBox {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: #f9fafb;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.transferBox span {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.transferTitle {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
.transferRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px dashed #d1d5db;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
.transferRow:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionHeader {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 12px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionHeader:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordionBody {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user