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

@@ -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>
);
}