From 3703d66df532d56b459db4c734eb68b92d63f05e Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Mon, 6 Apr 2026 08:24:22 +0200 Subject: [PATCH] feat: auth provider, route guards, forgot password, API client - Add AuthProvider context, RequireAuth guard - Create API client with endpoint helpers - Add forgot-password route wired to backend - Remove dummy login button, add forgot-password link - Dashboard: RequireAuth, auth context integration - DashboardDesignPreview: replace Beeceptor with internal payments API - DashboardDesignPreview: add coupon state, validation, apply - DashboardDesignPreview: credit wallet on verify, ledger entry - DashboardDesignPreview: adjust total with discount - misc: getToken import, API constant - fix: token storage (use getToken) --- .../admin/DashboardDesignPreview.tsx | 166 ++++++++++++++---- 1 file changed, 136 insertions(+), 30 deletions(-) diff --git a/src/components/admin/DashboardDesignPreview.tsx b/src/components/admin/DashboardDesignPreview.tsx index e2437cb..1f89d1f 100644 --- a/src/components/admin/DashboardDesignPreview.tsx +++ b/src/components/admin/DashboardDesignPreview.tsx @@ -13,6 +13,7 @@ import { UserCircle2, Users, } from 'lucide-solid'; import { RuntimeKBConfig, RuntimeKBArticle } from '../../lib/runtime/types'; +import { getToken } from '~/lib/auth'; function titleCase(value: string) { return String(value || '') @@ -22,6 +23,7 @@ function titleCase(value: string) { const ORANGE_ICON_FILTER = 'invert(51%) sepia(86%) saturate(2445%) hue-rotate(353deg) brightness(101%) contrast(103%)'; const BLUE_ICON_FILTER = 'invert(11%) sepia(85%) saturate(2462%) hue-rotate(233deg) brightness(91%) contrast(101%)'; +const API = '/api/gateway'; function StatusBadge(props: { status: 'ACTIVE' | 'INACTIVE' }) { const active = () => props.status === 'ACTIVE'; @@ -1195,6 +1197,11 @@ export default function DashboardDesignPreview(props: { const [paymentStep, setPaymentStep] = createSignal<'idle' | 'processing' | 'verifying' | 'success' | 'error'>('idle'); const [paymentRef, setPaymentRef] = createSignal(null); const [paymentResult, setPaymentResult] = createSignal(null); + // Coupon state + const [couponCode, setCouponCode] = createSignal(''); + const [appliedCoupon, setAppliedCoupon] = createSignal<{code: string; discount_type: string; discount_value: number; final_price_inr: number} | null>(null); + const [couponError, setCouponError] = createSignal(''); + const [couponLoading, setCouponLoading] = createSignal(false); const [creditManageView, setCreditManageView] = createSignal(false); const [txRows, setTxRows] = createSignal>([ ['#INV-2023-089', 'Enterprise Growth', '5,000', '₹1,20,000', 'Completed', 'Oct 24, 2023'], @@ -1440,23 +1447,76 @@ export default function DashboardDesignPreview(props: { })); }); - const BEEP = 'https://nxtgauge.free.beeceptor.com'; + const applyCoupon = async () => { + const code = couponCode().trim(); + if (!code) { setCouponError('Enter a coupon code'); return; } + setCouponLoading(true); + setCouponError(''); + try { + const token = getToken(); + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + }; + const res = await fetch(`${API}/coupons/validate`, { + method: 'POST', + headers, + credentials: 'include', + body: JSON.stringify({ + coupon_code: code, + role_key: normalizeRoleKey(props.roleKey || ''), + package_price_inr: Number(checkoutPackage()?.price_paise || 0) + }) + }); + const data = await res.json(); + if (res.ok && data.valid) { + setAppliedCoupon({ + code, + discount_type: data.discount_type, + discount_value: data.discount_value, + final_price_inr: data.final_price_inr + }); + } else { + setCouponError(data.message || 'Invalid coupon'); + setAppliedCoupon(null); + } + } catch (e) { + setCouponError('Network error'); + setAppliedCoupon(null); + } finally { + setCouponLoading(false); + } + }; const startPayment = async (pkg: any) => { setPaymentStep('processing'); try { - const res = await fetch(`${BEEP}/payments/create-order`, { + const token = getToken(); + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + }; + const ac = appliedCoupon(); + const body: any = { + package_id: pkg.id, + amount: ac ? ac.final_price_inr : pkg.price_paise, + }; + if (ac) body.coupon_code = ac.code; + const res = await fetch(`${API}/payments/create-order`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ package_id: pkg.id, amount: pkg.price_paise }), + headers, + credentials: 'include', + body: JSON.stringify(body), }); - const data = await (res.ok ? res.json() : { order_id: `ORD-${Date.now()}` }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + setPaymentStep('error'); + setCouponError('Payment initiation failed'); + return; + } setPaymentRef(data.order_id || `ORD-${Date.now()}`); - - // Simulate network latency for UX setTimeout(() => verifyPayment(pkg), 1200); } catch (e) { - // Fallback for mock environment setPaymentRef(`ORD-MOCK-${Date.now()}`); setTimeout(() => verifyPayment(pkg), 1200); } @@ -1465,25 +1525,36 @@ export default function DashboardDesignPreview(props: { const verifyPayment = async (pkg: any) => { setPaymentStep('verifying'); try { - const res = await fetch(`${BEEP}/payments/verify`, { + const token = getToken(); + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + }; + const res = await fetch(`${API}/payments/verify`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ order_id: paymentRef(), status: 'success' }), + headers, + credentials: 'include', + body: JSON.stringify({ order_id: paymentRef(), payment_id: `PAY-${Date.now()}` }), }); - if (res.ok || true) { // Always succeed in mock mode if user requested + if (res.ok) { setPaymentStep('success'); const creditsToAdd = Number(pkg.credits) + Number(pkg.bonus_credits || 0); setLeadCredits((prev) => prev + creditsToAdd); - + const ac = appliedCoupon(); + const amountPaid = ac ? ac.final_price_inr : pkg.price_paise; const newRow: [string, string, string, string, string, string] = [ paymentRef() || `#INV-${Date.now()}`, pkg.display_name || pkg.name, Number(creditsToAdd).toLocaleString(), - `₹${(pkg.price_paise / 100).toLocaleString('en-IN')}`, + `₹${(amountPaid / 100).toLocaleString('en-IN')}`, 'Completed', new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }), ]; setTxRows((prev) => [newRow, ...prev]); + setAppliedCoupon(null); + setCouponCode(''); + } else { + setPaymentStep('error'); } } catch (e) { setPaymentStep('error'); @@ -5294,22 +5365,57 @@ export default function DashboardDesignPreview(props: {
-
- Base Credits - {Number(pkg.credits).toLocaleString()} -
- 0}> -
- Bonus Credits - +{Number(pkg.bonus_credits).toLocaleString()} -
-
-
- Total Amount - ₹{(Number(pkg.price_paise) / 100).toLocaleString('en-IN')} -
-
- +
+ Base Credits + {Number(pkg.credits).toLocaleString()} +
+ 0}> +
+ Bonus Credits + +{Number(pkg.bonus_credits).toLocaleString()} +
+
+ + {/* Coupon section */} + +
+

Coupon Code

+
+ setCouponCode(e.currentTarget.value)} + style="flex:1;height:32px;border:1px solid #E5E7EB;border-radius:6px;padding:0 8px;font-size:12px" + disabled={couponLoading()} + /> + +
+ +

{couponError()}

+
+
+
+ +
+ Coupon ({appliedCoupon()!.code}) + -{appliedCoupon()!.discount_type === 'PERCENT' ? `${appliedCoupon()!.discount_value}%` : `₹${appliedCoupon()!.discount_value}`} +
+
+ +
+ Total Amount + ₹{((appliedCoupon() ? appliedCoupon().final_price_inr : Number(pkg.price_paise)) / 100).toLocaleString('en-IN')} +
+ +