From cb76eb648866e76daf80f90a1b3e59f8a5c95101 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Thu, 12 Feb 2026 06:21:11 +0530 Subject: [PATCH] feat: Implement payment success confirmation page and testimonials form with new icons. --- public/assets/icons/copy-icon.svg | 4 + public/assets/icons/dashboard-white-icon.svg | 6 + public/assets/icons/download-white-icon.svg | 4 + public/assets/icons/payment-success-icon.svg | 4 + public/assets/icons/sort-arrow-down-icon.svg | 3 + .../icons/verified-shield-green-icon.svg | 4 + .../settings/billings/confirmation/page.tsx | 18 ++ .../agent/settings/testimonials/page.tsx | 11 +- .../settings/PaymentCheckoutForm.tsx | 5 +- .../settings/PaymentSuccessForm.tsx | 158 ++++++++++++ src/components/settings/TestimonialsForm.tsx | 227 ++++++++++++++++++ src/components/settings/index.ts | 2 + src/services/api.ts | 39 ++- 13 files changed, 455 insertions(+), 30 deletions(-) create mode 100644 public/assets/icons/copy-icon.svg create mode 100644 public/assets/icons/dashboard-white-icon.svg create mode 100644 public/assets/icons/download-white-icon.svg create mode 100644 public/assets/icons/payment-success-icon.svg create mode 100644 public/assets/icons/sort-arrow-down-icon.svg create mode 100644 public/assets/icons/verified-shield-green-icon.svg create mode 100644 src/app/(agent)/agent/settings/billings/confirmation/page.tsx create mode 100644 src/components/settings/PaymentSuccessForm.tsx create mode 100644 src/components/settings/TestimonialsForm.tsx diff --git a/public/assets/icons/copy-icon.svg b/public/assets/icons/copy-icon.svg new file mode 100644 index 0000000..7bd31cc --- /dev/null +++ b/public/assets/icons/copy-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/icons/dashboard-white-icon.svg b/public/assets/icons/dashboard-white-icon.svg new file mode 100644 index 0000000..8967d9c --- /dev/null +++ b/public/assets/icons/dashboard-white-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/icons/download-white-icon.svg b/public/assets/icons/download-white-icon.svg new file mode 100644 index 0000000..ca7a49d --- /dev/null +++ b/public/assets/icons/download-white-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/icons/payment-success-icon.svg b/public/assets/icons/payment-success-icon.svg new file mode 100644 index 0000000..3b1058f --- /dev/null +++ b/public/assets/icons/payment-success-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/icons/sort-arrow-down-icon.svg b/public/assets/icons/sort-arrow-down-icon.svg new file mode 100644 index 0000000..342fb98 --- /dev/null +++ b/public/assets/icons/sort-arrow-down-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/verified-shield-green-icon.svg b/public/assets/icons/verified-shield-green-icon.svg new file mode 100644 index 0000000..42544e4 --- /dev/null +++ b/public/assets/icons/verified-shield-green-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/app/(agent)/agent/settings/billings/confirmation/page.tsx b/src/app/(agent)/agent/settings/billings/confirmation/page.tsx new file mode 100644 index 0000000..44c2fbd --- /dev/null +++ b/src/app/(agent)/agent/settings/billings/confirmation/page.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { SettingsSidebar } from '@/components/settings'; +import { PaymentSuccessForm } from '@/components/settings/PaymentSuccessForm'; + +export default function PaymentConfirmationPage() { + return ( +
+ {/* Left Sidebar */} + + + {/* Main Content */} +
+ +
+
+ ); +} diff --git a/src/app/(agent)/agent/settings/testimonials/page.tsx b/src/app/(agent)/agent/settings/testimonials/page.tsx index 86e37e4..0c58a7d 100644 --- a/src/app/(agent)/agent/settings/testimonials/page.tsx +++ b/src/app/(agent)/agent/settings/testimonials/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { SettingsSidebar } from '@/components/settings'; +import { TestimonialsForm } from '@/components/settings/TestimonialsForm'; export default function TestimonialsPage() { return ( @@ -10,15 +11,7 @@ export default function TestimonialsPage() { {/* Main Content */}
-
-

- Add Testimonials -

-
-

- Testimonials management coming soon. -

-
+
); diff --git a/src/components/settings/PaymentCheckoutForm.tsx b/src/components/settings/PaymentCheckoutForm.tsx index 76b3c21..91bb792 100644 --- a/src/components/settings/PaymentCheckoutForm.tsx +++ b/src/components/settings/PaymentCheckoutForm.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import Image from 'next/image'; +import { useRouter } from 'next/navigation'; interface CheckoutFeature { label: string; @@ -15,11 +16,13 @@ const CHECKOUT_FEATURES: CheckoutFeature[] = [ export function PaymentCheckoutForm() { const [isProcessing, setIsProcessing] = useState(false); + const router = useRouter(); const handlePayNow = async () => { setIsProcessing(true); // TODO: Integrate with payment gateway - setTimeout(() => setIsProcessing(false), 2000); + // For now, navigate to confirmation page + router.push('/agent/settings/billings/confirmation'); }; return ( diff --git a/src/components/settings/PaymentSuccessForm.tsx b/src/components/settings/PaymentSuccessForm.tsx new file mode 100644 index 0000000..29eba57 --- /dev/null +++ b/src/components/settings/PaymentSuccessForm.tsx @@ -0,0 +1,158 @@ +'use client'; + +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; + +interface TransactionDetail { + label: string; + value: string; + bold?: boolean; +} + +const TRANSACTION_DETAILS: TransactionDetail[] = [ + { label: 'Transaction ID', value: 'REQ-89234475', bold: true }, + { label: 'Amount Paid', value: '$499.00', bold: true }, + { label: 'Date', value: 'Oct 24, 2024', bold: true }, +]; + +export function PaymentSuccessForm() { + const router = useRouter(); + + return ( + <> + {/* Page Title */} +
+

+ Payments +

+

+ Manage Your billing cycles , upgrade your plan , or boost individual listings +

+
+ + {/* Payment Success Card */} +
+ {/* Success Icon */} +
+ Success +
+ + {/* Success Title */} +

+ Payment Successful ! +

+ + {/* Success Description */} +

+ Your annual membership is now active. You have full access to all premium features +

+ + {/* Transaction Details Section */} +
+ {/* Header Row */} +
+ + Transaction Details + +
+ Verified + + Verified User + +
+
+ + {/* Detail Rows */} +
+ {TRANSACTION_DETAILS.map((detail) => ( +
+ + {detail.label} + + + {detail.value} + +
+ ))} +
+ + {/* Divider */} +
+ + {/* Next Renewal Date */} +
+ + Next Renewal Date + + + Oct 24, 2025 + +
+ + {/* Action Buttons */} +
+ + +
+ + {/* Trust Badges */} +
+
+ Verified + + Verified User + +
+
+ Secure + + Secure Checkout + +
+
+
+
+ + ); +} diff --git a/src/components/settings/TestimonialsForm.tsx b/src/components/settings/TestimonialsForm.tsx new file mode 100644 index 0000000..550b5a5 --- /dev/null +++ b/src/components/settings/TestimonialsForm.tsx @@ -0,0 +1,227 @@ +'use client'; + +import { useState } from 'react'; +import Image from 'next/image'; + +interface Testimonial { + id: number; + rating: number; + text: string; + authorName: string; + authorType: string; + date: string; +} + +const LATEST_TESTIMONIALS: Testimonial[] = [ + { + id: 1, + rating: 5, + text: 'Brian was amazing to work with. He found us the perfect home quickly and made the entire process smooth and stress-free.', + authorName: 'Neha R', + authorType: 'Home Buyer', + date: '2 days ago', + }, + { + id: 2, + rating: 5, + text: 'Brian was amazing to work with. He found us the perfect home quickly and made the entire process smooth and stress-free.', + authorName: 'Neha R', + authorType: 'Home Buyer', + date: 'April,23,2023', + }, + { + id: 3, + rating: 5, + text: 'Brian was amazing to work with. He found us the perfect home quickly and made the entire process smooth and stress-free.', + authorName: 'Neha R', + authorType: 'Home Buyer', + date: '2 days ago', + }, +]; + +const OLDEST_TESTIMONIALS: Testimonial[] = [ + { + id: 4, + rating: 5, + text: 'Brian was amazing to work with. He found us the perfect home quickly and made the entire process smooth and stress-free.', + authorName: 'Neha R', + authorType: 'Investor', + date: '5 Months ago', + }, + { + id: 5, + rating: 5, + text: 'Brian was amazing to work with. He found us the perfect home quickly and made the entire process smooth and stress-free.', + authorName: 'Mark R', + authorType: 'Home Seller', + date: '6 Months ago', + }, + { + id: 6, + rating: 5, + text: 'Brian was amazing to work with. He found us the perfect home quickly and made the entire process smooth and stress-free.', + authorName: 'Alex P', + authorType: 'Home Buyer', + date: '8 Months ago', + }, +]; + +function StarRating({ rating }: { rating: number }) { + return ( +
+ {Array.from({ length: rating }).map((_, i) => ( + + ))} +
+ ); +} + +function TestimonialCard({ testimonial }: { testimonial: Testimonial }) { + return ( +
+ +

+ {testimonial.text} +

+

+ {testimonial.authorName} , + {testimonial.authorType} ยท {testimonial.date} +

+
+ ); +} + +export function TestimonialsForm() { + const [copied, setCopied] = useState(false); + const testimonialLink = 'https://www.re-quest1.com/testimonials/9A528z'; + + const handleCopyLink = async () => { + try { + await navigator.clipboard.writeText(testimonialLink); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // Fallback for older browsers + } + }; + + return ( + <> + {/* Page Title */} +
+

+ Add Testimonials +

+

+ What people are saying about you +

+
+ + {/* Create Testimonials Section */} +
+

+ Create Testimonials +

+ +
+ {/* Generate Link Button */} + + + {/* Link Input with Copy */} +
+ + {testimonialLink} + +
+ +
+
+
+ + {/* Testimonials Columns */} +
+ {/* Latest Column */} +
+ {/* Sort Header */} +
+ + Sort By : Latest + + +
+ + {/* Cards */} +
+ {LATEST_TESTIMONIALS.map((testimonial) => ( + + ))} +
+
+ + {/* Oldest Column */} +
+ {/* Sort Header */} +
+ + Sort By : Oldest + + +
+ + {/* Cards */} +
+ {OLDEST_TESTIMONIALS.map((testimonial) => ( + + ))} +
+
+
+ + {/* Custom scrollbar styles */} + + + ); +} diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index 19a3271..ec48a76 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -7,3 +7,5 @@ export { PrivacyForm } from './PrivacyForm'; export { TwoFactorSettings } from './TwoFactorSettings'; export { SubscriptionForm } from './SubscriptionForm'; export { PaymentCheckoutForm } from './PaymentCheckoutForm'; +export { PaymentSuccessForm } from './PaymentSuccessForm'; +export { TestimonialsForm } from './TestimonialsForm'; diff --git a/src/services/api.ts b/src/services/api.ts index 366a9ac..4a4b179 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -21,12 +21,25 @@ export interface ApiErrorResponse { // Token refresh state let isRefreshing = false; +let isLoggingOut = false; let failedQueue: Array<{ resolve: (value: AxiosResponse) => void; reject: (error: AxiosError) => void; config: InternalAxiosRequestConfig; }> = []; +// Force logout: clear all tokens and redirect to login +const forceLogout = () => { + if (isLoggingOut) return; // Prevent multiple redirects + isLoggingOut = true; + if (typeof window !== 'undefined') { + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + localStorage.removeItem('user'); + window.location.href = '/api/auth/signout?callbackUrl=/login'; + } +}; + const processQueue = (error: AxiosError | null, newToken?: string) => { failedQueue.forEach((prom) => { if (error) { @@ -111,14 +124,7 @@ api.interceptors.response.use( // Don't try to refresh if this was the refresh token request itself if (originalRequest.url?.includes('/auth/refresh')) { - // Refresh token is also invalid, clear everything and sign out properly - if (typeof window !== 'undefined') { - localStorage.removeItem('accessToken'); - localStorage.removeItem('refreshToken'); - localStorage.removeItem('user'); - // Use NextAuth's signout endpoint to properly clear the session - window.location.href = '/api/auth/signout?callbackUrl=/login'; - } + forceLogout(); return Promise.reject(error); } @@ -135,13 +141,10 @@ api.interceptors.response.use( const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null; if (!refreshToken) { - // No refresh token available, just clear tokens but don't redirect for better UX - if (typeof window !== 'undefined') { - localStorage.removeItem('accessToken'); - localStorage.removeItem('refreshToken'); - localStorage.removeItem('user'); - } + // No refresh token available, force logout isRefreshing = false; + processQueue(error); + forceLogout(); return Promise.reject(error); } @@ -173,14 +176,10 @@ api.interceptors.response.use( // Retry original request return api(originalRequest); } catch (refreshError) { - // Refresh failed, clear tokens but don't redirect - if (typeof window !== 'undefined') { - localStorage.removeItem('accessToken'); - localStorage.removeItem('refreshToken'); - localStorage.removeItem('user'); - } + // Refresh failed, force logout processQueue(refreshError as AxiosError); isRefreshing = false; + forceLogout(); return Promise.reject(refreshError); } }