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

@@ -18,35 +18,12 @@ import {
import { agentsService, AgentProfile, FieldValueResponse } from '@/services/agents.service';
import { connectionRequestsService } from '@/services/connection-requests.service';
import { uploadService } from '@/services/upload.service';
import { testimonialsService, Testimonial } from '@/services/testimonials.service';
import { mapFieldValuesToExperience, mapFieldValuesToProfileCard, mapFieldValuesToAvailability, mapFieldValuesToSpecializationFields, mapFieldValuesToContactInfo, ExperienceData, ProfileCardData, AvailabilityData, SpecializationFieldsData, ContactInfoData } from '@/utils/profileDataMapper';
// Mock data for sections not yet available from API
const mockData = {
preferredWorkEnvironment: 'Preferred work environment includes on-site property visits, in-depth market research, client consultations, property tours, and active involvement in negotiations to ensure the best outcomes',
testimonialHighlight: "The most amazing experience I've had as a real estate professional is helping a family secure their dream home and seeing their happiness when they received the keys.",
testimonials: [
{
id: 1,
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
author: 'Kenedy Kenney',
role: 'Chief Operations Officer',
rating: 5,
},
{
id: 2,
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
author: 'Kenedy Kenney',
role: 'Chief Operations Officer',
rating: 5,
},
{
id: 3,
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
author: 'Kenedy Kenney',
role: 'Chief Operations Officer',
rating: 5,
},
],
};
// Default experience data when no field values are available
@@ -101,6 +78,7 @@ export default function AgentDashboard() {
const [isAvailable, setIsAvailable] = useState(true);
const [isTogglingAvailability, setIsTogglingAvailability] = useState(false);
const [pendingRequestCount, setPendingRequestCount] = useState(0);
const [testimonials, setTestimonials] = useState<{ id: string; text: string; author: string; role: string; rating: number }[]>([]);
const defaultImage = '/assets/demo_images/8f017153a7b4a239a4f0691234ef97dd55092282.jpg';
@@ -118,11 +96,12 @@ export default function AgentDashboard() {
setLoading(true);
setError(null);
// Fetch profile, field values, and pending request count in parallel
const [profile, fieldValuesResponse, requestCounts] = await Promise.all([
// Fetch profile, field values, pending request count, and testimonials in parallel
const [profile, fieldValuesResponse, requestCounts, testimonialsData] = await Promise.all([
agentsService.getMyProfile(),
agentsService.getFieldValues(),
connectionRequestsService.getRequestCounts().catch(() => null),
testimonialsService.getMyTestimonials('latest').catch(() => [] as Testimonial[]),
]);
setAgentProfile(profile);
@@ -132,6 +111,17 @@ export default function AgentDashboard() {
setPendingRequestCount(requestCounts.pending);
}
// Map testimonials for TestimonialsSection (latest 3)
setTestimonials(
testimonialsData.slice(0, 3).map((t) => ({
id: t.id,
text: t.text,
author: t.authorName,
role: t.authorRole,
rating: t.rating,
}))
);
// Map field values to experience data structure
const mappedExperience = mapFieldValuesToExperience(fieldValuesResponse.fieldValues);
setExperienceData(mappedExperience);
@@ -404,6 +394,7 @@ export default function AgentDashboard() {
}
content={<p className="font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]">{mockData.preferredWorkEnvironment}</p>}
/>
{testimonials.length > 0 && (
<InfoCard
title=""
icon={
@@ -414,15 +405,16 @@ export default function AgentDashboard() {
height={28}
/>
}
content={<p className="text-[14px] font-semibold font-serif leading-[19px] text-center text-[#00293D]">&ldquo;{mockData.testimonialHighlight}&rdquo;</p>}
content={<p className="text-[14px] font-semibold font-serif leading-[19px] text-center text-[#00293D]">&ldquo;{testimonials[0].text.length > 150 ? testimonials[0].text.substring(0, 150) + '...' : testimonials[0].text}&rdquo;</p>}
/>
)}
</div>
{/* Specialization Section */}
<SpecializationSection fieldsData={specializationFieldsData} />
{/* Testimonials Section */}
<TestimonialsSection testimonials={mockData.testimonials} />
<TestimonialsSection testimonials={testimonials} />
</div>
</div>

View File

@@ -19,35 +19,12 @@ import { ConnectRequestModal } from '@/components/modals';
import { agentsService, AgentProfile, FieldValueResponse } from '@/services/agents.service';
import { connectionRequestsService, ConnectionStatus } from '@/services/connection-requests.service';
import { uploadService } from '@/services/upload.service';
import { testimonialsService, Testimonial } from '@/services/testimonials.service';
import { mapFieldValuesToExperience, mapFieldValuesToSpecializationFields, mapFieldValuesToProfileCard, mapFieldValuesToContactInfo, mapFieldValuesToAvailability, ExperienceData, SpecializationFieldsData, ProfileCardData, ContactInfoData, AvailabilityData } from '@/utils/profileDataMapper';
// Mock data for sections not yet available from API
const mockData = {
preferredWorkEnvironment: 'Preferred work environment includes on-site property visits, in-depth market research, client consultations, property tours, and active involvement in negotiations to ensure the best outcomes',
testimonialHighlight: "The most amazing experience I've had as a real estate professional is helping a family secure their dream home and seeing their happiness when they received the keys.",
testimonials: [
{
id: 1,
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
author: 'Kenedy Kenney',
role: 'Chief Operations Officer',
rating: 5,
},
{
id: 2,
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
author: 'Kenedy Kenney',
role: 'Chief Operations Officer',
rating: 5,
},
{
id: 3,
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean commodo ligula eget dolor. Sed dignissim, nisl eget tincidunt vulputate, lacus justo bibendum ipsum, vitae tempus risus lorem at nunc. Integer sed arcu vitae risus feugiat vehicula. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.',
author: 'Kenedy Kenney',
role: 'Chief Operations Officer',
rating: 5,
},
],
};
// Default experience data when no field values are available
@@ -102,6 +79,8 @@ export default function AgentProfileView() {
const [imageError, setImageError] = useState(false);
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [testimonials, setTestimonials] = useState<{ id: string; text: string; author: string; role: string; rating: number }[]>([]);
// Connect modal state
const [showConnectModal, setShowConnectModal] = useState(false);
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus | null>(null);
@@ -125,10 +104,11 @@ export default function AgentProfileView() {
setLoading(true);
setError(null);
// Fetch profile and field values in parallel
const [profile, fieldValuesResponse] = await Promise.all([
// Fetch profile, field values, and testimonials in parallel
const [profile, fieldValuesResponse, testimonialsData] = await Promise.all([
agentsService.getAgentById(id),
agentsService.getFieldValuesByAgentId(id),
testimonialsService.getAgentTestimonials(id).catch(() => [] as Testimonial[]),
]);
setAgentProfile(profile);
@@ -154,6 +134,17 @@ export default function AgentProfileView() {
const mappedAvailability = mapFieldValuesToAvailability(fieldValuesResponse.fieldValues);
setAvailabilityData(mappedAvailability);
// Map testimonials for TestimonialsSection
setTestimonials(
testimonialsData.map((t) => ({
id: t.id,
text: t.text,
author: t.authorName,
role: t.authorRole,
rating: t.rating,
}))
);
// If avatar is an S3 key, fetch presigned URL
if (profile.avatar && isS3Key(profile.avatar)) {
try {
@@ -399,6 +390,7 @@ export default function AgentProfileView() {
}
content={<p className="font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]">{mockData.preferredWorkEnvironment}</p>}
/>
{testimonials.length > 0 && (
<InfoCard
title=""
icon={
@@ -409,15 +401,16 @@ export default function AgentProfileView() {
height={28}
/>
}
content={<p className="text-[14px] font-semibold font-serif leading-[19px] text-center text-[#00293D]">&ldquo;{mockData.testimonialHighlight}&rdquo;</p>}
content={<p className="text-[14px] font-semibold font-serif leading-[19px] text-center text-[#00293D]">&ldquo;{testimonials[0].text.length > 150 ? testimonials[0].text.substring(0, 150) + '...' : testimonials[0].text}&rdquo;</p>}
/>
)}
</div>
{/* Specialization Section */}
<SpecializationSection fieldsData={specializationFieldsData} />
{/* Testimonials Section */}
<TestimonialsSection testimonials={mockData.testimonials} />
<TestimonialsSection testimonials={testimonials} />
</div>
</div>

View File

@@ -0,0 +1,222 @@
'use client';
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Image from 'next/image';
import { testimonialsService, AgentByTokenResponse } from '@/services/testimonials.service';
const ROLE_OPTIONS = ['Home Buyer', 'Home Seller', 'Investor', 'Renter', 'Landlord', 'Other'];
export default function SubmitTestimonialPage() {
const params = useParams();
const token = params.token as string;
const [agent, setAgent] = useState<AgentByTokenResponse | null>(null);
const [loading, setLoading] = useState(true);
const [invalidToken, setInvalidToken] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [rating, setRating] = useState(0);
const [text, setText] = useState('');
const [authorName, setAuthorName] = useState('');
const [authorRole, setAuthorRole] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});
useEffect(() => {
const fetchAgent = async () => {
try {
const agentData = await testimonialsService.getAgentByToken(token);
setAgent(agentData);
} catch {
setInvalidToken(true);
} finally {
setLoading(false);
}
};
fetchAgent();
}, [token]);
const validate = () => {
const newErrors: Record<string, string> = {};
if (rating === 0) newErrors.rating = 'Please select a rating';
if (!text.trim()) newErrors.text = 'Please write your testimonial';
if (!authorName.trim()) newErrors.authorName = 'Please enter your name';
if (!authorRole) newErrors.authorRole = 'Please select your role';
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) return;
try {
setSubmitting(true);
await testimonialsService.submitTestimonial(token, {
rating,
text: text.trim(),
authorName: authorName.trim(),
authorRole,
});
setSubmitted(true);
} catch (err) {
console.error('Failed to submit testimonial:', err);
setErrors({ submit: 'Failed to submit. Please try again.' });
} finally {
setSubmitting(false);
}
};
if (loading) {
return (
<div className="min-h-screen bg-[#f5f5f5] flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#E58625]"></div>
</div>
);
}
if (invalidToken) {
return (
<div className="min-h-screen bg-[#f5f5f5] flex items-center justify-center px-4">
<div className="bg-white rounded-[20px] p-8 max-w-md text-center shadow-[0px_10px_20px_rgba(217,217,217,0.5)]">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Image src="/assets/icons/caution-icon.svg" alt="Error" width={24} height={24} />
</div>
<h2 className="text-[20px] font-bold font-fractul text-[#00293D] mb-2">Invalid Link</h2>
<p className="text-[14px] font-serif text-[#00293D]/70">
This testimonial link is invalid or has expired. Please contact the agent for a new link.
</p>
</div>
</div>
);
}
if (submitted) {
return (
<div className="min-h-screen bg-[#f5f5f5] flex items-center justify-center px-4">
<div className="bg-white rounded-[20px] p-8 max-w-md text-center shadow-[0px_10px_20px_rgba(217,217,217,0.5)]">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Image src="/assets/icons/verified-icon.svg" alt="Success" width={24} height={24} />
</div>
<h2 className="text-[20px] font-bold font-fractul text-[#00293D] mb-2">Thank You!</h2>
<p className="text-[14px] font-serif text-[#00293D]/70">
Your testimonial has been submitted successfully.
</p>
</div>
</div>
);
}
const agentName = [agent?.firstName, agent?.lastName].filter(Boolean).join(' ') || 'Agent';
return (
<div className="min-h-screen bg-[#f5f5f5] py-8 px-4">
<div className="max-w-lg mx-auto">
{/* Agent Info */}
<div className="bg-white rounded-[20px] p-6 mb-6 text-center shadow-[0px_10px_20px_rgba(217,217,217,0.5)]">
<h1 className="text-[24px] font-bold font-fractul text-[#00293D] mb-2">
Leave a Testimonial
</h1>
<p className="text-[14px] font-serif text-[#00293D]/70">
for <span className="font-bold">{agentName}</span>
{agent?.agentType?.name && <span> · {agent.agentType.name}</span>}
</p>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="bg-white rounded-[20px] p-6 shadow-[0px_10px_20px_rgba(217,217,217,0.5)]">
{/* Star Rating */}
<div className="mb-6">
<label className="block font-fractul font-bold text-[14px] text-[#00293D] mb-3">
Rating *
</label>
<div className="flex gap-2">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => setRating(star)}
className="transition-transform hover:scale-110"
>
<Image
src={star <= rating ? '/assets/icons/star-filled-icon.svg' : '/assets/icons/star-empty-icon.svg'}
alt={`${star} star`}
width={32}
height={32}
/>
</button>
))}
</div>
{errors.rating && <p className="text-red-500 text-[12px] mt-1">{errors.rating}</p>}
</div>
{/* Testimonial Text */}
<div className="mb-6">
<label className="block font-fractul font-bold text-[14px] text-[#00293D] mb-2">
Your Testimonial *
</label>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Share your experience working with this agent..."
rows={5}
maxLength={2000}
className="w-full border border-[#00293D]/10 rounded-[10px] p-4 font-serif text-[14px] text-[#00293D] placeholder:text-[#00293D]/30 resize-none focus:outline-none focus:border-[#E58625]"
/>
{errors.text && <p className="text-red-500 text-[12px] mt-1">{errors.text}</p>}
</div>
{/* Author Name */}
<div className="mb-6">
<label className="block font-fractul font-bold text-[14px] text-[#00293D] mb-2">
Your Name *
</label>
<input
type="text"
value={authorName}
onChange={(e) => setAuthorName(e.target.value)}
placeholder="Enter your name"
maxLength={100}
className="w-full border border-[#00293D]/10 rounded-[10px] p-4 font-serif text-[14px] text-[#00293D] placeholder:text-[#00293D]/30 focus:outline-none focus:border-[#E58625]"
/>
{errors.authorName && <p className="text-red-500 text-[12px] mt-1">{errors.authorName}</p>}
</div>
{/* Author Role */}
<div className="mb-8">
<label className="block font-fractul font-bold text-[14px] text-[#00293D] mb-2">
Your Role *
</label>
<select
value={authorRole}
onChange={(e) => setAuthorRole(e.target.value)}
className="w-full border border-[#00293D]/10 rounded-[10px] p-4 font-serif text-[14px] text-[#00293D] focus:outline-none focus:border-[#E58625] bg-white"
>
<option value="">Select your role</option>
{ROLE_OPTIONS.map((role) => (
<option key={role} value={role}>{role}</option>
))}
</select>
{errors.authorRole && <p className="text-red-500 text-[12px] mt-1">{errors.authorRole}</p>}
</div>
{/* Error message */}
{errors.submit && (
<p className="text-red-500 text-[14px] text-center mb-4">{errors.submit}</p>
)}
{/* Submit Button */}
<button
type="submit"
disabled={submitting}
className="w-full bg-[#e58625] text-white font-fractul font-bold text-[16px] py-4 rounded-[15px] hover:bg-[#d47920] transition-colors disabled:opacity-50"
>
{submitting ? 'Submitting...' : 'Submit Testimonial'}
</button>
</form>
</div>
</div>
);
}

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,15 +120,21 @@ 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>
{testimonialLink && (
<>
<div className="h-[46px] w-px bg-[#00293D]/10" />
<button
onClick={handleCopyLink}
@@ -155,15 +150,30 @@ export function TestimonialsForm() {
{copied ? 'Copied!' : 'Copy'}
</span>
</button>
</>
)}
</div>
</div>
</div>
{/* Testimonials Columns */}
{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">
{/* Sort Header */}
<div className="flex items-center gap-1 mb-4">
<span className="font-fractul font-medium text-[14px] text-black">
Sort By : Latest
@@ -175,10 +185,8 @@ export function TestimonialsForm() {
height={14}
/>
</div>
{/* Cards */}
<div className="space-y-4 max-h-[500px] overflow-y-auto pr-1 custom-scrollbar">
{LATEST_TESTIMONIALS.map((testimonial) => (
{latestTestimonials.map((testimonial) => (
<TestimonialCard key={testimonial.id} testimonial={testimonial} />
))}
</div>
@@ -186,7 +194,6 @@ export function TestimonialsForm() {
{/* 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
@@ -198,15 +205,14 @@ export function TestimonialsForm() {
height={14}
/>
</div>
{/* Cards */}
<div className="space-y-4 max-h-[500px] overflow-y-auto pr-1 custom-scrollbar">
{OLDEST_TESTIMONIALS.map((testimonial) => (
{oldestTestimonials.map((testimonial) => (
<TestimonialCard key={testimonial.id} testimonial={testimonial} />
))}
</div>
</div>
</div>
)}
{/* Custom scrollbar styles */}
<style jsx>{`

View File

@@ -0,0 +1,62 @@
import api from './api';
interface ApiResponse<T> {
success: boolean;
data: T;
}
export interface Testimonial {
id: string;
agentProfileId: string;
rating: number;
text: string;
authorName: string;
authorRole: string;
isPublished: boolean;
createdAt: string;
updatedAt: string;
}
export interface GenerateLinkResponse {
token: string;
}
export interface AgentByTokenResponse {
id: string;
firstName: string | null;
lastName: string | null;
avatar: string | null;
agentType: { name: string } | null;
}
class TestimonialsService {
private basePath = '/testimonials';
async generateLink(): Promise<GenerateLinkResponse> {
const response = await api.post<ApiResponse<GenerateLinkResponse>>(`${this.basePath}/generate-link`);
return response.data.data;
}
async getMyTestimonials(sort: 'latest' | 'oldest' = 'latest'): Promise<Testimonial[]> {
const response = await api.get<ApiResponse<Testimonial[]>>(`${this.basePath}/my?sort=${sort}`);
return response.data.data;
}
async getAgentTestimonials(agentProfileId: string, limit?: number): Promise<Testimonial[]> {
const params = limit ? `?limit=${limit}` : '';
const response = await api.get<ApiResponse<Testimonial[]>>(`${this.basePath}/agent/${agentProfileId}${params}`);
return response.data.data;
}
async getAgentByToken(token: string): Promise<AgentByTokenResponse> {
const response = await api.get<ApiResponse<AgentByTokenResponse>>(`${this.basePath}/submit/${token}`);
return response.data.data;
}
async submitTestimonial(token: string, data: { rating: number; text: string; authorName: string; authorRole: string }): Promise<Testimonial> {
const response = await api.post<ApiResponse<Testimonial>>(`${this.basePath}/submit/${token}`, data);
return response.data.data;
}
}
export const testimonialsService = new TestimonialsService();