feat: add SEO metadata and profile schema #1
35
src/app/(agent)/agent/dashboard/layout.tsx
Normal file
35
src/app/(agent)/agent/dashboard/layout.tsx
Normal 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;
|
||||||
|
}
|
||||||
20
src/app/(agent)/agent/edit/layout.tsx
Normal file
20
src/app/(agent)/agent/edit/layout.tsx
Normal 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;
|
||||||
|
}
|
||||||
28
src/app/(agent)/agent/message/layout.tsx
Normal file
28
src/app/(agent)/agent/message/layout.tsx
Normal 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;
|
||||||
|
}
|
||||||
35
src/app/(agent)/agent/network/layout.tsx
Normal file
35
src/app/(agent)/agent/network/layout.tsx
Normal 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;
|
||||||
|
}
|
||||||
35
src/app/(agent)/agent/notifications/layout.tsx
Normal file
35
src/app/(agent)/agent/notifications/layout.tsx
Normal 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;
|
||||||
|
}
|
||||||
35
src/app/(agent)/agent/settings/layout.tsx
Normal file
35
src/app/(agent)/agent/settings/layout.tsx
Normal 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;
|
||||||
|
}
|
||||||
19
src/app/(auth)/forgot-password/layout.tsx
Normal file
19
src/app/(auth)/forgot-password/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
19
src/app/(auth)/login/layout.tsx
Normal file
19
src/app/(auth)/login/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
19
src/app/(auth)/reset-password/layout.tsx
Normal file
19
src/app/(auth)/reset-password/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
19
src/app/(auth)/signup/layout.tsx
Normal file
19
src/app/(auth)/signup/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
19
src/app/(auth)/verify-email/layout.tsx
Normal file
19
src/app/(auth)/verify-email/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
20
src/app/(user)/user/dashboard/layout.tsx
Normal file
20
src/app/(user)/user/dashboard/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
20
src/app/(user)/user/message/layout.tsx
Normal file
20
src/app/(user)/user/message/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
20
src/app/(user)/user/notifications/layout.tsx
Normal file
20
src/app/(user)/user/notifications/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
25
src/app/(user)/user/profile/[id]/layout.tsx
Normal file
25
src/app/(user)/user/profile/[id]/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
@@ -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] =
|
||||||
|
await Promise.all([
|
||||||
agentsService.getAgentById(id),
|
agentsService.getAgentById(id),
|
||||||
agentsService.getFieldValuesByAgentId(id),
|
agentsService.getFieldValuesByAgentId(id),
|
||||||
testimonialsService.getAgentTestimonials(id).catch(() => [] as Testimonial[]),
|
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,10 +413,65 @@ 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 (
|
||||||
|
<>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: JSON.stringify(profileSchema),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<div className="max-w-7xl mx-auto px-4 lg:px-8 pt-2 pb-6 space-y-6">
|
<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 />
|
<MobileBackButton
|
||||||
|
label="Back"
|
||||||
|
fallbackHref="/user/profiles"
|
||||||
|
alwaysShow
|
||||||
|
/>
|
||||||
{/* Main Layout - Responsive: Column on mobile, Row on desktop */}
|
{/* Main Layout - Responsive: Column on mobile, Row on desktop */}
|
||||||
<div className="flex flex-col lg:flex-row gap-6 lg:items-stretch">
|
<div className="flex flex-col lg:flex-row gap-6 lg:items-stretch">
|
||||||
{/* Left Sidebar - Status & Contact */}
|
{/* Left Sidebar - Status & Contact */}
|
||||||
@@ -333,7 +482,9 @@ export default function AgentProfileView() {
|
|||||||
{/* Shimmer while loading, initials only on error/no image */}
|
{/* Shimmer while loading, initials only on error/no image */}
|
||||||
{imageError || !getProfileImageUrl() ? (
|
{imageError || !getProfileImageUrl() ? (
|
||||||
<div className="absolute inset-0 bg-[#c4d9d4] flex items-center justify-center rounded-[15px]">
|
<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>
|
<span className="font-bold text-[#00293d] text-[80px]">
|
||||||
|
{agentProfile?.firstName?.[0]?.toUpperCase() || "?"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : !imageLoaded ? (
|
) : !imageLoaded ? (
|
||||||
<div className="absolute inset-0 shimmer-loading rounded-[15px]" />
|
<div className="absolute inset-0 shimmer-loading rounded-[15px]" />
|
||||||
@@ -342,10 +493,15 @@ export default function AgentProfileView() {
|
|||||||
<>
|
<>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
ref={(el) => { if (el?.complete) { if (el.naturalWidth > 0) setImageLoaded(true); else setImageError(true); } }}
|
ref={(el) => {
|
||||||
|
if (el?.complete) {
|
||||||
|
if (el.naturalWidth > 0) setImageLoaded(true);
|
||||||
|
else setImageError(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
src={getProfileImageUrl()!}
|
src={getProfileImageUrl()!}
|
||||||
alt="Profile"
|
alt="Profile"
|
||||||
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${imageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${imageLoaded ? "opacity-100" : "opacity-0"}`}
|
||||||
onLoad={() => setImageLoaded(true)}
|
onLoad={() => setImageLoaded(true)}
|
||||||
onError={() => setImageError(true)}
|
onError={() => setImageError(true)}
|
||||||
/>
|
/>
|
||||||
@@ -366,8 +522,13 @@ export default function AgentProfileView() {
|
|||||||
|
|
||||||
{/* Contact Info */}
|
{/* Contact Info */}
|
||||||
<ContactInfo
|
<ContactInfo
|
||||||
email={agentProfile.email || agentProfile.user?.email || contactInfoData.email || ''}
|
email={
|
||||||
phone={agentProfile.phone || contactInfoData.phone || ''}
|
agentProfile.email ||
|
||||||
|
agentProfile.user?.email ||
|
||||||
|
contactInfoData.email ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
phone={agentProfile.phone || contactInfoData.phone || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -378,13 +539,24 @@ export default function AgentProfileView() {
|
|||||||
firstName={agentProfile.firstName}
|
firstName={agentProfile.firstName}
|
||||||
lastName={agentProfile.lastName}
|
lastName={agentProfile.lastName}
|
||||||
isVerified={agentProfile.isVerified}
|
isVerified={agentProfile.isVerified}
|
||||||
title={agentProfile.agentType?.name || 'Real Estate Agent'}
|
title={agentProfile.agentType?.name || "Real Estate Agent"}
|
||||||
location={profileCardData.city && profileCardData.state
|
location={
|
||||||
|
profileCardData.city && profileCardData.state
|
||||||
? `${profileCardData.state}, ${profileCardData.city}`
|
? `${profileCardData.state}, ${profileCardData.city}`
|
||||||
: profileCardData.state || profileCardData.city || agentProfile.serviceAreas?.[0] || '-'}
|
: profileCardData.state ||
|
||||||
memberSince={formatMemberSince((agentProfile as unknown as { createdAt?: string }).createdAt)}
|
profileCardData.city ||
|
||||||
bio={profileCardData.bio || agentProfile.bio || ''}
|
agentProfile.serviceAreas?.[0] ||
|
||||||
expertise={profileCardData.expertise.length > 0 ? profileCardData.expertise : (agentProfile.specializations || [])}
|
"-"
|
||||||
|
}
|
||||||
|
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}
|
showEditButton={false}
|
||||||
messageHref={`/user/message?agentProfileId=${agentProfile.id}`}
|
messageHref={`/user/message?agentProfileId=${agentProfile.id}`}
|
||||||
connectionStatus={connectionStatus}
|
connectionStatus={connectionStatus}
|
||||||
@@ -413,10 +585,17 @@ export default function AgentProfileView() {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{availabilityData.schedule.length > 0 ? (
|
{availabilityData.schedule.length > 0 ? (
|
||||||
availabilityData.schedule.map((item, index) => (
|
availabilityData.schedule.map((item, index) => (
|
||||||
<p key={index} className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]">{item}</p>
|
<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>
|
<p className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]/60">
|
||||||
|
Not specified
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -434,9 +613,13 @@ export default function AgentProfileView() {
|
|||||||
}
|
}
|
||||||
content={
|
content={
|
||||||
workEnvironmentData.content ? (
|
workEnvironmentData.content ? (
|
||||||
<p className="font-serif font-semibold text-[14px] leading-[19px] text-[#00293D]">{workEnvironmentData.content}</p>
|
<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>
|
<p className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]/60">
|
||||||
|
Not specified
|
||||||
|
</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -452,9 +635,17 @@ export default function AgentProfileView() {
|
|||||||
}
|
}
|
||||||
content={
|
content={
|
||||||
personalTaglineData.content ? (
|
personalTaglineData.content ? (
|
||||||
<p className="text-[14px] font-semibold font-serif leading-[19px] text-center text-[#00293D]">“{personalTaglineData.content.length > 150 ? personalTaglineData.content.substring(0, 150) + '...' : personalTaglineData.content}”</p>
|
<p className="text-[14px] font-semibold font-serif leading-[19px] text-center text-[#00293D]">
|
||||||
|
“
|
||||||
|
{personalTaglineData.content.length > 150
|
||||||
|
? personalTaglineData.content.substring(0, 150) + "..."
|
||||||
|
: personalTaglineData.content}
|
||||||
|
”
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]/60">Not specified</p>
|
<p className="font-normal font-serif text-[14px] leading-[19px] text-[#00293D]/60">
|
||||||
|
Not specified
|
||||||
|
</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -473,10 +664,14 @@ export default function AgentProfileView() {
|
|||||||
isOpen={showConnectModal}
|
isOpen={showConnectModal}
|
||||||
onClose={() => setShowConnectModal(false)}
|
onClose={() => setShowConnectModal(false)}
|
||||||
agentProfileId={id}
|
agentProfileId={id}
|
||||||
agentName={`${agentProfile.firstName || ''} ${agentProfile.lastName || ''}`.trim() || 'Agent'}
|
agentName={
|
||||||
|
`${agentProfile.firstName || ""} ${agentProfile.lastName || ""}`.trim() ||
|
||||||
|
"Agent"
|
||||||
|
}
|
||||||
existingStatus={connectionStatus}
|
existingStatus={connectionStatus}
|
||||||
onSuccess={handleConnectionSuccess}
|
onSuccess={handleConnectionSuccess}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/app/(user)/user/profiles/layout.tsx
Normal file
68
src/app/(user)/user/profiles/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
20
src/app/(user)/user/settings/layout.tsx
Normal file
20
src/app/(user)/user/settings/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Account Settings",
|
||||||
|
description:
|
||||||
|
"Manage your RE-Quest account settings, profile information, privacy preferences, and notification settings.",
|
||||||
|
|
||||||
|
robots: {
|
||||||
|
index: false,
|
||||||
|
follow: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
15
src/app/about/layout.tsx
Normal file
15
src/app/about/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
14
src/app/coming-soon/layout.tsx
Normal file
14
src/app/coming-soon/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
15
src/app/contact/layout.tsx
Normal file
15
src/app/contact/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
15
src/app/education/layout.tsx
Normal file
15
src/app/education/layout.tsx
Normal 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
11
src/app/faq/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
@@ -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<{
|
||||||
|
|||||||
19
src/app/logout/layout.tsx
Normal file
19
src/app/logout/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
15
src/app/privacy-policy/layout.tsx
Normal file
15
src/app/privacy-policy/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
15
src/app/support/chat/layout.tsx
Normal file
15
src/app/support/chat/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
15
src/app/terms-of-service/layout.tsx
Normal file
15
src/app/terms-of-service/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
15
src/app/testimonials/[token]/layout.tsx
Normal file
15
src/app/testimonials/[token]/layout.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
@@ -1,20 +1,27 @@
|
|||||||
'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: "Education", href: "/education" },
|
||||||
{ label: 'About Us', href: '/about' },
|
{ label: "About Us", href: "/about" },
|
||||||
{ label: "FAQ's", href: '/faq' },
|
{ 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 +33,32 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
};
|
};
|
||||||
}, [showProfileMenu, showGuestMenu, showMobileMenu]);
|
}, [showProfileMenu, showGuestMenu, showMobileMenu]);
|
||||||
|
|
||||||
@@ -50,14 +66,18 @@ 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";
|
||||||
|
|
||||||
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]">
|
||||||
@@ -72,7 +92,27 @@ export function CommonHeader() {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Navigation - Desktop only */}
|
{/* Navigation - Desktop only */}
|
||||||
|
{/* <nav className="hidden md:flex items-center gap-8 ml-auto mr-8">
|
||||||
|
{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> */}
|
||||||
<nav className="hidden md:flex items-center gap-8 ml-auto mr-8">
|
<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) => (
|
{navLinks.map((link) => (
|
||||||
<Link
|
<Link
|
||||||
key={link.href}
|
key={link.href}
|
||||||
@@ -86,12 +126,12 @@ export function CommonHeader() {
|
|||||||
|
|
||||||
{/* 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 +145,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 +154,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 +170,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 +187,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 +211,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 +228,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 +248,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 +257,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 +383,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 +404,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 +448,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>
|
||||||
@@ -352,7 +470,29 @@ export function CommonHeader() {
|
|||||||
{/* Mobile Navigation Menu */}
|
{/* Mobile Navigation Menu */}
|
||||||
{showMobileMenu && (
|
{showMobileMenu && (
|
||||||
<div className="md:hidden border-t border-white/20 py-3 pb-4">
|
<div className="md:hidden border-t border-white/20 py-3 pb-4">
|
||||||
|
{/* <nav className="flex flex-col gap-1">
|
||||||
|
{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> */}
|
||||||
<nav className="flex flex-col gap-1">
|
<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) => (
|
{navLinks.map((link) => (
|
||||||
<Link
|
<Link
|
||||||
key={link.href}
|
key={link.href}
|
||||||
|
|||||||
@@ -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 className="flex items-center gap-2">
|
</div>
|
||||||
<span className="font-bold text-[#00293d] text-sm flex-shrink-0">Ph.No:</span>
|
</div>
|
||||||
<span className="text-[#00293d] text-sm font-serif truncate min-w-0 flex-1">
|
|
||||||
{showPhone ? phone : maskPhone(phone)}
|
{/* Copy Button */}
|
||||||
</span>
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
|
onClick={() => handleCopy(email, "email")}
|
||||||
className="p-1 hover:bg-gray-100 rounded flex-shrink-0"
|
className="p-1 hover:bg-gray-100 rounded flex-shrink-0"
|
||||||
onClick={() => setShowPhone(!showPhone)}
|
title="Copy email"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={showPhone ? "/assets/icons/eye-icon.svg" : "/assets/icons/eye-off-icon.svg"}
|
src="/assets/icons/copy-icon.svg"
|
||||||
alt={showPhone ? "Hide phone" : "Show phone"}
|
alt="Copy email"
|
||||||
width={16}
|
width={18}
|
||||||
height={16}
|
height={18}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Copied Tooltip */}
|
||||||
|
{copiedField === "email" && (
|
||||||
|
<div className="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-50">
|
||||||
|
<div className="relative bg-[#00293d] text-white text-xs px-3 py-2 rounded-md shadow-lg whitespace-nowrap">
|
||||||
|
Email copied!
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 top-full border-4 border-transparent border-t-[#00293d]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* PHONE */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-bold text-[#00293d] text-sm flex-shrink-0">
|
||||||
|
Ph.No:
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="relative group min-w-0 flex-1">
|
||||||
|
<span className="text-[#00293d] text-sm font-serif truncate block cursor-default">
|
||||||
|
{phone?.trim() || "-"}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{phone?.trim() && (
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 hidden group-hover:block z-50">
|
||||||
|
<div className="relative bg-[#00293d] text-white text-xs px-3 py-2 rounded-md whitespace-nowrap shadow-lg">
|
||||||
|
{phone}
|
||||||
|
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 top-full border-4 border-transparent border-t-[#00293d]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{phone?.trim() && (
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => handleCopy(phone, "phone")}
|
||||||
|
className="p-1 hover:bg-gray-100 rounded flex-shrink-0"
|
||||||
|
title="Copy phone number"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src="/assets/icons/copy-icon.svg"
|
||||||
|
alt="Copy phone"
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{copiedField === "phone" && (
|
||||||
|
<div className="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-50">
|
||||||
|
<div className="relative bg-[#00293d] text-white text-xs px-3 py-2 rounded-md shadow-lg whitespace-nowrap">
|
||||||
|
Phone copied!
|
||||||
|
<div className="absolute left-1/2 -translate-x-1/2 top-full border-4 border-transparent border-t-[#00293d]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user