Compare commits

...

7 Commits

Author SHA1 Message Date
pradeepkumar
a07484af7e refactor: decouple /home from cross-route page/layout imports
Move /home into the (user) route group so it inherits UserLayout via the
router instead of importing (user)/layout.tsx manually, and render shared
HomeDashboard content in both /home and /user/dashboard instead of importing
the dashboard route's default page export. Removes the fragile cross-route
coupling while keeping the same URL, SEO metadata, and rendered output.
2026-06-23 17:13:55 +05:30
Chinraj P
987ca11a4d Added home page with dashboard content and SEO metadata 2026-06-23 14:14:08 +05:30
pradeepkumar
2245584734 fix: prevent profile bio/description horizontal overflow
Add min-w-0 to the flex-1 right content column so it can shrink below
its content width (flex items default to min-width:auto), and break-words
on the bio paragraph for long unbroken strings.
2026-06-23 12:58:56 +05:30
decea4687e Merge pull request 'feat: add SEO metadata and profile schema' (#1) from fix/profile-contact-improvements into main
Reviewed-on: #1
2026-06-19 05:44:14 +00:00
Chinraj P
376051c41e feat: add SEO metadata and profile schema 2026-06-18 14:37:55 +05:30
pradeepkumar
9ebc55de73 feat: cloud save-state icons for profile auto-save indicator 2026-06-12 07:47:21 +05:30
pradeepkumar
8f0becb53a fix: enable profile auto-save for all verification statuses including approved 2026-06-12 07:42:57 +05:30
42 changed files with 1606 additions and 449 deletions

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4s1.79-4 4-4h.71C7.37 7.69 9.48 6 12 6c3.04 0 5.5 2.46 5.5 5.5v.5H19c1.66 0 3 1.34 3 3s-1.34 3-3 3z" fill="#9CA3AF"/>
</svg>

After

Width:  |  Height:  |  Size: 408 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM10 17l-3.5-3.5 1.41-1.41L10 14.17 15.18 9l1.41 1.41L10 17z" fill="#22C55E"/>
</svg>

After

Width:  |  Height:  |  Size: 342 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z" fill="#E58625"/>
</svg>

After

Width:  |  Height:  |  Size: 311 B

View File

@@ -0,0 +1,35 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Agent Dashboard",
description:
"Manage your professional profile, availability, testimonials, connection requests, and client interactions on RE-Quest.",
robots: {
index: false,
follow: false,
},
openGraph: {
title: "Agent Dashboard",
description:
"Manage your professional profile, availability, testimonials, connection requests, and client interactions on RE-Quest.",
siteName: "RE-Quest",
type: "website",
},
twitter: {
card: "summary",
title: "Agent Dashboard",
description:
"Manage your professional profile, availability, testimonials, connection requests, and client interactions on RE-Quest.",
},
};
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}

View File

@@ -0,0 +1,20 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Edit Profile",
description:
"Update your professional profile, experience, certifications, availability, and contact information on RE-Quest.",
robots: {
index: false,
follow: false,
},
};
export default function EditLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}

View File

@@ -1,6 +1,7 @@
'use client';
import { useState, useRef, useEffect, useCallback } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { QuickLinks } from './components';
import DynamicSection from './components/DynamicSection';
@@ -48,9 +49,7 @@ export default function EditProfilePage() {
const repeatableDataRef = useRef<Record<string, RepeatableEntryData[]>>({});
// Dirty (unsaved) field slugs; repeatable sections tracked as `${slug}_entries`
const dirtySlugsRef = useRef<Set<string>>(new Set());
// Server auto-save stays off for APPROVED profiles — their field values are
// live, so changes only go out via the explicit Save button. Local drafts
// still work for everyone.
// Set once the profile loads; guards flushes from firing before then
const autoSaveEnabledRef = useRef(false);
const draftKeyRef = useRef<string | null>(null);
const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -233,12 +232,9 @@ export default function EditProfilePage() {
return;
}
// APPROVED profiles are live — keep their changes behind the explicit
// Save button. Everyone else gets server auto-save while they build.
autoSaveEnabledRef.current =
!profile.verificationStatus ||
profile.verificationStatus === 'NONE' ||
profile.verificationStatus === 'REJECTED';
// Auto-save for all statuses. Note: APPROVED profiles are live, so
// auto-saved edits update the public profile immediately.
autoSaveEnabledRef.current = true;
draftKeyRef.current = `agent-profile-draft:${profile.userId}`;
if (!profile.agentTypeId) {
@@ -640,25 +636,27 @@ export default function EditProfilePage() {
{/* Auto-save status */}
{(autoSaveStatus !== 'idle' || lastSavedAt) && (
<div className="flex items-center justify-end gap-2 px-1">
<span
className={`inline-block w-2 h-2 rounded-full ${
<Image
src={
autoSaveStatus === 'saving'
? 'bg-[#E58625] animate-pulse'
: autoSaveStatus === 'saved' || (autoSaveStatus === 'idle' && lastSavedAt)
? 'bg-green-500'
? '/assets/icons/cloud-saving-icon.svg'
: autoSaveStatus === 'error'
? 'bg-red-500'
: 'bg-gray-400'
}`}
></span>
? '/assets/icons/caution-icon.svg'
: autoSaveStatus === 'dirty'
? '/assets/icons/cloud-pending-icon.svg'
: '/assets/icons/cloud-saved-icon.svg'
}
alt={autoSaveStatus === 'saving' ? 'Saving' : autoSaveStatus === 'dirty' ? 'Unsaved changes' : autoSaveStatus === 'error' ? 'Save failed' : 'Saved'}
width={16}
height={16}
className={autoSaveStatus === 'saving' ? 'animate-pulse' : ''}
/>
<span className="text-[12px] font-serif text-[#00293D]/60">
{autoSaveStatus === 'saving' && 'Saving…'}
{autoSaveStatus === 'saved' && lastSavedAt && `All changes saved at ${formatTime(lastSavedAt)}`}
{autoSaveStatus === 'error' && 'Auto-save failed — retrying'}
{autoSaveStatus === 'dirty' &&
(autoSaveEnabledRef.current
? 'Unsaved changes'
: 'Unsaved changes — remember to save')}
(lastSavedAt ? `Last saved at ${formatTime(lastSavedAt)}` : 'Unsaved changes')}
{autoSaveStatus === 'idle' && lastSavedAt && `Last saved at ${formatTime(lastSavedAt)}`}
</span>
</div>
@@ -675,9 +673,7 @@ export default function EditProfilePage() {
hour: 'numeric',
minute: '2-digit',
})}
{autoSaveEnabledRef.current
? '. Keep editing — your progress is saved automatically.'
: '. Review them and press Save Changes to keep them.'}
. Keep editing your progress is saved automatically.
</p>
<button
onClick={() => setDraftRestoredAt(null)}

View File

@@ -0,0 +1,28 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Messages",
description:
"View and manage your conversations, client inquiries, and professional communications on RE-Quest.",
robots: {
index: false,
follow: false,
},
openGraph: {
title: "Messages",
description:
"View and manage your conversations, client inquiries, and professional communications on RE-Quest.",
siteName: "RE-Quest",
type: "website",
},
};
export default function MessageLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}

View File

@@ -0,0 +1,35 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Network",
description:
"Manage your professional connections, connection requests, and networking opportunities on RE-Quest.",
robots: {
index: false,
follow: false,
},
openGraph: {
title: "Network",
description:
"Manage your professional connections, connection requests, and networking opportunities on RE-Quest.",
siteName: "RE-Quest",
type: "website",
},
twitter: {
card: "summary",
title: "Network",
description:
"Manage your professional connections, connection requests, and networking opportunities on RE-Quest.",
},
};
export default function NetworkLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}

View File

@@ -0,0 +1,35 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Notifications",
description:
"View and manage your notifications, updates, connection requests, and account activity on RE-Quest.",
robots: {
index: false,
follow: false,
},
openGraph: {
title: "Notifications",
description:
"View and manage your notifications, updates, connection requests, and account activity on RE-Quest.",
siteName: "RE-Quest",
type: "website",
},
twitter: {
card: "summary",
title: "Notifications",
description:
"View and manage your notifications, updates, connection requests, and account activity on RE-Quest.",
},
};
export default function NotificationsLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}

View File

@@ -0,0 +1,35 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Settings",
description:
"Manage your account settings, privacy preferences, notifications, billing information, password, and testimonials on RE-Quest.",
robots: {
index: false,
follow: false,
},
openGraph: {
title: "Settings",
description:
"Manage your account settings, privacy preferences, notifications, billing information, password, and testimonials on RE-Quest.",
siteName: "RE-Quest",
type: "website",
},
twitter: {
card: "summary",
title: "Settings",
description:
"Manage your account settings, privacy preferences, notifications, billing information, password, and testimonials on RE-Quest.",
},
};
export default function SettingsLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}

View File

