feat: Implement payment success confirmation page and testimonials form with new icons.
This commit is contained in:
4
public/assets/icons/copy-icon.svg
Normal file
4
public/assets/icons/copy-icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="5.5" y="5.5" width="9" height="9" rx="1.5" stroke="#00293D" stroke-width="1.2"/>
|
||||||
|
<path d="M10.5 5.5V3C10.5 2.17157 9.82843 1.5 9 1.5H3C2.17157 1.5 1.5 2.17157 1.5 3V9C1.5 9.82843 2.17157 10.5 3 10.5H5.5" stroke="#00293D" stroke-width="1.2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 354 B |
6
public/assets/icons/dashboard-white-icon.svg
Normal file
6
public/assets/icons/dashboard-white-icon.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2" y="2" width="6.5" height="6.5" rx="1.5" stroke="white" stroke-width="1.5"/>
|
||||||
|
<rect x="11.5" y="2" width="6.5" height="6.5" rx="1.5" stroke="white" stroke-width="1.5"/>
|
||||||
|
<rect x="2" y="11.5" width="6.5" height="6.5" rx="1.5" stroke="white" stroke-width="1.5"/>
|
||||||
|
<rect x="11.5" y="11.5" width="6.5" height="6.5" rx="1.5" stroke="white" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 467 B |
4
public/assets/icons/download-white-icon.svg
Normal file
4
public/assets/icons/download-white-icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 3V13M10 13L6 9M10 13L14 9" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 14V15C3 16.1046 3.89543 17 5 17H15C16.1046 17 17 16.1046 17 15V14" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 388 B |
4
public/assets/icons/payment-success-icon.svg
Normal file
4
public/assets/icons/payment-success-icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="20" cy="20" r="20" fill="#e58625"/>
|
||||||
|
<path d="M14 20.5L18 24.5L26 16.5" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 269 B |
3
public/assets/icons/sort-arrow-down-icon.svg
Normal file
3
public/assets/icons/sort-arrow-down-icon.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7 3V11M7 11L3.5 7.5M7 11L10.5 7.5" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 233 B |
4
public/assets/icons/verified-shield-green-icon.svg
Normal file
4
public/assets/icons/verified-shield-green-icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10 1.5L3 4.5V9.5C3 13.64 6.04 17.5 10 18.5C13.96 17.5 17 13.64 17 9.5V4.5L10 1.5Z" fill="#4CAF50"/>
|
||||||
|
<path d="M7.5 10L9.5 12L13 8" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 326 B |
@@ -0,0 +1,18 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { SettingsSidebar } from '@/components/settings';
|
||||||
|
import { PaymentSuccessForm } from '@/components/settings/PaymentSuccessForm';
|
||||||
|
|
||||||
|
export default function PaymentConfirmationPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col lg:flex-row gap-6">
|
||||||
|
{/* Left Sidebar */}
|
||||||
|
<SettingsSidebar basePath="/agent/settings" />
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<PaymentSuccessForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { SettingsSidebar } from '@/components/settings';
|
import { SettingsSidebar } from '@/components/settings';
|
||||||
|
import { TestimonialsForm } from '@/components/settings/TestimonialsForm';
|
||||||
|
|
||||||
export default function TestimonialsPage() {
|
export default function TestimonialsPage() {
|
||||||
return (
|
return (
|
||||||
@@ -10,15 +11,7 @@ export default function TestimonialsPage() {
|
|||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="border border-[#00293d]/10 rounded-[15px] bg-white p-6">
|
<TestimonialsForm />
|
||||||
<h2 className="font-fractul font-bold text-[20px] text-[#00293D] mb-4">
|
|
||||||
Add Testimonials
|
|
||||||
</h2>
|
|
||||||
<div className="border-b border-[#00293D]/10 mb-6" />
|
|
||||||
<p className="font-serif text-[14px] text-[#00293D]/70">
|
|
||||||
Testimonials management coming soon.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
interface CheckoutFeature {
|
interface CheckoutFeature {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -15,11 +16,13 @@ const CHECKOUT_FEATURES: CheckoutFeature[] = [
|
|||||||
|
|
||||||
export function PaymentCheckoutForm() {
|
export function PaymentCheckoutForm() {
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handlePayNow = async () => {
|
const handlePayNow = async () => {
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
// TODO: Integrate with payment gateway
|
// TODO: Integrate with payment gateway
|
||||||
setTimeout(() => setIsProcessing(false), 2000);
|
// For now, navigate to confirmation page
|
||||||
|
router.push('/agent/settings/billings/confirmation');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
158
src/components/settings/PaymentSuccessForm.tsx
Normal file
158
src/components/settings/PaymentSuccessForm.tsx
Normal file
@@ -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 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="font-fractul font-bold text-[20px] text-[#00293D] mb-2">
|
||||||
|
Payments
|
||||||
|
</h1>
|
||||||
|
<p className="font-serif text-[14px] text-[#00293D]/70">
|
||||||
|
Manage Your billing cycles , upgrade your plan , or boost individual listings
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payment Success Card */}
|
||||||
|
<div className="border border-[#00293D] rounded-[10px] py-10 px-8">
|
||||||
|
{/* Success Icon */}
|
||||||
|
<div className="flex justify-center mb-5">
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/payment-success-icon.svg"
|
||||||
|
alt="Success"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Success Title */}
|
||||||
|
<h2 className="font-fractul font-bold text-[20px] text-[#00293D] text-center mb-3">
|
||||||
|
Payment Successful !
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Success Description */}
|
||||||
|
<p className="font-serif text-[14px] text-[#00293D] text-center mb-8 max-w-[329px] mx-auto leading-[22px]">
|
||||||
|
Your annual membership is now active. You have full access to all premium features
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Transaction Details Section */}
|
||||||
|
<div className="max-w-[430px] mx-auto">
|
||||||
|
{/* Header Row */}
|
||||||
|
<div className="flex items-center justify-between mb-5">
|
||||||
|
<span className="font-serif text-[15px] text-[#00293D]">
|
||||||
|
Transaction Details
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/verified-shield-green-icon.svg"
|
||||||
|
alt="Verified"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
<span className="font-serif text-[14px] text-[#00293D]">
|
||||||
|
Verified User
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Detail Rows */}
|
||||||
|
<div className="space-y-5 mb-6">
|
||||||
|
{TRANSACTION_DETAILS.map((detail) => (
|
||||||
|
<div key={detail.label} className="flex items-center justify-between">
|
||||||
|
<span className="font-serif text-[15px] text-[#00293D]/50">
|
||||||
|
{detail.label}
|
||||||
|
</span>
|
||||||
|
<span className={`font-serif text-[14px] text-[#00293D] ${detail.bold ? 'font-bold' : ''}`}>
|
||||||
|
{detail.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="border-b border-[#00293D]/20 mb-5" />
|
||||||
|
|
||||||
|
{/* Next Renewal Date */}
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<span className="font-serif text-[15px] text-[#00293D]">
|
||||||
|
Next Renewal Date
|
||||||
|
</span>
|
||||||
|
<span className="font-serif font-bold text-[14px] text-[#e58625]">
|
||||||
|
Oct 24, 2025
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-4 mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/agent/dashboard')}
|
||||||
|
className="flex-1 bg-[#e58625] border border-[#e58625] text-white font-fractul font-bold text-[14px] py-4 rounded-[15px] hover:bg-[#d47920] transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/dashboard-white-icon.svg"
|
||||||
|
alt=""
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
Go To Dashboard
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex-1 bg-[#e58625] border border-[#e58625] text-white font-fractul font-bold text-[14px] py-4 rounded-[15px] hover:bg-[#d47920] transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/download-white-icon.svg"
|
||||||
|
alt=""
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
Download Receipt
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trust Badges */}
|
||||||
|
<div className="flex items-center justify-center gap-8">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/verified-shield-green-icon.svg"
|
||||||
|
alt="Verified"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
<span className="font-serif text-[14px] text-[#00293D]">
|
||||||
|
Verified User
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/lock-icon.svg"
|
||||||
|
alt="Secure"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
<span className="font-serif text-[14px] text-[#00293D]">
|
||||||
|
Secure Checkout
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
227
src/components/settings/TestimonialsForm.tsx
Normal file
227
src/components/settings/TestimonialsForm.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{Array.from({ length: rating }).map((_, i) => (
|
||||||
|
<Image
|
||||||
|
key={i}
|
||||||
|
src="/assets/icons/star-filled-icon.svg"
|
||||||
|
alt=""
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestimonialCard({ testimonial }: { testimonial: Testimonial }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-[7px] p-4">
|
||||||
|
<StarRating rating={testimonial.rating} />
|
||||||
|
<p className="font-serif text-[14px] text-black mt-3 leading-[22px]">
|
||||||
|
{testimonial.text}
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-[14px] text-[#00293D]">
|
||||||
|
<span className="font-fractul font-bold">{testimonial.authorName} ,</span>
|
||||||
|
<span className="font-fractul font-light"> {testimonial.authorType} · {testimonial.date}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="font-fractul font-bold text-[20px] text-[#00293D] mb-2">
|
||||||
|
Add Testimonials
|
||||||
|
</h1>
|
||||||
|
<p className="font-fractul font-medium text-[14px] text-[#00293D]">
|
||||||
|
What people are saying about you
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create Testimonials Section */}
|
||||||
|
<div className="border border-[#00293D]/10 rounded-[7px] p-6 mb-6">
|
||||||
|
<h2 className="font-fractul font-bold text-[20px] text-[#00293D] mb-4">
|
||||||
|
Create Testimonials
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
|
{/* Generate Link Button */}
|
||||||
|
<button className="bg-[#e58625] text-white font-fractul font-bold text-[14px] px-8 py-3 rounded-[15px] hover:bg-[#d47920] transition-colors whitespace-nowrap">
|
||||||
|
Generate Link
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Link Input with Copy */}
|
||||||
|
<div className="flex-1 flex items-center border border-[#00293D]/10 rounded-[15px] overflow-hidden">
|
||||||
|
<span className="flex-1 font-fractul font-light text-[14px] text-black px-4 py-3 truncate">
|
||||||
|
{testimonialLink}
|
||||||
|
</span>
|
||||||
|
<div className="h-[46px] w-px bg-[#00293D]/10" />
|
||||||
|
<button
|
||||||
|
onClick={handleCopyLink}
|
||||||
|
className="flex items-center gap-1.5 px-4 py-3 hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/copy-icon.svg"
|
||||||
|
alt="Copy"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
<span className="font-serif font-light text-[14px] text-black">
|
||||||
|
{copied ? 'Copied!' : 'Copy'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Testimonials Columns */}
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4">
|
||||||
|
{/* Latest Column */}
|
||||||
|
<div className="flex-1 bg-[#d9d9d9]/25 rounded-[7px] p-5">
|
||||||
|
{/* Sort Header */}
|
||||||
|
<div className="flex items-center gap-1 mb-4">
|
||||||
|
<span className="font-fractul font-medium text-[14px] text-black">
|
||||||
|
Sort By : Latest
|
||||||
|
</span>
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/sort-arrow-down-icon.svg"
|
||||||
|
alt=""
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards */}
|
||||||
|
<div className="space-y-4 max-h-[500px] overflow-y-auto pr-1 custom-scrollbar">
|
||||||
|
{LATEST_TESTIMONIALS.map((testimonial) => (
|
||||||
|
<TestimonialCard key={testimonial.id} testimonial={testimonial} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Oldest Column */}
|
||||||
|
<div className="flex-1 bg-[#d9d9d9]/25 rounded-[7px] p-5">
|
||||||
|
{/* Sort Header */}
|
||||||
|
<div className="flex items-center gap-1 mb-4">
|
||||||
|
<span className="font-fractul font-medium text-[14px] text-black">
|
||||||
|
Sort By : Oldest
|
||||||
|
</span>
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/sort-arrow-down-icon.svg"
|
||||||
|
alt=""
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards */}
|
||||||
|
<div className="space-y-4 max-h-[500px] overflow-y-auto pr-1 custom-scrollbar">
|
||||||
|
{OLDEST_TESTIMONIALS.map((testimonial) => (
|
||||||
|
<TestimonialCard key={testimonial.id} testimonial={testimonial} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom scrollbar styles */}
|
||||||
|
<style jsx>{`
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: #d9d9d9;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 41, 61, 0.25);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,3 +7,5 @@ export { PrivacyForm } from './PrivacyForm';
|
|||||||
export { TwoFactorSettings } from './TwoFactorSettings';
|
export { TwoFactorSettings } from './TwoFactorSettings';
|
||||||
export { SubscriptionForm } from './SubscriptionForm';
|
export { SubscriptionForm } from './SubscriptionForm';
|
||||||
export { PaymentCheckoutForm } from './PaymentCheckoutForm';
|
export { PaymentCheckoutForm } from './PaymentCheckoutForm';
|
||||||
|
export { PaymentSuccessForm } from './PaymentSuccessForm';
|
||||||
|
export { TestimonialsForm } from './TestimonialsForm';
|
||||||
|
|||||||
@@ -21,12 +21,25 @@ export interface ApiErrorResponse {
|
|||||||
|
|
||||||
// Token refresh state
|
// Token refresh state
|
||||||
let isRefreshing = false;
|
let isRefreshing = false;
|
||||||
|
let isLoggingOut = false;
|
||||||
let failedQueue: Array<{
|
let failedQueue: Array<{
|
||||||
resolve: (value: AxiosResponse) => void;
|
resolve: (value: AxiosResponse) => void;
|
||||||
reject: (error: AxiosError) => void;
|
reject: (error: AxiosError) => void;
|
||||||
config: InternalAxiosRequestConfig;
|
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) => {
|
const processQueue = (error: AxiosError | null, newToken?: string) => {
|
||||||
failedQueue.forEach((prom) => {
|
failedQueue.forEach((prom) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -111,14 +124,7 @@ api.interceptors.response.use(
|
|||||||
|
|
||||||
// Don't try to refresh if this was the refresh token request itself
|
// Don't try to refresh if this was the refresh token request itself
|
||||||
if (originalRequest.url?.includes('/auth/refresh')) {
|
if (originalRequest.url?.includes('/auth/refresh')) {
|
||||||
// Refresh token is also invalid, clear everything and sign out properly
|
forceLogout();
|
||||||
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';
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,13 +141,10 @@ api.interceptors.response.use(
|
|||||||
const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null;
|
const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null;
|
||||||
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
// No refresh token available, just clear tokens but don't redirect for better UX
|
// No refresh token available, force logout
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
localStorage.removeItem('accessToken');
|
|
||||||
localStorage.removeItem('refreshToken');
|
|
||||||
localStorage.removeItem('user');
|
|
||||||
}
|
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
|
processQueue(error);
|
||||||
|
forceLogout();
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,14 +176,10 @@ api.interceptors.response.use(
|
|||||||
// Retry original request
|
// Retry original request
|
||||||
return api(originalRequest);
|
return api(originalRequest);
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
// Refresh failed, clear tokens but don't redirect
|
// Refresh failed, force logout
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
localStorage.removeItem('accessToken');
|
|
||||||
localStorage.removeItem('refreshToken');
|
|
||||||
localStorage.removeItem('user');
|
|
||||||
}
|
|
||||||
processQueue(refreshError as AxiosError);
|
processQueue(refreshError as AxiosError);
|
||||||
isRefreshing = false;
|
isRefreshing = false;
|
||||||
|
forceLogout();
|
||||||
return Promise.reject(refreshError);
|
return Promise.reject(refreshError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user