diff --git a/src/app/(agent)/agent/dashboard/page.tsx b/src/app/(agent)/agent/dashboard/page.tsx
index f3ebe6d..bc0d36f 100644
--- a/src/app/(agent)/agent/dashboard/page.tsx
+++ b/src/app/(agent)/agent/dashboard/page.tsx
@@ -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,25 +394,27 @@ export default function AgentDashboard() {
}
content={
{mockData.preferredWorkEnvironment}
}
/>
-
- }
- content={“{mockData.testimonialHighlight}”
}
- />
+ {testimonials.length > 0 && (
+
+ }
+ content={“{testimonials[0].text.length > 150 ? testimonials[0].text.substring(0, 150) + '...' : testimonials[0].text}”
}
+ />
+ )}
{/* Specialization Section */}
{/* Testimonials Section */}
-
+
diff --git a/src/app/(user)/user/profile/[id]/page.tsx b/src/app/(user)/user/profile/[id]/page.tsx
index 128681e..908060c 100644
--- a/src/app/(user)/user/profile/[id]/page.tsx
+++ b/src/app/(user)/user/profile/[id]/page.tsx
@@ -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(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(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,25 +390,27 @@ export default function AgentProfileView() {
}
content={{mockData.preferredWorkEnvironment}
}
/>
-
- }
- content={“{mockData.testimonialHighlight}”
}
- />
+ {testimonials.length > 0 && (
+
+ }
+ content={“{testimonials[0].text.length > 150 ? testimonials[0].text.substring(0, 150) + '...' : testimonials[0].text}”
}
+ />
+ )}
{/* Specialization Section */}
{/* Testimonials Section */}
-
+
diff --git a/src/app/testimonials/[token]/page.tsx b/src/app/testimonials/[token]/page.tsx
new file mode 100644
index 0000000..5dc7342
--- /dev/null
+++ b/src/app/testimonials/[token]/page.tsx
@@ -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(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>({});
+
+ 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 = {};
+ 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 (
+
+ );
+ }
+
+ if (invalidToken) {
+ return (
+
+
+
+
+
+
Invalid Link
+
+ This testimonial link is invalid or has expired. Please contact the agent for a new link.
+
+
+
+ );
+ }
+
+ if (submitted) {
+ return (
+
+
+
+
+
+
Thank You!
+
+ Your testimonial has been submitted successfully.
+
+
+
+ );
+ }
+
+ const agentName = [agent?.firstName, agent?.lastName].filter(Boolean).join(' ') || 'Agent';
+
+ return (
+
+
+ {/* Agent Info */}
+
+
+ Leave a Testimonial
+
+
+ for {agentName}
+ {agent?.agentType?.name && · {agent.agentType.name} }
+
+
+
+ {/* Form */}
+
+
+
+ );
+}
diff --git a/src/components/profile/TestimonialsSection.tsx b/src/components/profile/TestimonialsSection.tsx
index c91def0..8cd5584 100644
--- a/src/components/profile/TestimonialsSection.tsx
+++ b/src/components/profile/TestimonialsSection.tsx
@@ -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 (
Testimonials
diff --git a/src/components/settings/TestimonialsForm.tsx b/src/components/settings/TestimonialsForm.tsx
index 550b5a5..1decf59 100644
--- a/src/components/settings/TestimonialsForm.tsx
+++ b/src/components/settings/TestimonialsForm.tsx
@@ -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 (
@@ -91,7 +43,7 @@ function TestimonialCard({ testimonial }: { testimonial: Testimonial }) {
{testimonial.authorName} ,
- {testimonial.authorType} · {testimonial.date}
+ {testimonial.authorRole} · {formatDate(testimonial.createdAt)}
);
@@ -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
([]);
+ const [oldestTestimonials, setOldestTestimonials] = useState([]);
+ 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() {
{/* Generate Link Button */}
-
- Generate Link
+
+ {generating ? 'Generating...' : 'Generate Link'}
{/* Link Input with Copy */}
- {testimonialLink}
+ {testimonialLink || 'Click "Generate Link" to create a shareable link'}
-
-
-
-
- {copied ? 'Copied!' : 'Copy'}
-
-
+ {testimonialLink && (
+ <>
+
+
+
+
+ {copied ? 'Copied!' : 'Copy'}
+
+
+ >
+ )}
{/* Testimonials Columns */}
-
- {/* Latest Column */}
-
- {/* Sort Header */}
-
-
- Sort By : Latest
-
-
+ {loading ? (
+
+ ) : latestTestimonials.length === 0 ? (
+
+
+ No testimonials yet
+
+
+ Generate a link and share it to collect testimonials
+
+
+ ) : (
+
+ {/* Latest Column */}
+
+
+
+ Sort By : Latest
+
+
+
+
+ {latestTestimonials.map((testimonial) => (
+
+ ))}
+
- {/* Cards */}
-
- {LATEST_TESTIMONIALS.map((testimonial) => (
-
- ))}
+ {/* Oldest Column */}
+
+
+
+ Sort By : Oldest
+
+
+
+
+ {oldestTestimonials.map((testimonial) => (
+
+ ))}
+
-
- {/* Oldest Column */}
-
- {/* Sort Header */}
-
-
- Sort By : Oldest
-
-
-
-
- {/* Cards */}
-
- {OLDEST_TESTIMONIALS.map((testimonial) => (
-
- ))}
-
-
-
+ )}
{/* Custom scrollbar styles */}