@@ -0,0 +1,19 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Forgot Password",
description:
"Reset your RE-Quest account password securely and regain access to your account.",
robots: {
index: false,
follow: false,
},
};
export default function ForgotPasswordLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,19 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Login",
description:
"Sign in to your RE-Quest account to connect with real estate professionals and manage your profile.",
robots: {
index: false,
follow: false,
},
};
export default function LoginLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,19 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Reset Password",
description:
"Create a new password for your RE-Quest account and securely continue using the platform.",
robots: {
index: false,
follow: false,
},
};
export default function ResetPasswordLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,19 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Create Account",
description:
"Create a RE-Quest account to connect with trusted real estate professionals and explore opportunities.",
robots: {
index: false,
follow: false,
},
};
export default function SignupLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,19 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Verify Email",
description:
"Verify your email address to activate and secure your RE-Quest account.",
robots: {
index: false,
follow: false,
},
};
export default function VerifyEmailLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,11 @@
import { HomeDashboard } from '@/components/home/HomeDashboard';
export const metadata = {
title: 'RE-Quest - Connect with Trusted Real Estate Professionals',
description:
'Discover verified real estate professionals for buying, selling, renting, and investing. Search by location, specialization, and expertise to connect with trusted agents on RE-Quest.',
};
export default function HomePage() {
return <HomeDashboard />;
}

View File

