feat: Implement a new testimonial submission page and service, and update related profile and settings components.

This commit is contained in:
pradeepkumar
2026-03-05 05:36:10 +05:30
parent a1c128ea2f
commit 93b86a3472
6 changed files with 485 additions and 206 deletions

View File

@@ -3,7 +3,7 @@
import { TestimonialCard } from './TestimonialCard';
interface Testimonial {
id: number;
id: string | number;
text: string;
author: string;
role: string;
@@ -15,6 +15,10 @@ interface TestimonialsSectionProps {
}
export function TestimonialsSection({ testimonials }: TestimonialsSectionProps) {
if (!testimonials || testimonials.length === 0) {
return null;
}
return (
<div className="bg-white rounded-[20px] p-8">
<h2 className="text-[20px] font-bold text-[#00293D] font-fractul leading-[24px] text-center mb-4">Testimonials</h2>

View File

@@ -1,70 +1,8 @@
'use client';
import { useState } from 'react';
import { useState, useEffect } 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',
},
];
import { testimonialsService, Testimonial } from '@/services/testimonials.service';
function StarRating({ rating }: { rating: number }) {
return (
@@ -83,6 +21,20 @@ function StarRating({ rating }: { rating: number }) {
}
function TestimonialCard({ testimonial }: { testimonial: Testimonial }) {
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) return 'Today';
if (diffDays === 1) return '1 day ago';
if (diffDays < 30) return `${diffDays} days ago`;
if (diffDays < 60) return '1 month ago';
if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
};
return (
<div className="bg-white rounded-[7px] p-4">
<StarRating rating={testimonial.rating} />
@@ -91,7 +43,7 @@ function TestimonialCard({ testimonial }: { testimonial: Testimonial }) {
</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>
<span className="font-fractul font-light"> {testimonial.authorRole} · {formatDate(testimonial.createdAt)}</span>
</p>
</div>
);
@@ -99,9 +51,46 @@ function TestimonialCard({ testimonial }: { testimonial: Testimonial }) {
export function TestimonialsForm() {
const [copied, setCopied] = useState(false);
const testimonialLink = 'https://www.re-quest1.com/testimonials/9A528z';
const [testimonialLink, setTestimonialLink] = useState('');
const [latestTestimonials, setLatestTestimonials] = useState<Testimonial[]>([]);
const [oldestTestimonials, setOldestTestimonials] = useState<Testimonial[]>([]);
const [loading, setLoading] = useState(true);
const [generating, setGenerating] = useState(false);
useEffect(() => {
const fetchTestimonials = async () => {
try {
const [latest, oldest] = await Promise.all([
testimonialsService.getMyTestimonials('latest'),
testimonialsService.getMyTestimonials('oldest'),
]);
setLatestTestimonials(latest);
setOldestTestimonials(oldest);
} catch (err) {
console.error('Failed to fetch testimonials:', err);
} finally {
setLoading(false);
}
};
fetchTestimonials();
}, []);
const handleGenerateLink = async () => {
try {
setGenerating(true);
const { token } = await testimonialsService.generateLink();
const baseUrl = window.location.origin;
setTestimonialLink(`${baseUrl}/testimonials/${token}`);
} catch (err) {
console.error('Failed to generate link:', err);
} finally {
setGenerating(false);
}
};
const handleCopyLink = async () => {
if (!testimonialLink) return;
try {
await navigator.clipboard.writeText(testimonialLink);
setCopied(true);
@@ -131,82 +120,99 @@ export function TestimonialsForm() {
<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
onClick={handleGenerateLink}
disabled={generating}
className="bg-[#e58625] text-white font-fractul font-bold text-[14px] px-8 py-3 rounded-[15px] hover:bg-[#d47920] transition-colors whitespace-nowrap disabled:opacity-50"
>
{generating ? 'Generating...' : '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}
{testimonialLink || 'Click "Generate Link" to create a shareable link'}
</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>
{testimonialLink && (
<>
<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}
/>
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#E58625]"></div>
</div>
) : latestTestimonials.length === 0 ? (
<div className="text-center py-12 bg-[#d9d9d9]/25 rounded-[7px]">
<p className="font-fractul font-medium text-[16px] text-[#00293D]/60">
No testimonials yet
</p>
<p className="font-fractul font-light text-[14px] text-[#00293D]/40 mt-2">
Generate a link and share it to collect testimonials
</p>
</div>
) : (
<div className="flex flex-col lg:flex-row gap-4">
{/* Latest Column */}
<div className="flex-1 bg-[#d9d9d9]/25 rounded-[7px] p-5">
<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>
<div className="space-y-4 max-h-[500px] overflow-y-auto pr-1 custom-scrollbar">
{latestTestimonials.map((testimonial) => (
<TestimonialCard key={testimonial.id} testimonial={testimonial} />
))}
</div>
</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} />
))}
{/* Oldest Column */}
<div className="flex-1 bg-[#d9d9d9]/25 rounded-[7px] p-5">
<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>
<div className="space-y-4 max-h-[500px] overflow-y-auto pr-1 custom-scrollbar">
{oldestTestimonials.map((testimonial) => (
<TestimonialCard key={testimonial.id} testimonial={testimonial} />
))}
</div>
</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>{`