feat: Implement a new testimonial submission page and service, and update related profile and settings components.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>{`
|
||||
|
||||
Reference in New Issue
Block a user