@@ -1,23 +1,26 @@
'use client';
"use client";
import { useSession } from 'next-auth/react';
import { useRouter, usePathname } from 'next/navigation';
import { useEffect } from 'react';
import Image from 'next/image';
import { Footer } from '@/components/layout/Footer';
import { CommonHeader } from '@/components/layout/CommonHeader';
import { PresenceProvider } from '@/components/providers/presence-provider';
import { useSession } from "next-auth/react";
import { useRouter, usePathname } from "next/navigation";
import { useEffect } from "react";
import Image from "next/image";
import { Footer } from "@/components/layout/Footer";
import { CommonHeader } from "@/components/layout/CommonHeader";
import { PresenceProvider } from "@/components/providers/presence-provider";
// Pages that don't require authentication
const publicPaths = [
'/user/dashboard',
'/user/profiles',
'/user/profile/', // Agent profile view (includes /user/profile/[id])
"/home",
"/user/dashboard",
"/user/profiles",
"/user/profile/", // Agent profile view (includes /user/profile/[id])
];
// Check if current path is public
const isPublicPath = (pathname: string) => {
return publicPaths.some(path => pathname === path || pathname.startsWith(path));
return publicPaths.some(
(path) => pathname === path || pathname.startsWith(path),
);
};
export default function UserLayout({
@@ -28,11 +31,12 @@ export default function UserLayout({
const { data: session, status } = useSession();
const router = useRouter();
const pathname = usePathname();
const isDashboard = pathname === '/user/dashboard';
// const isDashboard = pathname === "/user/dashboard";
const isDashboard = pathname === "/user/dashboard" || pathname === "/home";
const isPublic = isPublicPath(pathname);
useEffect(() => {
if (status === 'loading') return;
if (status === "loading") return;
// Allow public pages without authentication.
// Agents are intentionally allowed to land on /user/dashboard — the logo
@@ -49,19 +53,31 @@ export default function UserLayout({
// Redirect agents to agent dashboard for protected user pages
const userRole = (session.user as any)?.role;
if (userRole === 'AGENT') {
router.replace('/agent/dashboard');
if (userRole === "AGENT") {
router.replace("/agent/dashboard");
}
}, [session, status, router, pathname, isPublic]);
const splashLoading = (
<div
className="min-h-screen flex flex-col items-center justify-center"
style={{ background: 'linear-gradient(to bottom, #c4d9d4, #f0f5fc)' }}
style={{ background: "linear-gradient(to bottom, #c4d9d4, #f0f5fc)" }}
>
<Image src="/assets/images/splash-house.png" alt="" width={150} height={108} priority />
<Image
src="/assets/images/splash-house.png"
alt=""
width={150}
height={108}
priority
/>
<div className="mt-[35px]">
<Image src="/assets/images/splash-logo.png" alt="RE-Quest" width={264} height={55} priority />
<Image
src="/assets/images/splash-logo.png"
alt="RE-Quest"
width={264}
height={55}
priority
/>
</div>
<div className="mt-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#00293d]" />
@@ -70,7 +86,7 @@ export default function UserLayout({
);
// Show loading only for protected pages while checking auth
if (status === 'loading' && !isPublic) {
if (status === "loading" && !isPublic) {
return splashLoading;
}
@@ -105,9 +121,7 @@ export default function UserLayout({
</div>
{/* Main Content */}
<main className="flex-1">
{children}
</main>
<main className="flex-1">{children}</main>
</>
)}

View File

@@ -0,0 +1,20 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "User Dashboard",
description:
"Access your RE-Quest dashboard to manage your profile, messages, notifications, and account activity.",
robots: {
index: false,
follow: false,
},
};
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -1,53 +1,7 @@
'use client';
import { useState, useEffect } from 'react';
import { HeroSection } from '@/components/home/HeroSection';
import { FeaturesSection } from '@/components/home/FeaturesSection';
import { TopProfessionals } from '@/components/home/TopProfessionals';
import { TestimonialsSection } from '@/components/home/TestimonialsSection';
import { cmsService, resolveImageUrl } from '@/services/cms.service';
import type { HeroContent, FeaturesContent, TopProfessionalsContent, TestimonialsContent, CmsContentRecord } from '@/types/cms';
import { HomeDashboard } from '@/components/home/HomeDashboard';
export default function UserDashboard() {
const [cmsData, setCmsData] = useState<Record<string, unknown>>({});
const [cmsLoaded, setCmsLoaded] = useState(false);
useEffect(() => {
const fetchCms = async () => {
try {
const sections = await cmsService.getPageContent('landing');
const data: Record<string, unknown> = {};
for (const s of sections) {
const content = s.content as Record<string, unknown>;
// Resolve S3 keys in image fields
if (s.sectionKey === 'features' && Array.isArray(content.features)) {
for (const feat of content.features as { iconPath?: string }[]) {
if (feat.iconPath) feat.iconPath = await resolveImageUrl(feat.iconPath);
}
}
if (s.sectionKey === 'testimonials' && Array.isArray(content.stats)) {
for (const stat of content.stats as { iconPath?: string }[]) {
if (stat.iconPath) stat.iconPath = await resolveImageUrl(stat.iconPath);
}
}
data[s.sectionKey] = content;
}
setCmsData(data);
} catch {
// Use default content on error
} finally {
setCmsLoaded(true);
}
};
fetchCms();
}, []);
return (
<div>
<HeroSection content={cmsData.hero as HeroContent | undefined} />
{cmsLoaded && <FeaturesSection content={cmsData.features as FeaturesContent | undefined} />}
<TopProfessionals content={cmsData.topProfessionals as TopProfessionalsContent | undefined} />
<TestimonialsSection content={cmsData.testimonials as TestimonialsContent | undefined} />
</div>
);
return <HomeDashboard />;
}

View File

@@ -0,0 +1,20 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Messages",
description:
"View and manage conversations with real estate professionals through RE-Quest messaging.",
robots: {
index: false,
follow: false,
},
};
export default function MessageLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,20 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Notifications",
description:
"Stay updated with connection requests, messages, profile activity, and important alerts on RE-Quest.",
robots: {
index: false,
follow: false,
},
};
export default function NotificationsLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,25 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Professional Profile",
description:
"View verified real estate professionals, agents, and lenders on RE-Quest.",
openGraph: {
title: "Professional Profile",
description:
"View verified real estate professionals, agents, and lenders on RE-Quest.",
type: "profile",
siteName: "RE-Quest",
},
alternates: {
canonical: "https://re-quest.com/user/profile",
},
};
export default function ProfileLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -1,9 +1,9 @@
'use client';
"use client";
import { useState, useEffect } from 'react';
import Image from 'next/image';
import { useParams } from 'next/navigation';
import { useSession } from 'next-auth/react';
import { useState, useEffect } from "react";
import Image from "next/image";
import { useParams } from "next/navigation";
import { useSession } from "next-auth/react";
// Import shared components
import {
@@ -14,43 +14,68 @@ import {
TestimonialsSection,
StatusButtons,
ContactInfo,
} from '@/components/profile';
import { ConnectRequestModal } from '@/components/modals';
import { MobileBackButton } from '@/components/layout/MobileBackButton';
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, mapFieldValuesToWorkEnvironment, mapFieldValuesToPersonalTagline, ExperienceData, SpecializationFieldsData, ProfileCardData, ContactInfoData, AvailabilityData, WorkEnvironmentData, PersonalTaglineData } from '@/utils/profileDataMapper';
} from "@/components/profile";
import { ConnectRequestModal } from "@/components/modals";
import { MobileBackButton } from "@/components/layout/MobileBackButton";
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,
mapFieldValuesToWorkEnvironment,
mapFieldValuesToPersonalTagline,
ExperienceData,
SpecializationFieldsData,
ProfileCardData,
ContactInfoData,
AvailabilityData,
WorkEnvironmentData,
PersonalTaglineData,
} from "@/utils/profileDataMapper";
// Default work environment data
const defaultWorkEnvironmentData: WorkEnvironmentData = {
label: 'Preferred Work Environment',
content: '',
label: "Preferred Work Environment",
content: "",
};
// Default personal tagline data
const defaultPersonalTaglineData: PersonalTaglineData = {
label: 'Personal Tagline',
content: '',
label: "Personal Tagline",
content: "",
};
// Default experience data when no field values are available
const defaultExperience: ExperienceData = {
years: '-',
yearsLabel: 'Years in Experience',
contracts: '-',
contractsLabel: 'Number of contracts closed',
years: "-",
yearsLabel: "Years in Experience",
contracts: "-",
contractsLabel: "Number of contracts closed",
licensingAreas: [],
licensingAreasLabel: 'Licensing & Areas',
licensingAreasLabel: "Licensing & Areas",
expertiseYears: [],
expertiseYearsLabel: 'Areas in expertise & Years',
expertiseYearsLabel: "Areas in expertise & Years",
certifications: [],
certificationsLabel: 'Certifications',
agencyName: '',
agencyDesignation: '',
certificationsLabel: "Certifications",
agencyName: "",
agencyDesignation: "",
agencyShowDesignation: false,
agencySectionLabel: 'Real Estate Agency & Designation',
agencySectionLabel: "Real Estate Agency & Designation",
};
// Default specialization fields data when no field values are available
@@ -60,7 +85,7 @@ const defaultSpecializationFieldsData: SpecializationFieldsData = {
// Default profile card data when no field values are available
const defaultProfileCardData: ProfileCardData = {
bio: '',
bio: "",
expertise: [],
serviceAreas: [],
city: null,
@@ -75,9 +100,9 @@ const defaultContactInfoData: ContactInfoData = {
// Default availability data when no field values are available
const defaultAvailabilityData: AvailabilityData = {
type: '',
type: "",
schedule: [],
label: 'Availability',
label: "Availability",
};
export default function AgentProfileView() {
@@ -87,31 +112,46 @@ export default function AgentProfileView() {
const [agentProfile, setAgentProfile] = useState<AgentProfile | null>(null);
const [fieldValues, setFieldValues] = useState<FieldValueResponse[]>([]);
const [experienceData, setExperienceData] = useState<ExperienceData>(defaultExperience);
const [specializationFieldsData, setSpecializationFieldsData] = useState<SpecializationFieldsData>(defaultSpecializationFieldsData);
const [profileCardData, setProfileCardData] = useState<ProfileCardData>(defaultProfileCardData);
const [contactInfoData, setContactInfoData] = useState<ContactInfoData>(defaultContactInfoData);
const [availabilityData, setAvailabilityData] = useState<AvailabilityData>(defaultAvailabilityData);
const [workEnvironmentData, setWorkEnvironmentData] = useState<WorkEnvironmentData>(defaultWorkEnvironmentData);
const [personalTaglineData, setPersonalTaglineData] = useState<PersonalTaglineData>(defaultPersonalTaglineData);
const [experienceData, setExperienceData] =
useState<ExperienceData>(defaultExperience);
const [specializationFieldsData, setSpecializationFieldsData] =
useState<SpecializationFieldsData>(defaultSpecializationFieldsData);
const [profileCardData, setProfileCardData] = useState<ProfileCardData>(
defaultProfileCardData,
);
const [contactInfoData, setContactInfoData] = useState<ContactInfoData>(
defaultContactInfoData,
);
const [availabilityData, setAvailabilityData] = useState<AvailabilityData>(
defaultAvailabilityData,
);
const [workEnvironmentData, setWorkEnvironmentData] =
useState<WorkEnvironmentData>(defaultWorkEnvironmentData);
const [personalTaglineData, setPersonalTaglineData] =
useState<PersonalTaglineData>(defaultPersonalTaglineData);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [imageError, setImageError] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [testimonials, setTestimonials] = useState<{ id: string; text: string; author: string; role: string; rating: number }[]>([]);
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);
const [connectionRequestId, setConnectionRequestId] = useState<string | null>(null);
const [connectionStatus, setConnectionStatus] =
useState<ConnectionStatus | null>(null);
const [connectionRequestId, setConnectionRequestId] = useState<string | null>(
null,
);
// Helper to check if avatar is an S3 key (not a full URL or local path)
const isS3Key = (avatar: string | null | undefined): boolean => {
if (!avatar) return false;
// S3 keys don't start with http or /
return !avatar.startsWith('http') && !avatar.startsWith('/');
return !avatar.startsWith("http") && !avatar.startsWith("/");
};
// Fetch agent profile and field values on mount
@@ -124,41 +164,58 @@ export default function AgentProfileView() {
setError(null);
// 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[]),
]);
const [profile, fieldValuesResponse, testimonialsData] =
await Promise.all([
agentsService.getAgentById(id),
agentsService.getFieldValuesByAgentId(id),
testimonialsService
.getAgentTestimonials(id)
.catch(() => [] as Testimonial[]),
]);
setAgentProfile(profile);
setFieldValues(fieldValuesResponse.fieldValues);
// Map field values to experience data structure
const mappedExperience = mapFieldValuesToExperience(fieldValuesResponse.fieldValues);
const mappedExperience = mapFieldValuesToExperience(
fieldValuesResponse.fieldValues,
);
setExperienceData(mappedExperience);
// Map field values to specialization fields data (only fields from "Specialization" section)
const mappedSpecializationFields = mapFieldValuesToSpecializationFields(fieldValuesResponse.fieldValues);
const mappedSpecializationFields = mapFieldValuesToSpecializationFields(
fieldValuesResponse.fieldValues,
);
setSpecializationFieldsData(mappedSpecializationFields);
// Map field values to profile card data (bio, expertise, location)
const mappedProfileCard = mapFieldValuesToProfileCard(fieldValuesResponse.fieldValues);
const mappedProfileCard = mapFieldValuesToProfileCard(
fieldValuesResponse.fieldValues,
);
setProfileCardData(mappedProfileCard);
// Map field values to contact info data (email, phone)
const mappedContactInfo = mapFieldValuesToContactInfo(fieldValuesResponse.fieldValues);
const mappedContactInfo = mapFieldValuesToContactInfo(
fieldValuesResponse.fieldValues,
);
setContactInfoData(mappedContactInfo);
// Map field values to availability data
const mappedAvailability = mapFieldValuesToAvailability(fieldValuesResponse.fieldValues);
const mappedAvailability = mapFieldValuesToAvailability(
fieldValuesResponse.fieldValues,
);
setAvailabilityData(mappedAvailability);
// Map field values to work environment data
const mappedWorkEnv = mapFieldValuesToWorkEnvironment(fieldValuesResponse.fieldValues);
const mappedWorkEnv = mapFieldValuesToWorkEnvironment(
fieldValuesResponse.fieldValues,
);
setWorkEnvironmentData(mappedWorkEnv);
// Map field values to personal tagline data
const mappedTagline = mapFieldValuesToPersonalTagline(fieldValuesResponse.fieldValues);
const mappedTagline = mapFieldValuesToPersonalTagline(
fieldValuesResponse.fieldValues,
);
setPersonalTaglineData(mappedTagline);
// Map testimonials for TestimonialsSection
@@ -169,29 +226,33 @@ export default function AgentProfileView() {
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 {
const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar);
const presignedUrl = await uploadService.getPresignedDownloadUrl(
profile.avatar,
);
setAvatarUrl(presignedUrl);
} catch (avatarErr) {
console.error('Failed to get avatar URL:', avatarErr);
console.error("Failed to get avatar URL:", avatarErr);
}
} else if (profile.avatar) {
setAvatarUrl(profile.avatar);
}
} catch (err: any) {
console.error('Failed to fetch profile:', err);
console.error("Failed to fetch profile:", err);
const status = err?.response?.status;
if (status === 403) {
setError('This profile is private. You don\u2019t have permission to view it.');
setError(
"This profile is private. You don\u2019t have permission to view it.",
);
} else if (status === 404) {
setError('Profile not found.');
setError("Profile not found.");
} else {
setError('Failed to load profile data');
setError("Failed to load profile data");
}
} finally {
setLoading(false);
@@ -207,11 +268,12 @@ export default function AgentProfileView() {
if (!id || !session) return;
try {
const statusResponse = await connectionRequestsService.getConnectionStatus(id);
const statusResponse =
await connectionRequestsService.getConnectionStatus(id);
setConnectionStatus(statusResponse?.status || null);
setConnectionRequestId(statusResponse?.id || null);
} catch (err) {
console.error('Failed to fetch connection status:', err);
console.error("Failed to fetch connection status:", err);
// Non-critical error, don't show to user
}
};
@@ -221,7 +283,7 @@ export default function AgentProfileView() {
// Handle connection request success
const handleConnectionSuccess = () => {
setConnectionStatus('PENDING');
setConnectionStatus("PENDING");
};
// Handle unlink/disconnect
@@ -233,7 +295,7 @@ export default function AgentProfileView() {
setConnectionStatus(null);
setConnectionRequestId(null);
} catch (err) {
console.error('Failed to unlink connection:', err);
console.error("Failed to unlink connection:", err);
}
};
@@ -249,12 +311,12 @@ export default function AgentProfileView() {
}
// Check for null, undefined, or empty string
if (!agentProfile?.avatar || agentProfile.avatar.trim() === '') {
if (!agentProfile?.avatar || agentProfile.avatar.trim() === "") {
return null;
}
// For relative paths (local assets), return as-is
if (agentProfile.avatar.startsWith('/')) {
if (agentProfile.avatar.startsWith("/")) {
return agentProfile.avatar;
}
@@ -264,12 +326,15 @@ export default function AgentProfileView() {
// Format member since date
const formatMemberSince = (dateString?: string) => {
if (!dateString) return 'Member';
if (!dateString) return "Member";
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
return date.toLocaleDateString("en-US", {
month: "long",
year: "numeric",
});
} catch {
return 'Member';
return "Member";
}
};
@@ -279,35 +344,64 @@ export default function AgentProfileView() {
<div className="flex items-center justify-center h-[calc(100vh-180px)]">
<div className="flex flex-col items-center gap-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#E58625]"></div>
<p className="text-[14px] font-serif text-[#00293D]/70">Loading profile...</p>
<p className="text-[14px] font-serif text-[#00293D]/70">
Loading profile...
</p>
</div>
</div>
</div>
);
}
const isPermissionError = error?.includes('permission') || error?.includes('private');
const isPermissionError =
error?.includes("permission") || error?.includes("private");
if (error || !agentProfile) {
return (
<div className="max-w-7xl mx-auto px-4 lg:px-8 py-6">
<div className="flex items-center justify-center h-[calc(100vh-180px)]">
<div className="bg-white rounded-[20px] p-8 shadow-[0px_10px_20px_rgba(217,217,217,0.5)] max-w-md text-center">
<div className={`w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 ${isPermissionError ? 'bg-[#e58625]/10' : 'bg-red-100'}`}>
<div
className={`w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 ${isPermissionError ? "bg-[#e58625]/10" : "bg-red-100"}`}
>
{isPermissionError ? (
<svg className="w-8 h-8 text-[#e58625]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
<svg
className="w-8 h-8 text-[#e58625]"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
) : (
<svg className="w-8 h-8 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
className="w-8 h-8 text-red-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
)}
</div>
<h2 className="text-[18px] font-bold font-serif text-[#00293D] mb-2">
{isPermissionError ? 'Profile Not Available' : 'Unable to Load Profile'}
{isPermissionError
? "Profile Not Available"
: "Unable to Load Profile"}
</h2>
<p className="text-[14px] font-serif text-[#00293D]/70 mb-4">{error || 'Profile not found'}</p>
<p className="text-[14px] font-serif text-[#00293D]/70 mb-4">
{error || "Profile not found"}
</p>
<button
onClick={() => window.history.back()}
className="px-6 py-2 bg-[#E58625] rounded-full text-[14px] font-semibold font-serif text-white hover:bg-[#E58625]/90 transition-colors"
@@ -319,164 +413,265 @@ export default function AgentProfileView() {
</div>
);
}
const profileImage = getProfileImageUrl();
const profileSchema = {
"@context": "https://schema.org",
"@type": "Person",
name: `${agentProfile.firstName} ${agentProfile.lastName}`.trim(),
jobTitle: agentProfile.agentType?.name || "Real Estate Professional",
description:
profileCardData.bio ||
agentProfile.bio ||
"Verified real estate professional on RE-Quest.",
image: profileImage
? profileImage.startsWith("http")
? profileImage
: `https://re-quest.com${profileImage}`
: undefined,
email: agentProfile.email || agentProfile.user?.email || undefined,
telephone: agentProfile.phone || contactInfoData.phone || undefined,
url: `https://re-quest.com/user/profile/${id}`,
mainEntityOfPage: {
"@type": "WebPage",
"@id": `https://re-quest.com/user/profile/${id}`,
},
address: {
"@type": "PostalAddress",
addressLocality: profileCardData.city || undefined,
addressRegion: profileCardData.state || undefined,
},
memberOf: {
"@type": "Organization",
name: "RE-Quest",
url: "https://re-quest.com",
},
sameAs: [`https://re-quest.com/user/profile/${id}`],
};
return (
<div className="max-w-7xl mx-auto px-4 lg:px-8 pt-2 pb-6 space-y-6">
<MobileBackButton label="Back" fallbackHref="/user/profiles" alwaysShow />
{/* Main Layout - Responsive: Column on mobile, Row on desktop */}
<div className="flex flex-col lg:flex-row gap-6 lg:items-stretch">
{/* Left Sidebar - Status & Contact */}
<div className="w-full lg:w-[280px] flex-shrink-0 space-y-4 flex flex-col items-center lg:items-start">
{/* Profile Image */}
<div className="relative w-[200px] lg:w-[260px]">
<div className="w-[200px] h-[200px] lg:w-[260px] lg:h-[260px] rounded-[15px] overflow-hidden bg-[#e8e8e8] relative">
{/* Shimmer while loading, initials only on error/no image */}
{imageError || !getProfileImageUrl() ? (
<div className="absolute inset-0 bg-[#c4d9d4] flex items-center justify-center rounded-[15px]">
<span className="font-bold text-[#00293d] text-[80px]">{agentProfile?.firstName?.[0]?.toUpperCase() || '?'}</span>
</div>
) : !imageLoaded ? (
<div className="absolute inset-0 shimmer-loading rounded-[15px]" />
) : null}
{getProfileImageUrl() && (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
ref={(el) => { if (el?.complete) { if (el.naturalWidth > 0) setImageLoaded(true); else setImageError(true); } }}
src={getProfileImageUrl()!}
alt="Profile"
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setImageLoaded(true)}
onError={() => setImageError(true)}
/>
</>
)}
{/* Gradient Overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-black/20 to-transparent pointer-events-none" />
</div>
</div>
{/* Status Buttons - Public view shows availability status with Connect/Pending/Unlink button */}
<StatusButtons
isAvailable={agentProfile.isAvailable ?? true}
connectionStatus={connectionStatus}
onConnectClick={() => setShowConnectModal(true)}
onUnlinkClick={handleUnlink}
/>
{/* Contact Info */}
<ContactInfo
email={agentProfile.email || agentProfile.user?.email || contactInfoData.email || ''}
phone={agentProfile.phone || contactInfoData.phone || ''}
/>
</div>
{/* Right Content - Profile Info + Experience + All Sections */}
<div className="flex-1 space-y-4">
{/* Profile Card - No edit button for user view */}
<ProfileCard
firstName={agentProfile.firstName}
lastName={agentProfile.lastName}
isVerified={agentProfile.isVerified}
title={agentProfile.agentType?.name || 'Real Estate Agent'}
location={profileCardData.city && profileCardData.state
? `${profileCardData.state}, ${profileCardData.city}`
: profileCardData.state || profileCardData.city || agentProfile.serviceAreas?.[0] || '-'}
memberSince={formatMemberSince((agentProfile as unknown as { createdAt?: string }).createdAt)}
bio={profileCardData.bio || agentProfile.bio || ''}
expertise={profileCardData.expertise.length > 0 ? profileCardData.expertise : (agentProfile.specializations || [])}
showEditButton={false}
messageHref={`/user/message?agentProfileId=${agentProfile.id}`}
connectionStatus={connectionStatus}
isAvailable={agentProfile.isAvailable ?? true}
onConnectClick={() => setShowConnectModal(true)}
onUnlinkClick={handleUnlink}
/>
{/* Experience Section - Dynamic data from profile fields */}
<ExperienceSection experience={experienceData} />
{/* Info Cards Section */}
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-6">
<InfoCard
title={availabilityData.label}
icon={
<Image
src="/assets/icons/availability-clock-icon.svg"
alt={availabilityData.label}
width={28}
height={31}
/>
}
content={
<div>
<div className="space-y-1">
{availabilityData.schedule.length > 0 ? (
availabilityData.schedule.map((item, index) => (
<p key={index} className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]">{item}</p>
))
) : (
<p className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]/60">Not specified</p>
)}
</div>
</div>
}
/>
<InfoCard
title={workEnvironmentData.label}
icon={
<Image
src="/assets/icons/work-environment-icon.svg"
alt={workEnvironmentData.label}
width={28}
height={28}
/>
}
content={
workEnvironmentData.content ? (
<p className="font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]">{workEnvironmentData.content}</p>
) : (
<p className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]/60">Not specified</p>
)
}
/>
<InfoCard
title={personalTaglineData.label}
icon={
<Image
src="/assets/icons/testimonial-star-icon.svg"
alt={personalTaglineData.label}
width={28}
height={28}
/>
}
content={
personalTaglineData.content ? (
<p className="text-[14px] font-semibold font-serif leading-[19px] text-center text-[#00293D]">&ldquo;{personalTaglineData.content.length > 150 ? personalTaglineData.content.substring(0, 150) + '...' : personalTaglineData.content}&rdquo;</p>
) : (
<p className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]/60">Not specified</p>
)
}
/>
</div>
{/* Specialization Section */}
<SpecializationSection fieldsData={specializationFieldsData} />
{/* Testimonials Section */}
<TestimonialsSection testimonials={testimonials} />
</div>
</div>
{/* Connect Request Modal */}
<ConnectRequestModal
isOpen={showConnectModal}
onClose={() => setShowConnectModal(false)}
agentProfileId={id}
agentName={`${agentProfile.firstName || ''} ${agentProfile.lastName || ''}`.trim() || 'Agent'}
existingStatus={connectionStatus}
onSuccess={handleConnectionSuccess}
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(profileSchema),
}}
/>
</div>
<div className="max-w-7xl mx-auto px-4 lg:px-8 pt-2 pb-6 space-y-6">
<MobileBackButton
label="Back"
fallbackHref="/user/profiles"
alwaysShow
/>
{/* Main Layout - Responsive: Column on mobile, Row on desktop */}
<div className="flex flex-col lg:flex-row gap-6 lg:items-stretch">
{/* Left Sidebar - Status & Contact */}
<div className="w-full lg:w-[280px] flex-shrink-0 space-y-4 flex flex-col items-center lg:items-start">
{/* Profile Image */}
<div className="relative w-[200px] lg:w-[260px]">
<div className="w-[200px] h-[200px] lg:w-[260px] lg:h-[260px] rounded-[15px] overflow-hidden bg-[#e8e8e8] relative">
{/* Shimmer while loading, initials only on error/no image */}
{imageError || !getProfileImageUrl() ? (
<div className="absolute inset-0 bg-[#c4d9d4] flex items-center justify-center rounded-[15px]">
<span className="font-bold text-[#00293d] text-[80px]">
{agentProfile?.firstName?.[0]?.toUpperCase() || "?"}
</span>
</div>
) : !imageLoaded ? (
<div className="absolute inset-0 shimmer-loading rounded-[15px]" />
) : null}
{getProfileImageUrl() && (
<>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
ref={(el) => {
if (el?.complete) {
if (el.naturalWidth > 0) setImageLoaded(true);
else setImageError(true);
}
}}
src={getProfileImageUrl()!}
alt="Profile"
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${imageLoaded ? "opacity-100" : "opacity-0"}`}
onLoad={() => setImageLoaded(true)}
onError={() => setImageError(true)}
/>
</>
)}
{/* Gradient Overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-black/20 to-transparent pointer-events-none" />
</div>
</div>
{/* Status Buttons - Public view shows availability status with Connect/Pending/Unlink button */}
<StatusButtons
isAvailable={agentProfile.isAvailable ?? true}
connectionStatus={connectionStatus}
onConnectClick={() => setShowConnectModal(true)}
onUnlinkClick={handleUnlink}
/>
{/* Contact Info */}
<ContactInfo
email={
agentProfile.email ||
agentProfile.user?.email ||
contactInfoData.email ||
""
}
phone={agentProfile.phone || contactInfoData.phone || ""}
/>
</div>
{/* Right Content - Profile Info + Experience + All Sections */}
<div className="flex-1 min-w-0 space-y-4">
{/* Profile Card - No edit button for user view */}
<ProfileCard
firstName={agentProfile.firstName}
lastName={agentProfile.lastName}
isVerified={agentProfile.isVerified}
title={agentProfile.agentType?.name || "Real Estate Agent"}
location={
profileCardData.city && profileCardData.state
? `${profileCardData.state}, ${profileCardData.city}`
: profileCardData.state ||
profileCardData.city ||
agentProfile.serviceAreas?.[0] ||
"-"
}
memberSince={formatMemberSince(
(agentProfile as unknown as { createdAt?: string }).createdAt,
)}
bio={profileCardData.bio || agentProfile.bio || ""}
expertise={
profileCardData.expertise.length > 0
? profileCardData.expertise
: agentProfile.specializations || []
}
showEditButton={false}
messageHref={`/user/message?agentProfileId=${agentProfile.id}`}
connectionStatus={connectionStatus}
isAvailable={agentProfile.isAvailable ?? true}
onConnectClick={() => setShowConnectModal(true)}
onUnlinkClick={handleUnlink}
/>
{/* Experience Section - Dynamic data from profile fields */}
<ExperienceSection experience={experienceData} />
{/* Info Cards Section */}
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-6">
<InfoCard
title={availabilityData.label}
icon={
<Image
src="/assets/icons/availability-clock-icon.svg"
alt={availabilityData.label}
width={28}
height={31}
/>
}
content={
<div>
<div className="space-y-1">
{availabilityData.schedule.length > 0 ? (
availabilityData.schedule.map((item, index) => (
<p
key={index}
className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]"
>
{item}
</p>
))
) : (
<p className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]/60">
Not specified
</p>
)}
</div>
</div>
}
/>
<InfoCard
title={workEnvironmentData.label}
icon={
<Image
src="/assets/icons/work-environment-icon.svg"
alt={workEnvironmentData.label}
width={28}
height={28}
/>
}
content={
workEnvironmentData.content ? (
<p className="font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]">
{workEnvironmentData.content}
</p>
) : (
<p className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]/60">
Not specified
</p>
)
}
/>
<InfoCard
title={personalTaglineData.label}
icon={
<Image
src="/assets/icons/testimonial-star-icon.svg"
alt={personalTaglineData.label}
width={28}
height={28}
/>
}
content={
personalTaglineData.content ? (
<p className="text-[14px] font-semibold font-serif leading-[19px] text-center text-[#00293D]">
&ldquo;
{personalTaglineData.content.length > 150
? personalTaglineData.content.substring(0, 150) + "..."
: personalTaglineData.content}
&rdquo;
</p>
) : (
<p className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]/60">
Not specified
</p>
)
}
/>
</div>
{/* Specialization Section */}
<SpecializationSection fieldsData={specializationFieldsData} />
{/* Testimonials Section */}
<TestimonialsSection testimonials={testimonials} />
</div>
</div>
{/* Connect Request Modal */}
<ConnectRequestModal
isOpen={showConnectModal}
onClose={() => setShowConnectModal(false)}
agentProfileId={id}
agentName={
`${agentProfile.firstName || ""} ${agentProfile.lastName || ""}`.trim() ||
"Agent"
}
existingStatus={connectionStatus}
onSuccess={handleConnectionSuccess}
/>
</div>
</>
);
}

View File

@@ -0,0 +1,68 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Find Real Estate Professionals",
description:
"Browse verified real estate professionals, agents, and lenders on RE-Quest. Search by expertise, location, and specialization.",
keywords: [
"real estate professionals",
"real estate agents",
"lenders",
"mortgage experts",
"real estate consultants",
"RE-Quest",
],
authors: [
{
name: "RE-Quest",
url: "https://re-quest.com",
},
],
creator: "RE-Quest",
publisher: "RE-Quest",
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
"max-video-preview": -1,
},
},
openGraph: {
title: "Find Real Estate Professionals",
description:
"Browse verified real estate professionals, agents, and lenders on RE-Quest.",
url: "https://re-quest.com/user/profiles",
siteName: "RE-Quest",
type: "website",
locale: "en_US",
},
twitter: {
card: "summary_large_image",
title: "Find Real Estate Professionals",
description:
"Browse verified real estate professionals, agents, and lenders on RE-Quest.",
},
alternates: {
canonical: "https://re-quest.com/user/profiles",
},
};
export default function ProfilesLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,20 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Account Settings",
description:
"Manage your RE-Quest account settings, profile information, privacy preferences, and notification settings.",
robots: {
index: false,
follow: false,
},
};
export default function SettingsLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

15
src/app/about/layout.tsx Normal file
View File

@@ -0,0 +1,15 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "About Us",
description:
"Learn about RE-Quest and our mission to connect users with trusted real estate professionals.",
};
export default function AboutLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,14 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Coming Soon",
description: "Stay tuned for upcoming features and enhancements on RE-Quest.",
};
export default function ComingSoonLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,15 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Contact Us",
description:
"Get in touch with the RE-Quest team for support, questions, and partnership inquiries.",
};
export default function ContactLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,15 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Education Resources",
description:
"Explore educational resources, guides, and insights about real estate and lending.",
};
export default function EducationLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

11
src/app/faq/layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Frequently Asked Questions",
description:
"Find answers to common questions about RE-Quest, professionals, profiles, and connections.",
};
export default function FAQLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}

View File

@@ -120,11 +120,30 @@ const sourceSerif4 = localFont({
variable: "--font-source-serif-4",
});
// export const metadata: Metadata = {
// title: "Real Estate Platform",
// description: "Find your dream property with trusted agents",
// };
export const metadata: Metadata = {
title: "Real Estate Platform",
description: "Find your dream property with trusted agents",
};
metadataBase: new URL("https://re-quest.com"),
title: {
default: "RE-Quest",
template: "%s | RE-Quest",
},
description:
"Connect with verified real estate professionals, agents, and lenders through RE-Quest.",
openGraph: {
siteName: "RE-Quest",
type: "website",
},
twitter: {
card: "summary_large_image",
},
};
export default function RootLayout({
children,
}: Readonly<{

19
src/app/logout/layout.tsx Normal file
View File

@@ -0,0 +1,19 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Logout | RE-Quest",
description: "Securely sign out from your RE-Quest account.",
robots: {
index: false,
follow: false,
},
};
export default function LogoutLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -1,27 +1,27 @@
'use client';
"use client";
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Home() {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === 'loading') return;
if (status === "loading") return;
// Redirect based on user role, or to public dashboard if not logged in
if (session) {
const userRole = (session.user as any)?.role;
if (userRole === 'AGENT') {
router.replace('/agent/dashboard');
if (userRole === "AGENT") {
router.replace("/agent/dashboard");
} else {
router.replace('/user/dashboard');
router.replace("/user/dashboard");
}
} else {
// Not logged in - go to public user dashboard
router.replace('/user/dashboard');
router.replace("/home");
}
}, [session, status, router]);

View File

@@ -0,0 +1,15 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Privacy Policy",
description:
"Read the RE-Quest Privacy Policy and learn how your information is collected, stored, and protected.",
};
export default function PrivacyPolicyLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,15 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Support",
description:
"Get support and assistance for your RE-Quest account and platform experience.",
};
export default function SupportLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,15 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Terms of Service",
description:
"Review the terms and conditions governing the use of the RE-Quest platform.",
};
export default function TermsLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,15 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Testimonials",
description:
"Read testimonials and success stories from professionals and users on RE-Quest.",
};
export default function TestimonialsLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}

View File

@@ -0,0 +1,54 @@
'use client';
import { useState, useEffect } from 'react';
import { HeroSection } from '@/components/home/HeroSection';
import { FeaturesSection } from '@/components/home/FeaturesSection';
import { TopProfessionals } from '@/components/home/TopProfessionals';
import { TestimonialsSection } from '@/components/home/TestimonialsSection';
import { cmsService, resolveImageUrl } from '@/services/cms.service';
import type { HeroContent, FeaturesContent, TopProfessionalsContent, TestimonialsContent } from '@/types/cms';
// Shared landing/dashboard content rendered by both /home and /user/dashboard.
export function HomeDashboard() {
const [cmsData, setCmsData] = useState<Record<string, unknown>>({});
const [cmsLoaded, setCmsLoaded] = useState(false);
useEffect(() => {
const fetchCms = async () => {
try {
const sections = await cmsService.getPageContent('landing');
const data: Record<string, unknown> = {};
for (const s of sections) {
const content = s.content as Record<string, unknown>;
// Resolve S3 keys in image fields
if (s.sectionKey === 'features' && Array.isArray(content.features)) {
for (const feat of content.features as { iconPath?: string }[]) {
if (feat.iconPath) feat.iconPath = await resolveImageUrl(feat.iconPath);
}
}
if (s.sectionKey === 'testimonials' && Array.isArray(content.stats)) {
for (const stat of content.stats as { iconPath?: string }[]) {
if (stat.iconPath) stat.iconPath = await resolveImageUrl(stat.iconPath);
}
}
data[s.sectionKey] = content;
}
setCmsData(data);
} catch {
// Use default content on error
} finally {
setCmsLoaded(true);
}
};
fetchCms();
}, []);
return (
<div>
<HeroSection content={cmsData.hero as HeroContent | undefined} />
{cmsLoaded && <FeaturesSection content={cmsData.features as FeaturesContent | undefined} />}
<TopProfessionals content={cmsData.topProfessionals as TopProfessionalsContent | undefined} />
<TestimonialsSection content={cmsData.testimonials as TestimonialsContent | undefined} />
</div>
);
}

View File

@@ -1,20 +1,28 @@
'use client';
"use client";
import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { useSession } from 'next-auth/react';
import { useHeaderData } from '@/components/providers/header-provider';
import { useState, useEffect, useRef } from "react";
import Link from "next/link";
import Image from "next/image";
import { useSession } from "next-auth/react";
import { useHeaderData } from "@/components/providers/header-provider";
const navLinks = [
{ label: 'Education', href: '/education' },
{ label: 'About Us', href: '/about' },
{ label: "FAQ's", href: '/faq' },
{ label: "Professional", href: "/user/profiles" },
{ label: "Education", href: "/education" },
{ label: "About Us", href: "/about" },
{ label: "FAQ's", href: "/faq" },
];
export function CommonHeader() {
const { data: session, status } = useSession();
const { profileImage, profileName, avatarLoaded, setAvatarLoaded, notificationCount, messageCount } = useHeaderData();
const {
profileImage,
profileName,
avatarLoaded,
setAvatarLoaded,
notificationCount,
messageCount,
} = useHeaderData();
const [showProfileMenu, setShowProfileMenu] = useState(false);
const [showGuestMenu, setShowGuestMenu] = useState(false);
const [showMobileMenu, setShowMobileMenu] = useState(false);
@@ -26,23 +34,33 @@ export function CommonHeader() {
// Close dropdowns when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (profileMenuRef.current && !profileMenuRef.current.contains(event.target as Node)) {
if (
profileMenuRef.current &&
!profileMenuRef.current.contains(event.target as Node)
) {
setShowProfileMenu(false);
}
if (guestMenuRef.current && !guestMenuRef.current.contains(event.target as Node)) {
if (
guestMenuRef.current &&
!guestMenuRef.current.contains(event.target as Node)
) {
setShowGuestMenu(false);
}
if (mobileMenuRef.current && !mobileMenuRef.current.contains(event.target as Node)) {
if (
mobileMenuRef.current &&
!mobileMenuRef.current.contains(event.target as Node)
) {
setShowMobileMenu(false);
}
}
if (showProfileMenu || showGuestMenu || showMobileMenu) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener("mousedown", handleClickOutside);
}
// const showProfessional = userRole === "AGENT" || userRole === "LENDER";
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener("mousedown", handleClickOutside);
};
}, [showProfileMenu, showGuestMenu, showMobileMenu]);
@@ -50,14 +68,19 @@ export function CommonHeader() {
const userName = profileName || session?.user?.name;
const userEmail = session?.user?.email;
const userRole = (session?.user as any)?.role;
// const showProfessional = userRole === "AGENT" || userRole === "LENDER";
// Use fetched profile image, fallback to session image
const userImage = profileImage || session?.user?.image;
// Logo destination — always lands on the user dashboard regardless of role.
const dashboardLink = '/user/dashboard';
// const dashboardLink = "/user/dashboard";
const dashboardLink = "/home";
return (
<header className="bg-[#648188] rounded-[20px] px-4 md:px-8 relative" ref={mobileMenuRef}>
<header
className="bg-[#648188] rounded-[20px] px-4 md:px-8 relative"
ref={mobileMenuRef}
>
<div className="flex justify-between items-center h-[60px] md:h-[70px]">
{/* Logo */}
<Link href={dashboardLink} className="flex-shrink-0 pt-[5px]">
@@ -83,15 +106,35 @@ export function CommonHeader() {
</Link>
))}
</nav>
{/* <nav className="hidden md:flex items-center gap-8 ml-auto mr-8">
{showProfessional && (
<Link
href="/user/profiles"
className="font-fractul font-bold text-[14px] leading-[18px] text-[#00293D] hover:text-[#e58625] transition-colors"
>
Professional
</Link>
)}
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="font-fractul font-bold text-[14px] leading-[18px] text-[#00293D] hover:text-[#e58625] transition-colors"
>
{link.label}
</Link>
))}
</nav> */}
{/* Right Side Icons */}
<div className="flex items-center gap-2 md:gap-4">
{status === 'loading' ? (
{status === "loading" ? (
<div className="w-[35px] h-[35px] rounded-full shimmer-loading" />
) : session ? (
<>
{/* Message Icon - User role only */}
{userRole === 'USER' && (
{userRole === "USER" && (
<Link
href="/user/message"
className="relative w-6 h-6 flex items-center justify-center hover:opacity-80 transition-opacity cursor-pointer"
@@ -105,7 +148,7 @@ export function CommonHeader() {
{messageCount > 0 && (
<span className="absolute -top-1.5 -right-2 min-w-[18px] h-[18px] bg-red-500 rounded-full flex items-center justify-center px-1">
<span className="text-white text-[10px] font-bold leading-none">
{messageCount > 99 ? '99+' : messageCount}
{messageCount > 99 ? "99+" : messageCount}
</span>
</span>
)}
@@ -114,7 +157,11 @@ export function CommonHeader() {
{/* Notification Bell */}
<Link
href={userRole === 'AGENT' ? '/agent/notifications' : '/user/notifications'}
href={
userRole === "AGENT"
? "/agent/notifications"
: "/user/notifications"
}
className="relative w-6 h-6 flex items-center justify-center hover:opacity-80 transition-opacity cursor-pointer"
>
<Image
@@ -126,7 +173,7 @@ export function CommonHeader() {
{notificationCount > 0 && (
<span className="absolute -top-1.5 -right-2 min-w-[18px] h-[18px] bg-red-500 rounded-full flex items-center justify-center px-1">
<span className="text-white text-[10px] font-bold leading-none">
{notificationCount > 99 ? '99+' : notificationCount}
{notificationCount > 99 ? "99+" : notificationCount}
</span>
</span>
)}
@@ -143,17 +190,22 @@ export function CommonHeader() {
{/* Shimmer while loading, initials only on error */}
{avatarError || !userImage ? (
<div className="absolute inset-0 rounded-full bg-[#c4d9d4] flex items-center justify-center">
<span className="font-bold text-[#00293d] text-[14px]">{userName?.[0]?.toUpperCase() || '?'}</span>
<span className="font-bold text-[#00293d] text-[14px]">
{userName?.[0]?.toUpperCase() || "?"}
</span>
</div>
) : !avatarLoaded ? (
<div className="absolute inset-0 rounded-full shimmer-loading" />
) : null}
{userImage && !avatarError && (
<img
ref={(el) => { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }}
ref={(el) => {
if (el?.complete && el.naturalWidth > 0)
setAvatarLoaded(true);
}}
src={userImage}
alt="Profile"
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? "opacity-100" : "opacity-0"}`}
onLoad={() => setAvatarLoaded(true)}
onError={() => setAvatarError(true)}
/>
@@ -162,7 +214,7 @@ export function CommonHeader() {
{/* Greeting Text - hidden on mobile */}
<div className="hidden sm:flex flex-col text-left">
<span className="font-bold text-[14px] text-[#e58625] leading-tight">
Hi {userName?.split(' ')[0] || 'User'} !
Hi {userName?.split(" ")[0] || "User"} !
</span>
<span className="font-bold text-[9px] text-[#00293d] leading-tight">
Welcome back !
@@ -179,14 +231,19 @@ export function CommonHeader() {
<div className="w-[42px] h-[42px] rounded-full overflow-hidden flex-shrink-0 bg-gray-100 relative">
{/* Initials base layer */}
<div className="absolute inset-0 rounded-full bg-[#c4d9d4] flex items-center justify-center">
<span className="font-bold text-[#00293d] text-[17px]">{userName?.[0]?.toUpperCase() || '?'}</span>
<span className="font-bold text-[#00293d] text-[17px]">
{userName?.[0]?.toUpperCase() || "?"}
</span>
</div>
{userImage && !avatarError && (
<img
ref={(el) => { if (el?.complete && el.naturalWidth > 0) setAvatarLoaded(true); }}
ref={(el) => {
if (el?.complete && el.naturalWidth > 0)
setAvatarLoaded(true);
}}
src={userImage}
alt="Profile"
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? 'opacity-100' : 'opacity-0'}`}
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${avatarLoaded ? "opacity-100" : "opacity-0"}`}
onLoad={() => setAvatarLoaded(true)}
onError={() => setAvatarError(true)}
/>
@@ -194,7 +251,7 @@ export function CommonHeader() {
</div>
<div className="flex flex-col min-w-0 overflow-hidden">
<p className="font-fractul font-medium text-[16px] leading-[20px] text-black">
{userName?.split(' ')[0] || 'User'}
{userName?.split(" ")[0] || "User"}
</p>
<p className="font-serif text-[14px] leading-[18px] text-black/50 truncate">
{userEmail}
@@ -203,46 +260,84 @@ export function CommonHeader() {
</div>
<Link
href={userRole === 'AGENT' ? '/agent/settings' : '/user/settings'}
href={
userRole === "AGENT"
? "/agent/settings"
: "/user/settings"
}
className="flex items-center gap-3 px-4 py-3 border-b border-black/10 hover:bg-black/5 transition-colors"
onClick={() => setShowProfileMenu(false)}
>
<Image src="/assets/icons/settings-icon.svg" alt="Settings" width={24} height={24} />
<Image
src="/assets/icons/settings-icon.svg"
alt="Settings"
width={24}
height={24}
/>
<div className="flex flex-col">
<p className="font-fractul text-[16px] leading-[18px] text-black">Account Settings</p>
<p className="font-serif text-[14px] leading-[18px] text-black/50">Profile, Security</p>
<p className="font-fractul text-[16px] leading-[18px] text-black">
Account Settings
</p>
<p className="font-serif text-[14px] leading-[18px] text-black/50">
Profile, Security
</p>
</div>
</Link>
<Link
href={userRole === 'AGENT' ? '/agent/settings/notifications' : '/user/settings/notifications'}
href={
userRole === "AGENT"
? "/agent/settings/notifications"
: "/user/settings/notifications"
}
className="flex items-center gap-3 px-4 py-3 border-b border-black/10 hover:bg-black/5 transition-colors"
onClick={() => setShowProfileMenu(false)}
>
<Image src="/assets/icons/notification-settings-icon.svg" alt="Notifications" width={24} height={24} />
<p className="font-fractul text-[16px] leading-[18px] text-black">Notification Settings</p>
<Image
src="/assets/icons/notification-settings-icon.svg"
alt="Notifications"
width={24}
height={24}
/>
<p className="font-fractul text-[16px] leading-[18px] text-black">
Notification Settings
</p>
</Link>
{userRole === 'AGENT' && (
{userRole === "AGENT" && (
<Link
href="/agent/dashboard"
className="flex items-center gap-3 px-4 py-3 border-b border-black/10 hover:bg-black/5 transition-colors"
onClick={() => setShowProfileMenu(false)}
>
<Image src="/assets/icons/edit-icon.svg" alt="Edit Page" width={24} height={24} />
<p className="font-fractul text-[16px] leading-[18px] text-black">Edit Page</p>
<Image
src="/assets/icons/edit-icon.svg"
alt="Edit Page"
width={24}
height={24}
/>
<p className="font-fractul text-[16px] leading-[18px] text-black">
Edit Page
</p>
</Link>
)}
<button
onClick={() => {
setShowProfileMenu(false);
window.location.href = '/logout';
window.location.href = "/logout";
}}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-black/5 transition-colors"
>
<Image src="/assets/icons/logout-icon.svg" alt="Log Out" width={24} height={24} />
<p className="font-fractul font-bold text-[16px] leading-[18px] text-[#e58625]">Log Out</p>
<Image
src="/assets/icons/logout-icon.svg"
alt="Log Out"
width={24}
height={24}
/>
<p className="font-fractul font-bold text-[16px] leading-[18px] text-[#e58625]">
Log Out
</p>
</button>
</div>
)}
@@ -291,10 +386,19 @@ export function CommonHeader() {
className="flex items-center gap-3 px-4 py-3 border-b border-black/10 hover:bg-black/5 transition-colors"
onClick={() => setShowGuestMenu(false)}
>
<Image src="/assets/icons/settings-icon.svg" alt="Settings" width={24} height={24} />
<Image
src="/assets/icons/settings-icon.svg"
alt="Settings"
width={24}
height={24}
/>
<div className="flex flex-col">
<p className="font-fractul text-[16px] leading-[18px] text-black">Account Settings</p>
<p className="font-serif text-[14px] leading-[18px] text-black/50">Profile, Security</p>
<p className="font-fractul text-[16px] leading-[18px] text-black">
Account Settings
</p>
<p className="font-serif text-[14px] leading-[18px] text-black/50">
Profile, Security
</p>
</div>
</Link>
@@ -303,8 +407,15 @@ export function CommonHeader() {
className="flex items-center gap-3 px-4 py-3 border-b border-black/10 hover:bg-black/5 transition-colors"
onClick={() => setShowGuestMenu(false)}
>
<Image src="/assets/icons/notification-settings-icon.svg" alt="Notifications" width={24} height={24} />
<p className="font-fractul text-[16px] leading-[18px] text-black">Notification Settings</p>
<Image
src="/assets/icons/notification-settings-icon.svg"
alt="Notifications"
width={24}
height={24}
/>
<p className="font-fractul text-[16px] leading-[18px] text-black">
Notification Settings
</p>
</Link>
<div className="flex gap-3 px-4 py-4">
@@ -340,9 +451,19 @@ export function CommonHeader() {
viewBox="0 0 24 24"
>
{showMobileMenu ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
)}
</svg>
</button>
@@ -364,6 +485,28 @@ export function CommonHeader() {
</Link>
))}
</nav>
{/* <nav className="flex flex-col gap-1">
{showProfessional && (
<Link
href="/user/profiles"
onClick={() => setShowMobileMenu(false)}
className="font-fractul font-bold text-[14px] text-[#00293D] hover:text-[#e58625] transition-colors px-2 py-2 rounded-[10px] hover:bg-white/10"
>
Professional
</Link>
)}
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setShowMobileMenu(false)}
className="font-fractul font-bold text-[14px] text-[#00293D] hover:text-[#e58625] transition-colors px-2 py-2 rounded-[10px] hover:bg-white/10"
>
{link.label}
</Link>
))}
</nav> */}
</div>
)}
</header>

View File

@@ -1,67 +1,200 @@
'use client';
// 'use client';
import { useState } from 'react';
import Image from 'next/image';
// import { useState } from 'react';
// import Image from 'next/image';
// interface ContactInfoProps {
// email: string;
// phone: string;
// }
// // Helper function to mask email
// function maskEmail(email: string): string {
// const [localPart, domain] = email.split('@');
// if (!domain) return '********';
// const maskedLocal = localPart.slice(0, 2) + '********';
// return `${maskedLocal}@${domain}`;
// }
// // Helper function to mask phone
// function maskPhone(phone: string): string {
// if (!phone || phone.trim() === '') return '-';
// if (phone.length <= 4) return '****' + phone;
// return phone.slice(0, -4).replace(/./g, '*') + phone.slice(-4);
// }
// export function ContactInfo({ email, phone }: ContactInfoProps) {
// const [showEmail, setShowEmail] = useState(false);
// const [showPhone, setShowPhone] = useState(false);
// return (
// <div className="w-full max-w-[354px] lg:max-w-none border border-[#00293d]/10 rounded-[15px] p-4">
// <div className="flex items-center gap-2 mb-2">
// <span className="font-bold text-[#00293d] text-sm flex-shrink-0">Email:</span>
// <span className="text-[#00293d] text-sm font-serif truncate min-w-0 flex-1">
// {showEmail ? email : maskEmail(email)}
// </span>
// <button
// className="p-1 hover:bg-gray-100 rounded flex-shrink-0"
// onClick={() => setShowEmail(!showEmail)}
// >
// <Image
// src={showEmail ? "/assets/icons/eye-icon.svg" : "/assets/icons/eye-off-icon.svg"}
// alt={showEmail ? "Hide email" : "Show email"}
// width={16}
// height={16}
// />
// </button>
// </div>
// <div className="flex items-center gap-2">
// <span className="font-bold text-[#00293d] text-sm flex-shrink-0">Ph.No:</span>
// <span className="text-[#00293d] text-sm font-serif truncate min-w-0 flex-1">
// {showPhone ? phone : maskPhone(phone)}
// </span>
// <button
// className="p-1 hover:bg-gray-100 rounded flex-shrink-0"
// onClick={() => setShowPhone(!showPhone)}
// >
// <Image
// src={showPhone ? "/assets/icons/eye-icon.svg" : "/assets/icons/eye-off-icon.svg"}
// alt={showPhone ? "Hide phone" : "Show phone"}
// width={16}
// height={16}
// />
// </button>
// </div>
// </div>
// );
// }
"use client";
import { useState } from "react";
import Image from "next/image";
interface ContactInfoProps {
email: string;
phone: string;
}
// Helper function to mask email
function maskEmail(email: string): string {
const [localPart, domain] = email.split('@');
if (!domain) return '********';
const maskedLocal = localPart.slice(0, 2) + '********';
return `${maskedLocal}@${domain}`;
}
// Helper function to mask phone
function maskPhone(phone: string): string {
if (!phone || phone.trim() === '') return '-';
if (phone.length <= 4) return '****' + phone;
return phone.slice(0, -4).replace(/./g, '*') + phone.slice(-4);
}
export function ContactInfo({ email, phone }: ContactInfoProps) {
const [showEmail, setShowEmail] = useState(false);
const [showPhone, setShowPhone] = useState(false);
const [copiedField, setCopiedField] = useState<"email" | "phone" | null>(
null,
);
const handleCopy = async (text: string, field: "email" | "phone") => {
if (!text?.trim()) return;
try {
await navigator.clipboard.writeText(text);
setCopiedField(field);
setTimeout(() => {
setCopiedField(null);
}, 1500);
} catch (error) {
console.error("Copy failed:", error);
}
};
return (
<div className="w-full max-w-[354px] lg:max-w-none border border-[#00293d]/10 rounded-[15px] p-4">
<div className="flex items-center gap-2 mb-2">
<span className="font-bold text-[#00293d] text-sm flex-shrink-0">Email:</span>
<span className="text-[#00293d] text-sm font-serif truncate min-w-0 flex-1">
{showEmail ? email : maskEmail(email)}
{/* EMAIL */}
<div className="flex items-center gap-2 mb-3">
<span className="font-bold text-[#00293d] text-sm flex-shrink-0">
Email:
</span>
<button
className="p-1 hover:bg-gray-100 rounded flex-shrink-0"
onClick={() => setShowEmail(!showEmail)}
>
<Image
src={showEmail ? "/assets/icons/eye-icon.svg" : "/assets/icons/eye-off-icon.svg"}
alt={showEmail ? "Hide email" : "Show email"}
width={16}
height={16}
/>
</button>
<div className="relative group min-w-0 flex-1">
<span className="text-[#00293d] text-sm font-serif truncate block cursor-default">
{email}
</span>
{/* Hover Tooltip */}
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 hidden group-hover:block z-50">
<div className="relative bg-[#00293d] text-white text-xs px-3 py-2 rounded-md whitespace-nowrap shadow-lg">
{email}
{/* Arrow */}
<div className="absolute left-1/2 -translate-x-1/2 top-full border-4 border-transparent border-t-[#00293d]" />
</div>
</div>
</div>
{/* Copy Button */}
<div className="relative">
<button
onClick={() => handleCopy(email, "email")}
className="p-1 hover:bg-gray-100 rounded flex-shrink-0"
title="Copy email"
>
<Image
src="/assets/icons/copy-icon.svg"
alt="Copy email"
width={18}
height={18}
/>
</button>
{/* Copied Tooltip */}
{copiedField === "email" && (
<div className="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-50">
<div className="relative bg-[#00293d] text-white text-xs px-3 py-2 rounded-md shadow-lg whitespace-nowrap">
Email copied!
<div className="absolute left-1/2 -translate-x-1/2 top-full border-4 border-transparent border-t-[#00293d]" />
</div>
</div>
)}
</div>
</div>
{/* PHONE */}
<div className="flex items-center gap-2">
<span className="font-bold text-[#00293d] text-sm flex-shrink-0">Ph.No:</span>
<span className="text-[#00293d] text-sm font-serif truncate min-w-0 flex-1">
{showPhone ? phone : maskPhone(phone)}
<span className="font-bold text-[#00293d] text-sm flex-shrink-0">
Ph.No:
</span>
<button
className="p-1 hover:bg-gray-100 rounded flex-shrink-0"
onClick={() => setShowPhone(!showPhone)}
>
<Image
src={showPhone ? "/assets/icons/eye-icon.svg" : "/assets/icons/eye-off-icon.svg"}
alt={showPhone ? "Hide phone" : "Show phone"}
width={16}
height={16}
/>
</button>
<div className="relative group min-w-0 flex-1">
<span className="text-[#00293d] text-sm font-serif truncate block cursor-default">
{phone?.trim() || "-"}
</span>
{phone?.trim() && (
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 hidden group-hover:block z-50">
<div className="relative bg-[#00293d] text-white text-xs px-3 py-2 rounded-md whitespace-nowrap shadow-lg">
{phone}
<div className="absolute left-1/2 -translate-x-1/2 top-full border-4 border-transparent border-t-[#00293d]" />
</div>
</div>
)}
</div>
{phone?.trim() && (
<div className="relative">
<button
onClick={() => handleCopy(phone, "phone")}
className="p-1 hover:bg-gray-100 rounded flex-shrink-0"
title="Copy phone number"
>
<Image
src="/assets/icons/copy-icon.svg"
alt="Copy phone"
width={18}
height={18}
/>
</button>
{copiedField === "phone" && (
<div className="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-50">
<div className="relative bg-[#00293d] text-white text-xs px-3 py-2 rounded-md shadow-lg whitespace-nowrap">
Phone copied!
<div className="absolute left-1/2 -translate-x-1/2 top-full border-4 border-transparent border-t-[#00293d]" />
</div>
</div>
)}
</div>
)}
</div>
</div>
);

View File

@@ -276,7 +276,7 @@ export function ProfileCard({
</div>
{/* Bio */}
<p className="text-[14px] font-normal text-[#00293D] font-serif leading-[20px] mb-4 text-center lg:text-left">
<p className="text-[14px] font-normal text-[#00293D] font-serif leading-[20px] mb-4 text-center lg:text-left break-words">
{bio}
</p>

View File

@@ -2,10 +2,30 @@ import { auth } from "@/auth";
import { NextResponse } from "next/server";
// Auth routes - logged-in users should be redirected away from these
const authRoutes = ["/login", "/signup", "/forgot-password", "/reset-password", "/verify-email"];
const authRoutes = [
"/login",
"/signup",
"/forgot-password",
"/reset-password",
"/verify-email",
];
// Public routes - accessible to everyone (logged in or not)
const publicRoutes = ["/", "/contact", "/about", "/faq", "/education", "/coming-soon", "/privacy-policy", "/terms-of-service", "/logout", "/user/dashboard", "/user/profiles", "/user/profile"];
const publicRoutes = [
"/",
"/home",
"/contact",
"/about",
"/faq",
"/education",
"/coming-soon",
"/privacy-policy",
"/terms-of-service",
"/logout",
"/user/dashboard",
"/user/profiles",
"/user/profile",
];
// Routes that should NEVER be redirected away from (even if logged in)
const noRedirectRoutes = ["/logout"];
@@ -16,23 +36,27 @@ export default auth((req) => {
const userRole = (req.auth?.user as any)?.role;
const isAuthRoute = authRoutes.some(
(route) => nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/")
(route) =>
nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/"),
);
const isPublicRoute = publicRoutes.some(
(route) => nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/")
(route) =>
nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/"),
);
const isAgentRoute = nextUrl.pathname.startsWith("/agent");
const isUserRoute = nextUrl.pathname.startsWith("/user");
const isApiRoute = nextUrl.pathname.startsWith("/api");
const isStaticRoute = nextUrl.pathname.startsWith("/_next") ||
nextUrl.pathname.startsWith("/assets") ||
nextUrl.pathname.includes(".");
const isStaticRoute =
nextUrl.pathname.startsWith("/_next") ||
nextUrl.pathname.startsWith("/assets") ||
nextUrl.pathname.includes(".");
const isNoRedirectRoute = noRedirectRoutes.some(
(route) => nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/")
(route) =>
nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/"),
);
// Skip middleware for API routes and static files