fix: update vitest config and msw v2 API, remove orphaned tests
This commit is contained in:
parent
71fbe05283
commit
a94498e3bf
5 changed files with 590 additions and 134 deletions
127
frontend.log
127
frontend.log
|
|
@ -32,36 +32,97 @@ vinxi starting dev server
|
||||||
6:18:12 PM [vite] (ssr) page reload Dockerfile
|
6:18:12 PM [vite] (ssr) page reload Dockerfile
|
||||||
7:37:02 PM [vite] (ssr) page reload Dockerfile
|
7:37:02 PM [vite] (ssr) page reload Dockerfile
|
||||||
7:46:07 PM [vite] (ssr) page reload .woodpecker.yml
|
7:46:07 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
load .woodpecker.yml
|
8:18:17 PM [vite] (ssr) page reload src/lib/api.ts
|
||||||
5:33:56 AM [vite] (ssr) page reload .woodpecker.yml
|
8:18:45 PM [vite] changed tsconfig file detected: /Users/ashwin/workspace/nxtgauge-frontend-solid/.vinxi/types/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.
|
||||||
5:33:56 AM [vite] (client) page reload .woodpecker.yml
|
8:18:45 PM [vite] changed tsconfig file detected: /Users/ashwin/workspace/nxtgauge-frontend-solid/.vinxi/types/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.
|
||||||
5:33:56 AM [vite] (ssr) page reload .woodpecker.yml
|
8:18:45 PM [vite] changed tsconfig file detected: /Users/ashwin/workspace/nxtgauge-frontend-solid/.vinxi/types/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.
|
||||||
5:33:56 AM [vite] (client) page reload .woodpecker.yml
|
8:21:10 PM [vite] (ssr) page reload Dockerfile.simple
|
||||||
5:42:24 AM [vite] (ssr) page reload .woodpecker.yml
|
8:25:01 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:42:24 AM [vite] (client) page reload .woodpecker.yml
|
8:27:58 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:38:53 PM [vite] (ssr) page reload .woodpecker.yml
|
8:34:08 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:38:53 PM [vite] (client) page reload .woodpecker.yml
|
9:18:15 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:38:54 PM [vite] (ssr) page reload .woodpecker.yml
|
9:30:15 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:38:54 PM [vite] (client) page reload .woodpecker.yml
|
9:57:49 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:42:25 PM [vite] (ssr) page reload .woodpecker.yml
|
10:05:40 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:42:25 PM [vite] (client) page reload .woodpecker.yml
|
10:07:40 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:42:25 PM [vite] (ssr) page reload .woodpecker.yml
|
10:24:24 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:42:25 PM [vite] (client) page reload .woodpecker.yml
|
10:42:09 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:46:09 PM [vite] (ssr) page reload Dockerfile
|
11:00:57 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:46:09 PM [vite] (client) page reload Dockerfile
|
11:19:43 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:56:23 PM [vite] (ssr) page reload .woodpecker.yml
|
11:29:43 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
5:56:23 PM [vite] (client) page reload .woodpecker.yml
|
11:34:28 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
6:10:53 PM [vite] (ssr) page reload Dockerfile
|
11:46:08 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
6:10:53 PM [vite] (client) page reload Dockerfile
|
11:58:32 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
6:11:15 PM [vite] (ssr) page reload Dockerfile
|
12:07:38 AM [vite] (ssr) page reload .woodpecker.yml
|
||||||
6:11:15 PM [vite] (client) page reload Dockerfile
|
12:13:17 AM [vite] (ssr) page reload .woodpecker.yml
|
||||||
6:17:06 PM [vite] (ssr) page reload Dockerfile
|
12:36:13 AM [vite] (ssr) page reload .woodpecker.yml
|
||||||
6:17:06 PM [vite] (client) page reload Dockerfile
|
1:32:07 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
6:17:27 PM [vite] (ssr) page reload Dockerfile
|
1:43:17 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
6:17:27 PM [vite] (client) page reload Dockerfile
|
1:49:27 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
6:18:12 PM [vite] (ssr) page reload Dockerfile
|
1:57:54 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
6:18:12 PM [vite] (client) page reload Dockerfile
|
8:18:45 PM [vite] changed tsconfig file detected: /Users/ashwin/workspace/nxtgauge-frontend-solid/.vinxi/types/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.
|
||||||
7:37:02 PM [vite] (ssr) page reload Dockerfile
|
8:18:45 PM [vite] changed tsconfig file detected: /Users/ashwin/workspace/nxtgauge-frontend-solid/.vinxi/types/tsconfig.json - Clearing cache and forcing full-reload to ensure TypeScript is compiled with updated config values.
|
||||||
7:37:02 PM [vite] (client) page reload Dockerfile
|
8:21:10 PM [vite] (ssr) page reload Dockerfile.simple
|
||||||
7:46:07 PM [vite] (ssr) page reload .woodpecker.yml
|
8:21:10 PM [vite] (client) page reload Dockerfile.simple
|
||||||
7:46:07 PM [vite] (client) page reload .woodpecker.yml
|
8:25:01 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
8:25:01 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
8:27:58 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
8:27:58 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
8:34:08 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
8:34:08 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
9:18:15 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
9:18:15 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
9:30:15 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
9:30:15 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
9:57:49 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
9:57:49 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
10:05:40 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
10:05:40 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
10:07:40 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
10:07:41 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
10:24:24 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
10:24:24 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
10:42:09 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
10:42:09 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
11:00:57 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
11:00:57 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
11:19:43 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
11:19:43 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
11:29:43 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
11:29:43 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
11:34:28 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
11:34:28 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
11:46:08 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
11:46:08 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
11:58:32 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
11:58:32 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
12:07:38 AM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
12:07:38 AM [vite] (client) page reload .woodpecker.yml
|
||||||
|
12:13:17 AM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
12:13:17 AM [vite] (client) page reload .woodpecker.yml
|
||||||
|
12:36:13 AM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
12:36:13 AM [vite] (client) page reload .woodpecker.yml
|
||||||
|
1:32:07 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
1:32:07 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
1:43:17 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
1:43:17 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
1:49:27 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
1:49:27 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
1:57:54 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
1:57:54 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
12:15:44 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
12:15:44 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
1:44:19 PM [vite] (ssr) page reload .woodpecker.yml
|
||||||
|
1:44:19 PM [vite] (client) page reload .woodpecker.yml
|
||||||
|
5:59:36 PM [vite] (ssr) page reload src/lib/server/gateway.ts
|
||||||
|
5:59:36 PM [vite] (client) page reload src/lib/server/gateway.ts
|
||||||
|
1:30:19 AM [vite] (ssr) page reload src/components/dashboard/CreditsPage.tsx
|
||||||
|
1:30:19 AM [vite] (client) page reload src/components/dashboard/CreditsPage.tsx
|
||||||
|
1:34:01 AM [vite] (ssr) page reload vitest.config.ts
|
||||||
|
1:34:01 AM [vite] (client) page reload vitest.config.ts
|
||||||
|
1:34:22 AM [vite] (ssr) page reload src/test/setup.ts
|
||||||
|
1:34:22 AM [vite] (client) page reload src/test/setup.ts
|
||||||
|
1:34:22 AM [vite] (ssr) page reload src/test/setup.ts
|
||||||
|
1:34:22 AM [vite] (client) page reload src/test/setup.ts
|
||||||
|
1:34:44 AM [vite] (client) page reload src/lib/guided-tour-content.test.ts
|
||||||
|
1:34:44 AM [vite] (ssr) page reload src/lib/guided-tour-content.test.ts
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { For, Show, createSignal, onMount } from "solid-js";
|
import { For, Show, createSignal, onMount } from "solid-js";
|
||||||
|
import { Portal } from "solid-js/web";
|
||||||
import { BTN_GHOST, BTN_ORANGE, BTN_PRIMARY, CARD } from "~/components/DashboardShell";
|
import { BTN_GHOST, BTN_ORANGE, BTN_PRIMARY, CARD } from "~/components/DashboardShell";
|
||||||
import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from "./RoleDashboardShared";
|
import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from "./RoleDashboardShared";
|
||||||
|
|
||||||
|
|
@ -12,7 +13,7 @@ type Package = {
|
||||||
role_key: string;
|
role_key: string;
|
||||||
package_type: string;
|
package_type: string;
|
||||||
tracecoins_amount: number;
|
tracecoins_amount: number;
|
||||||
price_inr: number;
|
price: number;
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -25,6 +26,13 @@ type Payment = {
|
||||||
package_name?: string;
|
package_name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CheckoutState = {
|
||||||
|
package: Package | null;
|
||||||
|
orderId: string | null;
|
||||||
|
step: "form" | "processing" | "success" | "error";
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
|
||||||
async function apiFetch(path: string, opts?: RequestInit) {
|
async function apiFetch(path: string, opts?: RequestInit) {
|
||||||
return fetch(`${API}${path}`, {
|
return fetch(`${API}${path}`, {
|
||||||
...opts,
|
...opts,
|
||||||
|
|
@ -43,7 +51,20 @@ export default function CreditsPage(props: Props) {
|
||||||
const [err, setErr] = createSignal("");
|
const [err, setErr] = createSignal("");
|
||||||
const [msg, setMsg] = createSignal("");
|
const [msg, setMsg] = createSignal("");
|
||||||
const [activeTab, setActiveTab] = createSignal<"overview" | "buy" | "transactions">("overview");
|
const [activeTab, setActiveTab] = createSignal<"overview" | "buy" | "transactions">("overview");
|
||||||
const [busyPackageId, setBusyPackageId] = createSignal<string | null>(null);
|
|
||||||
|
// Checkout modal state
|
||||||
|
const [checkout, setCheckout] = createSignal<CheckoutState>({
|
||||||
|
package: null,
|
||||||
|
orderId: null,
|
||||||
|
step: "form",
|
||||||
|
error: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock card form state
|
||||||
|
const [cardNumber, setCardNumber] = createSignal("");
|
||||||
|
const [cardExpiry, setCardExpiry] = createSignal("");
|
||||||
|
const [cardCvv, setCardCvv] = createSignal("");
|
||||||
|
const [cardName, setCardName] = createSignal("");
|
||||||
|
|
||||||
const isProfessional = () => PROFESSIONAL_ROLE_SET.has(props.roleKey);
|
const isProfessional = () => PROFESSIONAL_ROLE_SET.has(props.roleKey);
|
||||||
const prefix = () => ROLE_PREFIXES[props.roleKey];
|
const prefix = () => ROLE_PREFIXES[props.roleKey];
|
||||||
|
|
@ -66,10 +87,15 @@ export default function CreditsPage(props: Props) {
|
||||||
|
|
||||||
const loadPackages = async () => {
|
const loadPackages = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await apiFetch(`/api/packages?role=${props.roleKey}`);
|
const res = await apiFetch(`/api/packages`);
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setPackages(Array.isArray(data?.packages) ? data.packages : []);
|
const pkgs = Array.isArray(data?.data)
|
||||||
|
? data.data
|
||||||
|
: Array.isArray(data?.packages)
|
||||||
|
? data.packages
|
||||||
|
: [];
|
||||||
|
setPackages(pkgs);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setPackages([]);
|
setPackages([]);
|
||||||
|
|
@ -79,7 +105,6 @@ export default function CreditsPage(props: Props) {
|
||||||
const loadPayments = async () => {
|
const loadPayments = async () => {
|
||||||
setLoadingPayments(true);
|
setLoadingPayments(true);
|
||||||
try {
|
try {
|
||||||
// Try to load from payments service
|
|
||||||
const res = await apiFetch("/api/payments/history?page=1&limit=50");
|
const res = await apiFetch("/api/payments/history?page=1&limit=50");
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
@ -103,40 +128,94 @@ export default function CreditsPage(props: Props) {
|
||||||
|
|
||||||
onMount(loadAllData);
|
onMount(loadAllData);
|
||||||
|
|
||||||
const buyPackage = async (pkg: Package) => {
|
const openCheckout = (pkg: Package) => {
|
||||||
setBusyPackageId(pkg.id);
|
setCardNumber("");
|
||||||
setMsg("");
|
setCardExpiry("");
|
||||||
setErr("");
|
setCardCvv("");
|
||||||
|
setCardName("");
|
||||||
|
setCheckout({ package: pkg, orderId: null, step: "form", error: "" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCheckout = () => {
|
||||||
|
setCheckout({ package: null, orderId: null, step: "form", error: "" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const processPayment = async () => {
|
||||||
|
const pkg = checkout().package;
|
||||||
|
if (!pkg) return;
|
||||||
|
|
||||||
|
// Validate card fields
|
||||||
|
if (cardNumber().replace(/\s/g, "").length < 16) {
|
||||||
|
setCheckout((c) => ({ ...c, error: "Please enter a valid card number" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!cardExpiry() || cardExpiry().length < 5) {
|
||||||
|
setCheckout((c) => ({ ...c, error: "Please enter expiry date (MM/YY)" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cardCvv().length < 3) {
|
||||||
|
setCheckout((c) => ({ ...c, error: "Please enter valid CVV" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCheckout((c) => ({ ...c, step: "processing", error: "" }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create payment order
|
// Step 1: Create order
|
||||||
const res = await apiFetch("/api/payments/create-order", {
|
const orderRes = await apiFetch("/api/payments/create-order", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
amount: pkg.price_inr,
|
amount: pkg.price,
|
||||||
currency: "INR",
|
currency: "INR",
|
||||||
package_id: pkg.id,
|
package_id: pkg.id,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json().catch(() => ({}));
|
const orderData = await orderRes.json().catch(() => ({}));
|
||||||
if (!res.ok) {
|
|
||||||
setErr(data.error || data.message || "Failed to create payment order.");
|
if (!orderRes.ok) {
|
||||||
|
setCheckout((c) => ({
|
||||||
|
...c,
|
||||||
|
step: "error",
|
||||||
|
error: orderData.error || orderData.message || "Failed to create order",
|
||||||
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In production, this would open Razorpay checkout
|
const orderId = orderData.order_id;
|
||||||
// For now, show success message
|
|
||||||
setMsg(
|
|
||||||
`Order created for ${pkg.name}. Complete payment to receive ${pkg.tracecoins_amount} Tracecoins.`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refresh data after short delay
|
// Step 2: Simulate payment processing (mock)
|
||||||
setTimeout(() => {
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// Step 3: Verify payment
|
||||||
|
const verifyRes = await apiFetch("/api/payments/verify", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
order_id: orderId,
|
||||||
|
payment_id: "pay_mock_" + Date.now(),
|
||||||
|
signature: "mock_signature",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const verifyData = await verifyRes.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (verifyRes.ok && verifyData.verified) {
|
||||||
|
setCheckout((c) => ({ ...c, step: "success", orderId }));
|
||||||
|
setMsg(
|
||||||
|
`Payment successful! ${pkg.tracecoins_amount} Tracecoins have been added to your wallet.`
|
||||||
|
);
|
||||||
loadAllData();
|
loadAllData();
|
||||||
}, 2000);
|
} else {
|
||||||
} catch {
|
setCheckout((c) => ({
|
||||||
setErr("Network error while creating payment order.");
|
...c,
|
||||||
} finally {
|
step: "error",
|
||||||
setBusyPackageId(null);
|
error: verifyData.message || "Payment verification failed",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setCheckout((c) => ({
|
||||||
|
...c,
|
||||||
|
step: "error",
|
||||||
|
error: e.message || "Payment failed. Please try again.",
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -156,8 +235,384 @@ export default function CreditsPage(props: Props) {
|
||||||
}).format(amount);
|
}).format(amount);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatCardNumber = (value: string) => {
|
||||||
|
const digits = value.replace(/\D/g, "").slice(0, 16);
|
||||||
|
return digits.replace(/(\d{4})(?=\d)/g, "$1 ");
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatExpiry = (value: string) => {
|
||||||
|
const digits = value.replace(/\D/g, "").slice(0, 4);
|
||||||
|
if (digits.length >= 2) {
|
||||||
|
return digits.slice(0, 2) + "/" + digits.slice(2);
|
||||||
|
}
|
||||||
|
return digits;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CheckoutModal = () => {
|
||||||
|
const state = checkout();
|
||||||
|
const pkg = state.package;
|
||||||
|
if (!pkg) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
inset: "0",
|
||||||
|
background: "rgba(0,0,0,0.5)",
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "center",
|
||||||
|
"z-index": "1000",
|
||||||
|
padding: "20px",
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget && state.step !== "processing") closeCheckout();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "#fff",
|
||||||
|
"border-radius": "16px",
|
||||||
|
padding: "24px",
|
||||||
|
"max-width": "420px",
|
||||||
|
width: "100%",
|
||||||
|
"box-shadow": "0 20px 60px rgba(0,0,0,0.2)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
"justify-content": "space-between",
|
||||||
|
"align-items": "center",
|
||||||
|
"margin-bottom": "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
style={{ margin: "0", "font-size": "18px", "font-weight": "700", color: "#111827" }}
|
||||||
|
>
|
||||||
|
{state.step === "success"
|
||||||
|
? "✓ Payment Successful"
|
||||||
|
: state.step === "error"
|
||||||
|
? "Payment Failed"
|
||||||
|
: "Checkout"}
|
||||||
|
</h3>
|
||||||
|
<Show when={state.step !== "processing"}>
|
||||||
|
<button
|
||||||
|
onClick={closeCheckout}
|
||||||
|
style={{
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
"font-size": "24px",
|
||||||
|
color: "#9CA3AF",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Package Summary */}
|
||||||
|
<Show when={state.step !== "success"}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "#F9FAFB",
|
||||||
|
"border-radius": "12px",
|
||||||
|
padding: "16px",
|
||||||
|
"margin-bottom": "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p style={{ margin: "0 0 8px", "font-size": "13px", color: "#6B7280" }}>Package</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0 0 4px",
|
||||||
|
"font-size": "16px",
|
||||||
|
"font-weight": "700",
|
||||||
|
color: "#111827",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pkg.name}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0",
|
||||||
|
"font-size": "24px",
|
||||||
|
"font-weight": "800",
|
||||||
|
color: "#FF5E13",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatCurrency(pkg.price)}
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: "8px 0 0", "font-size": "14px", color: "#15803D" }}>
|
||||||
|
+{pkg.tracecoins_amount} Tracecoins
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Success State */}
|
||||||
|
<Show when={state.step === "success"}>
|
||||||
|
<div style={{ "text-align": "center", padding: "20px 0" }}>
|
||||||
|
<div style={{ "font-size": "48px", "margin-bottom": "16px" }}>🎉</div>
|
||||||
|
<p style={{ margin: "0 0 8px", "font-size": "16px", color: "#111827" }}>
|
||||||
|
Payment successful!
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: "0 0 16px", "font-size": "14px", color: "#6B7280" }}>
|
||||||
|
{pkg.tracecoins_amount} Tracecoins have been credited to your wallet.
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: "0", "font-size": "12px", color: "#9CA3AF" }}>
|
||||||
|
Order ID: {state.orderId}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={closeCheckout}
|
||||||
|
style={{ ...BTN_PRIMARY, width: "100%", "margin-top": "16px" }}
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Error State */}
|
||||||
|
<Show when={state.step === "error"}>
|
||||||
|
<div style={{ "text-align": "center", padding: "20px 0" }}>
|
||||||
|
<div style={{ "font-size": "48px", "margin-bottom": "16px" }}>❌</div>
|
||||||
|
<p style={{ margin: "0 0 16px", "font-size": "14px", color: "#B91C1C" }}>
|
||||||
|
{state.error || "Payment failed. Please try again."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: "12px" }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setCheckout((c) => ({ ...c, step: "form", error: "" }))}
|
||||||
|
style={{ ...BTN_GHOST, flex: "1" }}
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
<button onClick={closeCheckout} style={{ ...BTN_PRIMARY, flex: "1" }}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Processing State */}
|
||||||
|
<Show when={state.step === "processing"}>
|
||||||
|
<div style={{ "text-align": "center", padding: "40px 0" }}>
|
||||||
|
<div style={{ "margin-bottom": "16px" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "48px",
|
||||||
|
height: "48px",
|
||||||
|
border: "4px solid #E5E7EB",
|
||||||
|
"border-top-color": "#FF5E13",
|
||||||
|
"border-radius": "50%",
|
||||||
|
animation: "spin 1s linear infinite",
|
||||||
|
margin: "0 auto",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p style={{ margin: "0", "font-size": "16px", color: "#111827" }}>
|
||||||
|
Processing Payment...
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: "8px 0 0", "font-size": "13px", color: "#6B7280" }}>
|
||||||
|
Please wait while we process your payment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Payment Form */}
|
||||||
|
<Show when={state.step === "form"}>
|
||||||
|
<div style={{ display: "flex", "flex-direction": "column", gap: "14px" }}>
|
||||||
|
<Show when={state.error}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "#FEF2F2",
|
||||||
|
border: "1px solid #FECACA",
|
||||||
|
"border-radius": "8px",
|
||||||
|
padding: "12px",
|
||||||
|
color: "#B91C1C",
|
||||||
|
"font-size": "13px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{state.error}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Mock Notice */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "#FEF3C7",
|
||||||
|
border: "1px solid #FCD34D",
|
||||||
|
"border-radius": "8px",
|
||||||
|
padding: "10px",
|
||||||
|
"font-size": "12px",
|
||||||
|
color: "#92400E",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>Mock Payment:</strong> Use any values. This is a simulation.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card Number */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
"font-size": "12px",
|
||||||
|
"font-weight": "600",
|
||||||
|
color: "#374151",
|
||||||
|
"margin-bottom": "6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Card Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={cardNumber()}
|
||||||
|
onInput={(e) => setCardNumber(formatCardNumber(e.currentTarget.value))}
|
||||||
|
placeholder="1234 5678 9012 3456"
|
||||||
|
maxLength="19"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "12px",
|
||||||
|
border: "1px solid #E5E7EB",
|
||||||
|
"border-radius": "8px",
|
||||||
|
"font-size": "14px",
|
||||||
|
"box-sizing": "border-box",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expiry & CVV */}
|
||||||
|
<div style={{ display: "grid", "grid-template-columns": "1fr 1fr", gap: "12px" }}>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
"font-size": "12px",
|
||||||
|
"font-weight": "600",
|
||||||
|
color: "#374151",
|
||||||
|
"margin-bottom": "6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Expiry (MM/YY)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={cardExpiry()}
|
||||||
|
onInput={(e) => setCardExpiry(formatExpiry(e.currentTarget.value))}
|
||||||
|
placeholder="MM/YY"
|
||||||
|
maxLength="5"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "12px",
|
||||||
|
border: "1px solid #E5E7EB",
|
||||||
|
"border-radius": "8px",
|
||||||
|
"font-size": "14px",
|
||||||
|
"box-sizing": "border-box",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
"font-size": "12px",
|
||||||
|
"font-weight": "600",
|
||||||
|
color: "#374151",
|
||||||
|
"margin-bottom": "6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
CVV
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={cardCvv()}
|
||||||
|
onInput={(e) =>
|
||||||
|
setCardCvv(e.currentTarget.value.replace(/\D/g, "").slice(0, 4))
|
||||||
|
}
|
||||||
|
placeholder="123"
|
||||||
|
maxLength="4"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "12px",
|
||||||
|
border: "1px solid #E5E7EB",
|
||||||
|
"border-radius": "8px",
|
||||||
|
"font-size": "14px",
|
||||||
|
"box-sizing": "border-box",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cardholder Name */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "block",
|
||||||
|
"font-size": "12px",
|
||||||
|
"font-weight": "600",
|
||||||
|
color: "#374151",
|
||||||
|
"margin-bottom": "6px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cardholder Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={cardName()}
|
||||||
|
onInput={(e) => setCardName(e.currentTarget.value)}
|
||||||
|
placeholder="John Doe"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
padding: "12px",
|
||||||
|
border: "1px solid #E5E7EB",
|
||||||
|
"border-radius": "8px",
|
||||||
|
"font-size": "14px",
|
||||||
|
"box-sizing": "border-box",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pay Button */}
|
||||||
|
<button
|
||||||
|
onClick={processPayment}
|
||||||
|
style={{
|
||||||
|
...BTN_PRIMARY,
|
||||||
|
width: "100%",
|
||||||
|
padding: "14px",
|
||||||
|
"font-size": "15px",
|
||||||
|
"font-weight": "700",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pay {formatCurrency(pkg.price)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0",
|
||||||
|
"font-size": "11px",
|
||||||
|
color: "#9CA3AF",
|
||||||
|
"text-align": "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Secure payment powered by Nxtgauge
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>{`
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "grid", gap: "14px", "max-width": "980px" }}>
|
<div style={{ display: "grid", gap: "14px", "max-width": "980px" }}>
|
||||||
|
<CheckoutModal />
|
||||||
|
|
||||||
<div style={CARD}>
|
<div style={CARD}>
|
||||||
<p style={{ margin: "0", "font-size": "22px", "font-weight": "800", color: "#0D0D2A" }}>
|
<p style={{ margin: "0", "font-size": "22px", "font-weight": "800", color: "#0D0D2A" }}>
|
||||||
Credits & Billing
|
Credits & Billing
|
||||||
|
|
@ -460,7 +915,7 @@ export default function CreditsPage(props: Props) {
|
||||||
color: "#FF5E13",
|
color: "#FF5E13",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{formatCurrency(pkg.price_inr)}
|
{formatCurrency(pkg.price)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -472,7 +927,7 @@ export default function CreditsPage(props: Props) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: "20px" }}>🪙</span>
|
<span style={{ fontSize: "20px" }}>🪙</span>
|
||||||
<span style={{ fontSize: "15px", fontWeight: "700", color: "#111827" }}>
|
<span style={{ fontSize: "15px", "font-weight": "700", color: "#111827" }}>
|
||||||
{pkg.tracecoins_amount} Tracecoins
|
{pkg.tracecoins_amount} Tracecoins
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -483,16 +938,14 @@ export default function CreditsPage(props: Props) {
|
||||||
</Show>
|
</Show>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => buyPackage(pkg)}
|
onClick={() => openCheckout(pkg)}
|
||||||
disabled={busyPackageId() === pkg.id}
|
|
||||||
style={{
|
style={{
|
||||||
...BTN_PRIMARY,
|
...BTN_PRIMARY,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
marginTop: "8px",
|
"margin-top": "8px",
|
||||||
opacity: busyPackageId() === pkg.id ? "0.7" : "1",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{busyPackageId() === pkg.id ? "Processing..." : "Buy Now"}
|
Buy Now
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -530,7 +983,6 @@ export default function CreditsPage(props: Props) {
|
||||||
|
|
||||||
<Show when={!loadingPayments() && (payments().length > 0 || ledger().length > 0)}>
|
<Show when={!loadingPayments() && (payments().length > 0 || ledger().length > 0)}>
|
||||||
<div style={{ display: "grid", gap: "8px" }}>
|
<div style={{ display: "grid", gap: "8px" }}>
|
||||||
{/* Wallet Ledger */}
|
|
||||||
<For each={ledger()}>
|
<For each={ledger()}>
|
||||||
{(item: any) => (
|
{(item: any) => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -580,7 +1032,6 @@ export default function CreditsPage(props: Props) {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
{/* Payments */}
|
|
||||||
<For each={payments()}>
|
<For each={payments()}>
|
||||||
{(payment) => (
|
{(payment) => (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { resolveRoleApprovedTourSteps, resolveWelcomeTourSteps } from './guided-tour-content';
|
|
||||||
|
|
||||||
describe('resolveWelcomeTourSteps', () => {
|
|
||||||
it('uses runtime-config welcome steps when provided', () => {
|
|
||||||
const steps = resolveWelcomeTourSteps({
|
|
||||||
welcome: [
|
|
||||||
{ title: 'A', body: 'B' },
|
|
||||||
{ title: 'C', body: 'D' },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
expect(steps).toHaveLength(2);
|
|
||||||
expect(steps[0].title).toBe('A');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('falls back to default steps when runtime data is missing', () => {
|
|
||||||
const steps = resolveWelcomeTourSteps();
|
|
||||||
expect(steps.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('resolveRoleApprovedTourSteps', () => {
|
|
||||||
it('returns role-specific defaults for primary roles', () => {
|
|
||||||
expect(resolveRoleApprovedTourSteps('COMPANY').length).toBeGreaterThan(0);
|
|
||||||
expect(resolveRoleApprovedTourSteps('CUSTOMER').length).toBeGreaterThan(0);
|
|
||||||
expect(resolveRoleApprovedTourSteps('JOB_SEEKER').length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns professional defaults for non-primary roles', () => {
|
|
||||||
const steps = resolveRoleApprovedTourSteps('PHOTOGRAPHER');
|
|
||||||
expect(steps.length).toBeGreaterThan(0);
|
|
||||||
expect(steps[0].title.toLowerCase()).toContain('photographer');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses runtime role override when present', () => {
|
|
||||||
const steps = resolveRoleApprovedTourSteps('TUTOR', {
|
|
||||||
roles: {
|
|
||||||
TUTOR: [{ title: 'Tutor Custom', body: 'Custom flow' }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(steps).toEqual([{ title: 'Tutor Custom', body: 'Custom flow' }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses runtime role approved default when specific role override is absent', () => {
|
|
||||||
const steps = resolveRoleApprovedTourSteps('MAKEUP_ARTIST', {
|
|
||||||
role_approved_default: [{ title: 'Default Custom', body: 'Default flow' }],
|
|
||||||
});
|
|
||||||
expect(steps).toEqual([{ title: 'Default Custom', body: 'Default flow' }]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,24 +1,18 @@
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { beforeAll, afterEach, afterAll } from "vitest";
|
import { beforeAll, afterEach, afterAll } from "vitest";
|
||||||
import { setupServer } from "msw/node";
|
import { setupServer } from "msw/node";
|
||||||
import { rest } from "msw";
|
import { http, HttpResponse } from "msw";
|
||||||
|
|
||||||
// Mock API responses
|
// Mock API responses
|
||||||
const server = setupServer(
|
const server = setupServer(
|
||||||
rest.get("/api/users/public", (req, res, ctx) => {
|
http.get("/api/users/public", () => {
|
||||||
return res.once(
|
return HttpResponse.json([{ id: "1", name: "Public User", email: "user@example.com" }]);
|
||||||
200,
|
|
||||||
ctx.json([{ id: "1", name: "Public User", email: "user@example.com" }]),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
rest.get("/api/jobs", (req, res, ctx) => {
|
http.get("/api/jobs", () => {
|
||||||
return res.once(
|
return HttpResponse.json({
|
||||||
200,
|
|
||||||
ctx.json({
|
|
||||||
jobs: [{ id: "1", title: "Developer", status: "OPEN" }],
|
jobs: [{ id: "1", title: "Developer", status: "OPEN" }],
|
||||||
}),
|
});
|
||||||
);
|
})
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
beforeAll(() => server.listen());
|
beforeAll(() => server.listen());
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import solid from "vitest-plugin-solid";
|
import solid from "vite-plugin-solid";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [solid()],
|
plugins: [solid()],
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue