feat: Implement a new testimonial submission page and service, and update related profile and settings components.
This commit is contained in:
222
src/app/testimonials/[token]/page.tsx
Normal file
222
src/app/testimonials/[token]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user