7 Commits

Author SHA1 Message Date
fc932dbc7e fix(security): resolve audit findings — logging, endpoints, contact details
- Stop logging submitted password forms to the browser console
- Drive analytics from NEXT_PUBLIC_UMAMI_* instead of a hardcoded vendor
  script URL and site ID; renders nothing when unset
- Replace the wildcard image remote host "**" with an explicit allowlist
  (adds DigitalOcean Spaces)
- Fix the socket URL fallback to the API port (:3001, was :4000)
- Replace placeholder and personal contact emails with support@re-quest.com,
  including the privacy policy and terms pages

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:30:46 +05:30
9c0e7457e5 Merge pull request 'Added home page with dashboard content and SEO metadata' (#2) from fix/profile-contact-improvements into main
Reviewed-on: #2
2026-06-23 11:44:37 +00:00
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
47 changed files with 1604 additions and 456 deletions

View File

@@ -7,18 +7,11 @@ const nextConfig: NextConfig = {
// Image optimization // Image optimization
images: { images: {
remotePatterns: [ remotePatterns: [
{ { protocol: "https", hostname: "*.contabostorage.com" },
protocol: "https", { protocol: "https", hostname: "*.amazonaws.com" },
hostname: "**", { protocol: "https", hostname: "*.digitaloceanspaces.com" },
}, { protocol: "http", hostname: "localhost" },
{ { protocol: "http", hostname: "127.0.0.1" },
protocol: "http",
hostname: "localhost",
},
{
protocol: "http",
hostname: "127.0.0.1",
},
], ],
// Don't proxy external images through Next.js server // Don't proxy external images through Next.js server
// Avoids SSL cert issues with Contabo S3 (sin1.contabostorage.com) // Avoids SSL cert issues with Contabo S3 (sin1.contabostorage.com)

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

@@ -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

@@ -3,8 +3,8 @@
import { SettingsSidebar, PasswordSecurityForm } from '@/components/settings'; import { SettingsSidebar, PasswordSecurityForm } from '@/components/settings';
export default function PasswordSecurityPage() { export default function PasswordSecurityPage() {
const handleSave = (data: { currentPassword: string; newPassword: string }) => { const handleSave = (_data: { currentPassword: string; newPassword: string }) => {
console.log('Updating agent password:', data); // TODO: call the change-password API. Do not log password payloads.
}; };
return ( return (

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 { useSession } from "next-auth/react";
import { useRouter, usePathname } from 'next/navigation'; import { useRouter, usePathname } from "next/navigation";
import { useEffect } from 'react'; import { useEffect } from "react";
import Image from 'next/image'; import Image from "next/image";
import { Footer } from '@/components/layout/Footer'; import { Footer } from "@/components/layout/Footer";
import { CommonHeader } from '@/components/layout/CommonHeader'; import { CommonHeader } from "@/components/layout/CommonHeader";
import { PresenceProvider } from '@/components/providers/presence-provider'; import { PresenceProvider } from "@/components/providers/presence-provider";
// Pages that don't require authentication // Pages that don't require authentication
const publicPaths = [ const publicPaths = [
'/user/dashboard', "/home",
'/user/profiles', "/user/dashboard",
'/user/profile/', // Agent profile view (includes /user/profile/[id]) "/user/profiles",
"/user/profile/", // Agent profile view (includes /user/profile/[id])
]; ];
// Check if current path is public // Check if current path is public
const isPublicPath = (pathname: string) => { 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({ export default function UserLayout({
@@ -28,11 +31,12 @@ export default function UserLayout({
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const isDashboard = pathname === '/user/dashboard'; // const isDashboard = pathname === "/user/dashboard";
const isDashboard = pathname === "/user/dashboard" || pathname === "/home";
const isPublic = isPublicPath(pathname); const isPublic = isPublicPath(pathname);
useEffect(() => { useEffect(() => {
if (status === 'loading') return; if (status === "loading") return;
// Allow public pages without authentication. // Allow public pages without authentication.
// Agents are intentionally allowed to land on /user/dashboard — the logo // 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 // Redirect agents to agent dashboard for protected user pages
const userRole = (session.user as any)?.role; const userRole = (session.user as any)?.role;
if (userRole === 'AGENT') { if (userRole === "AGENT") {
router.replace('/agent/dashboard'); router.replace("/agent/dashboard");
} }
}, [session, status, router, pathname, isPublic]); }, [session, status, router, pathname, isPublic]);
const splashLoading = ( const splashLoading = (
<div <div
className="min-h-screen flex flex-col items-center justify-center" 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]"> <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>
<div className="mt-8"> <div className="mt-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#00293d]" /> <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 // Show loading only for protected pages while checking auth
if (status === 'loading' && !isPublic) { if (status === "loading" && !isPublic) {
return splashLoading; return splashLoading;
} }
@@ -105,9 +121,7 @@ export default function UserLayout({
</div> </div>
{/* Main Content */} {/* Main Content */}
<main className="flex-1"> <main className="flex-1">{children}</main>
{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'; 'use client';
import { useState, useEffect } from 'react'; import { HomeDashboard } from '@/components/home/HomeDashboard';
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';
export default function UserDashboard() { export default function UserDashboard() {
const [cmsData, setCmsData] = useState<Record<string, unknown>>({}); return <HomeDashboard />;
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

@@ -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 { useState, useEffect } from "react";
import Image from 'next/image'; import Image from "next/image";
import { useParams } from 'next/navigation'; import { useParams } from "next/navigation";
import { useSession } from 'next-auth/react'; import { useSession } from "next-auth/react";
// Import shared components // Import shared components
import { import {
@@ -14,43 +14,68 @@ import {
TestimonialsSection, TestimonialsSection,
StatusButtons, StatusButtons,
ContactInfo, ContactInfo,
} from '@/components/profile'; } from "@/components/profile";
import { ConnectRequestModal } from '@/components/modals'; import { ConnectRequestModal } from "@/components/modals";
import { MobileBackButton } from '@/components/layout/MobileBackButton'; import { MobileBackButton } from "@/components/layout/MobileBackButton";
import { agentsService, AgentProfile, FieldValueResponse } from '@/services/agents.service'; import {
import { connectionRequestsService, ConnectionStatus } from '@/services/connection-requests.service'; agentsService,
import { uploadService } from '@/services/upload.service'; AgentProfile,
import { testimonialsService, Testimonial } from '@/services/testimonials.service'; FieldValueResponse,
import { mapFieldValuesToExperience, mapFieldValuesToSpecializationFields, mapFieldValuesToProfileCard, mapFieldValuesToContactInfo, mapFieldValuesToAvailability, mapFieldValuesToWorkEnvironment, mapFieldValuesToPersonalTagline, ExperienceData, SpecializationFieldsData, ProfileCardData, ContactInfoData, AvailabilityData, WorkEnvironmentData, PersonalTaglineData } from '@/utils/profileDataMapper'; } 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 // Default work environment data
const defaultWorkEnvironmentData: WorkEnvironmentData = { const defaultWorkEnvironmentData: WorkEnvironmentData = {
label: 'Preferred Work Environment', label: "Preferred Work Environment",
content: '', content: "",
}; };
// Default personal tagline data // Default personal tagline data
const defaultPersonalTaglineData: PersonalTaglineData = { const defaultPersonalTaglineData: PersonalTaglineData = {
label: 'Personal Tagline', label: "Personal Tagline",
content: '', content: "",
}; };
// Default experience data when no field values are available // Default experience data when no field values are available
const defaultExperience: ExperienceData = { const defaultExperience: ExperienceData = {
years: '-', years: "-",
yearsLabel: 'Years in Experience', yearsLabel: "Years in Experience",
contracts: '-', contracts: "-",
contractsLabel: 'Number of contracts closed', contractsLabel: "Number of contracts closed",
licensingAreas: [], licensingAreas: [],
licensingAreasLabel: 'Licensing & Areas', licensingAreasLabel: "Licensing & Areas",
expertiseYears: [], expertiseYears: [],
expertiseYearsLabel: 'Areas in expertise & Years', expertiseYearsLabel: "Areas in expertise & Years",
certifications: [], certifications: [],
certificationsLabel: 'Certifications', certificationsLabel: "Certifications",
agencyName: '', agencyName: "",
agencyDesignation: '', agencyDesignation: "",
agencyShowDesignation: false, agencyShowDesignation: false,
agencySectionLabel: 'Real Estate Agency & Designation', agencySectionLabel: "Real Estate Agency & Designation",
}; };
// Default specialization fields data when no field values are available // 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 // Default profile card data when no field values are available
const defaultProfileCardData: ProfileCardData = { const defaultProfileCardData: ProfileCardData = {
bio: '', bio: "",
expertise: [], expertise: [],
serviceAreas: [], serviceAreas: [],
city: null, city: null,
@@ -75,9 +100,9 @@ const defaultContactInfoData: ContactInfoData = {
// Default availability data when no field values are available // Default availability data when no field values are available
const defaultAvailabilityData: AvailabilityData = { const defaultAvailabilityData: AvailabilityData = {
type: '', type: "",
schedule: [], schedule: [],
label: 'Availability', label: "Availability",
}; };
export default function AgentProfileView() { export default function AgentProfileView() {
@@ -87,31 +112,46 @@ export default function AgentProfileView() {
const [agentProfile, setAgentProfile] = useState<AgentProfile | null>(null); const [agentProfile, setAgentProfile] = useState<AgentProfile | null>(null);
const [fieldValues, setFieldValues] = useState<FieldValueResponse[]>([]); const [fieldValues, setFieldValues] = useState<FieldValueResponse[]>([]);
const [experienceData, setExperienceData] = useState<ExperienceData>(defaultExperience); const [experienceData, setExperienceData] =
const [specializationFieldsData, setSpecializationFieldsData] = useState<SpecializationFieldsData>(defaultSpecializationFieldsData); useState<ExperienceData>(defaultExperience);
const [profileCardData, setProfileCardData] = useState<ProfileCardData>(defaultProfileCardData); const [specializationFieldsData, setSpecializationFieldsData] =
const [contactInfoData, setContactInfoData] = useState<ContactInfoData>(defaultContactInfoData); useState<SpecializationFieldsData>(defaultSpecializationFieldsData);
const [availabilityData, setAvailabilityData] = useState<AvailabilityData>(defaultAvailabilityData); const [profileCardData, setProfileCardData] = useState<ProfileCardData>(
const [workEnvironmentData, setWorkEnvironmentData] = useState<WorkEnvironmentData>(defaultWorkEnvironmentData); defaultProfileCardData,
const [personalTaglineData, setPersonalTaglineData] = useState<PersonalTaglineData>(defaultPersonalTaglineData); );
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 [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [imageError, setImageError] = useState(false); const [imageError, setImageError] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false); const [imageLoaded, setImageLoaded] = useState(false);
const [avatarUrl, setAvatarUrl] = useState<string | null>(null); 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 // Connect modal state
const [showConnectModal, setShowConnectModal] = useState(false); const [showConnectModal, setShowConnectModal] = useState(false);
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus | null>(null); const [connectionStatus, setConnectionStatus] =
const [connectionRequestId, setConnectionRequestId] = useState<string | null>(null); 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) // Helper to check if avatar is an S3 key (not a full URL or local path)
const isS3Key = (avatar: string | null | undefined): boolean => { const isS3Key = (avatar: string | null | undefined): boolean => {
if (!avatar) return false; if (!avatar) return false;
// S3 keys don't start with http or / // 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 // Fetch agent profile and field values on mount
@@ -124,41 +164,58 @@ export default function AgentProfileView() {
setError(null); setError(null);
// Fetch profile, field values, and testimonials in parallel // Fetch profile, field values, and testimonials in parallel
const [profile, fieldValuesResponse, testimonialsData] = await Promise.all([ const [profile, fieldValuesResponse, testimonialsData] =
agentsService.getAgentById(id), await Promise.all([
agentsService.getFieldValuesByAgentId(id), agentsService.getAgentById(id),
testimonialsService.getAgentTestimonials(id).catch(() => [] as Testimonial[]), agentsService.getFieldValuesByAgentId(id),
]); testimonialsService
.getAgentTestimonials(id)
.catch(() => [] as Testimonial[]),
]);
setAgentProfile(profile); setAgentProfile(profile);
setFieldValues(fieldValuesResponse.fieldValues); setFieldValues(fieldValuesResponse.fieldValues);
// Map field values to experience data structure // Map field values to experience data structure
const mappedExperience = mapFieldValuesToExperience(fieldValuesResponse.fieldValues); const mappedExperience = mapFieldValuesToExperience(
fieldValuesResponse.fieldValues,
);
setExperienceData(mappedExperience); setExperienceData(mappedExperience);
// Map field values to specialization fields data (only fields from "Specialization" section) // Map field values to specialization fields data (only fields from "Specialization" section)
const mappedSpecializationFields = mapFieldValuesToSpecializationFields(fieldValuesResponse.fieldValues); const mappedSpecializationFields = mapFieldValuesToSpecializationFields(
fieldValuesResponse.fieldValues,
);
setSpecializationFieldsData(mappedSpecializationFields); setSpecializationFieldsData(mappedSpecializationFields);
// Map field values to profile card data (bio, expertise, location) // Map field values to profile card data (bio, expertise, location)
const mappedProfileCard = mapFieldValuesToProfileCard(fieldValuesResponse.fieldValues); const mappedProfileCard = mapFieldValuesToProfileCard(
fieldValuesResponse.fieldValues,
);
setProfileCardData(mappedProfileCard); setProfileCardData(mappedProfileCard);
// Map field values to contact info data (email, phone) // Map field values to contact info data (email, phone)
const mappedContactInfo = mapFieldValuesToContactInfo(fieldValuesResponse.fieldValues); const mappedContactInfo = mapFieldValuesToContactInfo(
fieldValuesResponse.fieldValues,
);
setContactInfoData(mappedContactInfo); setContactInfoData(mappedContactInfo);
// Map field values to availability data // Map field values to availability data
const mappedAvailability = mapFieldValuesToAvailability(fieldValuesResponse.fieldValues); const mappedAvailability = mapFieldValuesToAvailability(
fieldValuesResponse.fieldValues,
);
setAvailabilityData(mappedAvailability); setAvailabilityData(mappedAvailability);
// Map field values to work environment data // Map field values to work environment data
const mappedWorkEnv = mapFieldValuesToWorkEnvironment(fieldValuesResponse.fieldValues); const mappedWorkEnv = mapFieldValuesToWorkEnvironment(
fieldValuesResponse.fieldValues,
);
setWorkEnvironmentData(mappedWorkEnv); setWorkEnvironmentData(mappedWorkEnv);
// Map field values to personal tagline data // Map field values to personal tagline data
const mappedTagline = mapFieldValuesToPersonalTagline(fieldValuesResponse.fieldValues); const mappedTagline = mapFieldValuesToPersonalTagline(
fieldValuesResponse.fieldValues,
);
setPersonalTaglineData(mappedTagline); setPersonalTaglineData(mappedTagline);
// Map testimonials for TestimonialsSection // Map testimonials for TestimonialsSection
@@ -169,29 +226,33 @@ export default function AgentProfileView() {
author: t.authorName, author: t.authorName,
role: t.authorRole, role: t.authorRole,
rating: t.rating, rating: t.rating,
})) })),
); );
// If avatar is an S3 key, fetch presigned URL // If avatar is an S3 key, fetch presigned URL
if (profile.avatar && isS3Key(profile.avatar)) { if (profile.avatar && isS3Key(profile.avatar)) {
try { try {
const presignedUrl = await uploadService.getPresignedDownloadUrl(profile.avatar); const presignedUrl = await uploadService.getPresignedDownloadUrl(
profile.avatar,
);
setAvatarUrl(presignedUrl); setAvatarUrl(presignedUrl);
} catch (avatarErr) { } catch (avatarErr) {
console.error('Failed to get avatar URL:', avatarErr); console.error("Failed to get avatar URL:", avatarErr);
} }
} else if (profile.avatar) { } else if (profile.avatar) {
setAvatarUrl(profile.avatar); setAvatarUrl(profile.avatar);
} }
} catch (err: any) { } catch (err: any) {
console.error('Failed to fetch profile:', err); console.error("Failed to fetch profile:", err);
const status = err?.response?.status; const status = err?.response?.status;
if (status === 403) { 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) { } else if (status === 404) {
setError('Profile not found.'); setError("Profile not found.");
} else { } else {
setError('Failed to load profile data'); setError("Failed to load profile data");
} }
} finally { } finally {
setLoading(false); setLoading(false);
@@ -207,11 +268,12 @@ export default function AgentProfileView() {
if (!id || !session) return; if (!id || !session) return;
try { try {
const statusResponse = await connectionRequestsService.getConnectionStatus(id); const statusResponse =
await connectionRequestsService.getConnectionStatus(id);
setConnectionStatus(statusResponse?.status || null); setConnectionStatus(statusResponse?.status || null);
setConnectionRequestId(statusResponse?.id || null); setConnectionRequestId(statusResponse?.id || null);
} catch (err) { } 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 // Non-critical error, don't show to user
} }
}; };
@@ -221,7 +283,7 @@ export default function AgentProfileView() {
// Handle connection request success // Handle connection request success
const handleConnectionSuccess = () => { const handleConnectionSuccess = () => {
setConnectionStatus('PENDING'); setConnectionStatus("PENDING");
}; };
// Handle unlink/disconnect // Handle unlink/disconnect
@@ -233,7 +295,7 @@ export default function AgentProfileView() {
setConnectionStatus(null); setConnectionStatus(null);
setConnectionRequestId(null); setConnectionRequestId(null);
} catch (err) { } 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 // Check for null, undefined, or empty string
if (!agentProfile?.avatar || agentProfile.avatar.trim() === '') { if (!agentProfile?.avatar || agentProfile.avatar.trim() === "") {
return null; return null;
} }
// For relative paths (local assets), return as-is // For relative paths (local assets), return as-is
if (agentProfile.avatar.startsWith('/')) { if (agentProfile.avatar.startsWith("/")) {
return agentProfile.avatar; return agentProfile.avatar;
} }
@@ -264,12 +326,15 @@ export default function AgentProfileView() {
// Format member since date // Format member since date
const formatMemberSince = (dateString?: string) => { const formatMemberSince = (dateString?: string) => {
if (!dateString) return 'Member'; if (!dateString) return "Member";
try { try {
const date = new Date(dateString); const date = new Date(dateString);
return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); return date.toLocaleDateString("en-US", {
month: "long",
year: "numeric",
});
} catch { } 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 items-center justify-center h-[calc(100vh-180px)]">
<div className="flex flex-col items-center gap-4"> <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> <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> </div>
</div> </div>
); );
} }
const isPermissionError = error?.includes('permission') || error?.includes('private'); const isPermissionError =
error?.includes("permission") || error?.includes("private");
if (error || !agentProfile) { if (error || !agentProfile) {
return ( return (
<div className="max-w-7xl mx-auto px-4 lg:px-8 py-6"> <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="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="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 ? ( {isPermissionError ? (
<svg className="w-8 h-8 text-[#e58625]" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<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" /> 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>
) : ( ) : (
<svg className="w-8 h-8 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> 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> </svg>
)} )}
</div> </div>
<h2 className="text-[18px] font-bold font-serif text-[#00293D] mb-2"> <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> </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 <button
onClick={() => window.history.back()} 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" 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> </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 ( 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 /> <script
{/* Main Layout - Responsive: Column on mobile, Row on desktop */} type="application/ld+json"
<div className="flex flex-col lg:flex-row gap-6 lg:items-stretch"> dangerouslySetInnerHTML={{
{/* Left Sidebar - Status & Contact */} __html: JSON.stringify(profileSchema),
<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}
/> />
</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}</>;
}

View File

@@ -3,8 +3,8 @@
import { SettingsSidebar, PasswordSecurityForm } from '@/components/settings'; import { SettingsSidebar, PasswordSecurityForm } from '@/components/settings';
export default function UserPasswordSecurityPage() { export default function UserPasswordSecurityPage() {
const handleSave = (data: { currentPassword: string; newPassword: string }) => { const handleSave = (_data: { currentPassword: string; newPassword: string }) => {
console.log('Updating user password:', data); // TODO: call the change-password API. Do not log password payloads.
}; };
return ( return (

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

@@ -31,7 +31,7 @@ interface ContactCta {
const defaultContactDetails: ContactDetails = { const defaultContactDetails: ContactDetails = {
title: 'Get In Touch', title: 'Get In Touch',
description: 'Have a question about a property or need assistance? Fill out the form below and our team will get back to you shortly.', description: 'Have a question about a property or need assistance? Fill out the form below and our team will get back to you shortly.',
email: '123support@gmail.com', email: 'support@re-quest.com',
phone: '1234567890', phone: '1234567890',
phoneHours: 'Mon-Fri 9am-6pm', phoneHours: 'Mon-Fri 9am-6pm',
officeAddress: '123 Market Street', officeAddress: '123 Market Street',

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

@@ -262,7 +262,7 @@ export default function FAQPage() {
Start Live Chat Start Live Chat
</Link> </Link>
<a <a
href="mailto:support@requesn.com" href="mailto:support@re-quest.com"
className="flex items-center justify-center gap-2 w-[174px] h-[51px] border border-[#00293d] rounded-[7px] font-fractul text-[16px] text-[#00293d] hover:bg-gray-50 transition-colors" className="flex items-center justify-center gap-2 w-[174px] h-[51px] border border-[#00293d] rounded-[7px] font-fractul text-[16px] text-[#00293d] hover:bg-gray-50 transition-colors"
> >
<Image <Image

View File

@@ -120,11 +120,30 @@ const sourceSerif4 = localFont({
variable: "--font-source-serif-4", 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 = { export const metadata: Metadata = {
title: "Real Estate Platform", metadataBase: new URL("https://re-quest.com"),
description: "Find your dream property with trusted agents",
};
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({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
@@ -139,12 +158,15 @@ export default function RootLayout({
<NotificationProvider /> <NotificationProvider />
{children} {children}
</SessionProvider> </SessionProvider>
{/* Umami analytics — loaded after page becomes interactive */} {/* Umami analytics — set NEXT_PUBLIC_UMAMI_URL and NEXT_PUBLIC_UMAMI_WEBSITE_ID to enable */}
<Script {process.env.NEXT_PUBLIC_UMAMI_URL &&
src="https://analytics.superlabs.co/script.js" process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
data-website-id="00e1ce31-e174-4519-8b59-63e8d4556b01" <Script
strategy="afterInteractive" src={process.env.NEXT_PUBLIC_UMAMI_URL}
/> data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
strategy="afterInteractive"
/>
)}
{/* Microsoft Clarity */} {/* Microsoft Clarity */}
{process.env.NEXT_PUBLIC_CLARITY_ID && ( {process.env.NEXT_PUBLIC_CLARITY_ID && (
<Script id="ms-clarity" strategy="afterInteractive"> <Script id="ms-clarity" strategy="afterInteractive">

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

@@ -477,10 +477,10 @@ export default function PrivacyPolicyPage() {
<p className="mb-1"> <p className="mb-1">
Email:{' '} Email:{' '}
<a <a
href="mailto:request.sha@gmail.com" href="mailto:support@re-quest.com"
className="text-[#e58625] underline hover:opacity-80" className="text-[#e58625] underline hover:opacity-80"
> >
request.sha@gmail.com support@re-quest.com
</a> </a>
</p> </p>
<p>Address: 1975 Peralta Point, Colorado Springs, CO 80910</p> <p>Address: 1975 Peralta Point, Colorado Springs, CO 80910</p>

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

@@ -163,7 +163,7 @@ export default function TermsOfServicePage() {
<p className="font-serif text-[15px] leading-[24px] text-[#00293d] mb-4"> <p className="font-serif text-[15px] leading-[24px] text-[#00293d] mb-4">
If you experience any threatening, abusive, or suspicious behavior from another user, please report the If you experience any threatening, abusive, or suspicious behavior from another user, please report the
interaction immediately using the in-app reporting feature or by contacting us at interaction immediately using the in-app reporting feature or by contacting us at
officialteam.request@gmail.com. RE-Quest will investigate reported incidents and take appropriate action, support@re-quest.com. RE-Quest will investigate reported incidents and take appropriate action,
which may include account suspension or referral to law enforcement. which may include account suspension or referral to law enforcement.
</p> </p>
@@ -464,7 +464,7 @@ export default function TermsOfServicePage() {
If you have any questions or concerns about these Terms, please contact us: If you have any questions or concerns about these Terms, please contact us:
</p> </p>
<p className="font-serif text-[15px] leading-[24px] text-[#00293d] mb-1"> <p className="font-serif text-[15px] leading-[24px] text-[#00293d] mb-1">
<span className="font-bold">Email:</span> officialteam.request@gmail.com <span className="font-bold">Email:</span> support@re-quest.com
</p> </p>
<p className="font-serif text-[15px] leading-[24px] text-[#00293d]"> <p className="font-serif text-[15px] leading-[24px] text-[#00293d]">
<span className="font-bold">Address:</span> 1975 Peralta Point, Colorado Springs, CO 80910 <span className="font-bold">Address:</span> 1975 Peralta Point, Colorado Springs, CO 80910

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

View File

@@ -1,67 +1,200 @@
'use client'; // 'use client';
import { useState } from 'react'; // import { useState } from 'react';
import Image from 'next/image'; // 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 { interface ContactInfoProps {
email: string; email: string;
phone: 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) { export function ContactInfo({ email, phone }: ContactInfoProps) {
const [showEmail, setShowEmail] = useState(false); const [copiedField, setCopiedField] = useState<"email" | "phone" | null>(
const [showPhone, setShowPhone] = useState(false); 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 ( return (
<div className="w-full max-w-[354px] lg:max-w-none border border-[#00293d]/10 rounded-[15px] p-4"> <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"> {/* EMAIL */}
<span className="font-bold text-[#00293d] text-sm flex-shrink-0">Email:</span> <div className="flex items-center gap-2 mb-3">
<span className="text-[#00293d] text-sm font-serif truncate min-w-0 flex-1"> <span className="font-bold text-[#00293d] text-sm flex-shrink-0">
{showEmail ? email : maskEmail(email)} Email:
</span> </span>
<button
className="p-1 hover:bg-gray-100 rounded flex-shrink-0" <div className="relative group min-w-0 flex-1">
onClick={() => setShowEmail(!showEmail)} <span className="text-[#00293d] text-sm font-serif truncate block cursor-default">
> {email}
<Image </span>
src={showEmail ? "/assets/icons/eye-icon.svg" : "/assets/icons/eye-off-icon.svg"}
alt={showEmail ? "Hide email" : "Show email"} {/* Hover Tooltip */}
width={16} <div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 hidden group-hover:block z-50">
height={16} <div className="relative bg-[#00293d] text-white text-xs px-3 py-2 rounded-md whitespace-nowrap shadow-lg">
/> {email}
</button>
{/* 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> </div>
{/* PHONE */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-bold text-[#00293d] text-sm flex-shrink-0">Ph.No:</span> <span className="font-bold text-[#00293d] text-sm flex-shrink-0">
<span className="text-[#00293d] text-sm font-serif truncate min-w-0 flex-1"> Ph.No:
{showPhone ? phone : maskPhone(phone)}
</span> </span>
<button
className="p-1 hover:bg-gray-100 rounded flex-shrink-0" <div className="relative group min-w-0 flex-1">
onClick={() => setShowPhone(!showPhone)} <span className="text-[#00293d] text-sm font-serif truncate block cursor-default">
> {phone?.trim() || "-"}
<Image </span>
src={showPhone ? "/assets/icons/eye-icon.svg" : "/assets/icons/eye-off-icon.svg"}
alt={showPhone ? "Hide phone" : "Show phone"} {phone?.trim() && (
width={16} <div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 hidden group-hover:block z-50">
height={16} <div className="relative bg-[#00293d] text-white text-xs px-3 py-2 rounded-md whitespace-nowrap shadow-lg">
/> {phone}
</button>
<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>
</div> </div>
); );

View File

@@ -276,7 +276,7 @@ export function ProfileCard({
</div> </div>
{/* Bio */} {/* 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} {bio}
</p> </p>

View File

@@ -270,10 +270,10 @@ export function SubscriptionForm() {
{/* Support Email */} {/* Support Email */}
<Link <Link
href="mailto:support@example.com" href="mailto:support@re-quest.com"
className="font-serif font-bold text-[14px] text-[#e58625] underline hover:text-[#d47920] transition-colors" className="font-serif font-bold text-[14px] text-[#e58625] underline hover:text-[#d47920] transition-colors"
> >
support@example.com support@re-quest.com
</Link> </Link>
</div> </div>

View File

@@ -2,10 +2,30 @@ import { auth } from "@/auth";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
// Auth routes - logged-in users should be redirected away from these // 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) // 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) // Routes that should NEVER be redirected away from (even if logged in)
const noRedirectRoutes = ["/logout"]; const noRedirectRoutes = ["/logout"];
@@ -16,23 +36,27 @@ export default auth((req) => {
const userRole = (req.auth?.user as any)?.role; const userRole = (req.auth?.user as any)?.role;
const isAuthRoute = authRoutes.some( const isAuthRoute = authRoutes.some(
(route) => nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/") (route) =>
nextUrl.pathname === route || nextUrl.pathname.startsWith(route + "/"),
); );
const isPublicRoute = publicRoutes.some( 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 isAgentRoute = nextUrl.pathname.startsWith("/agent");
const isUserRoute = nextUrl.pathname.startsWith("/user"); const isUserRoute = nextUrl.pathname.startsWith("/user");
const isApiRoute = nextUrl.pathname.startsWith("/api"); const isApiRoute = nextUrl.pathname.startsWith("/api");
const isStaticRoute = nextUrl.pathname.startsWith("/_next") || const isStaticRoute =
nextUrl.pathname.startsWith("/assets") || nextUrl.pathname.startsWith("/_next") ||
nextUrl.pathname.includes("."); nextUrl.pathname.startsWith("/assets") ||
nextUrl.pathname.includes(".");
const isNoRedirectRoute = noRedirectRoutes.some( 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 // Skip middleware for API routes and static files

View File

@@ -59,7 +59,7 @@ class SocketService {
} }
// Extract base URL without /api/v1 path for Socket.io connection // Extract base URL without /api/v1 path for Socket.io connection
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'; const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1';
const baseUrl = apiUrl.replace(/\/api\/v1\/?$/, ''); const baseUrl = apiUrl.replace(/\/api\/v1\/?$/, '');
this.socket = io(baseUrl, { this.socket = io(baseUrl, {