{t("academy.profile.trainingPackages")}
diff --git a/src/app/(projects)/new-project/page.tsx b/src/app/(projects)/new-project/page.tsx
index fccbce4..94f3b57 100644
--- a/src/app/(projects)/new-project/page.tsx
+++ b/src/app/(projects)/new-project/page.tsx
@@ -3,13 +3,15 @@
import Container from "@/components/elements/Container";
import MultiStepForm from "@/components/projects/NewProject/MultiStepForm";
import { ProjectFormProvider } from "@/contexts/ProjectFormContext";
-import React from "react";
+import React, { Suspense } from "react";
function NewProject() {
return (
-
+
+
+
);
diff --git a/src/app/(projects)/projects/[id]/[title]/ProjectDetailClient.tsx b/src/app/(projects)/projects/[id]/[title]/ProjectDetailClient.tsx
index 3f278fb..869e4ab 100644
--- a/src/app/(projects)/projects/[id]/[title]/ProjectDetailClient.tsx
+++ b/src/app/(projects)/projects/[id]/[title]/ProjectDetailClient.tsx
@@ -6,8 +6,8 @@ import ProjectRequestForm from "@/components/projects/ProjectPage/ProjectRequest
import ProjectRequests from "@/components/projects/ProjectPage/ProjectRequests";
import PageLoader from "@/components/ui/PageLoader";
import useAxios from "@/hooks/useAxios";
-import { Project, IProjectRequest } from "@/types/types";
-import { useEffect, useState } from "react";
+import { Project, IProjectRequest, ProjectRole } from "@/types/types";
+import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
export default function ProjectDetailClient({ id }: { id: string }) {
@@ -17,6 +17,7 @@ export default function ProjectDetailClient({ id }: { id: string }) {
const [projectRequests, setProjectRequests] = useState
([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
+ const [selectedRoleId, setSelectedRoleId] = useState(null);
useEffect(() => {
let cancelled = false;
@@ -28,15 +29,14 @@ export default function ProjectDetailClient({ id }: { id: string }) {
const response = await request<{
project: Project;
projectRequests?: IProjectRequest[];
- }>(
- "GET",
- `/projects/get/web/${id}`,
- null,
- { noToast: true }
- );
+ }>("GET", `/projects/get/web/${id}`, null, { noToast: true });
if (!cancelled) {
- setProject(response?.project ?? null);
+ const loaded = response?.project ?? null;
+ setProject(loaded);
setProjectRequests(response?.projectRequests ?? []);
+ if (loaded?.roles?.length) {
+ setSelectedRoleId(loaded.roles[0]._id);
+ }
}
} catch {
if (!cancelled) setError(true);
@@ -51,6 +51,15 @@ export default function ProjectDetailClient({ id }: { id: string }) {
};
}, [id, request]);
+ const selectedRole: ProjectRole | null = useMemo(() => {
+ if (!project?.roles?.length) return null;
+ return (
+ project.roles.find((r) => r._id === selectedRoleId) ||
+ project.roles[0] ||
+ null
+ );
+ }, [project, selectedRoleId]);
+
if (loading) return ;
if (error || !project) {
@@ -65,22 +74,61 @@ export default function ProjectDetailClient({ id }: { id: string }) {
return (
-
- {projectRequests.length > 0 ? (
-
-
- {t("projects.requestUsersTitle")}
+
+
+ {selectedRole ? (
+
+
{selectedRole.expertise}
+ {selectedRole.sub_expertise?.length ? (
+
+ {selectedRole.sub_expertise.join(" _ ")}
-
-
- ) : null}
-
+ ) : null}
+ {(selectedRole.height_min != null ||
+ selectedRole.height_max != null) && (
+
+ {t("filters.heightCm")}: {selectedRole.height_min ?? "—"} -{" "}
+ {selectedRole.height_max ?? "—"}
+
+ )}
+ {(selectedRole.weight_min != null ||
+ selectedRole.weight_max != null) && (
+
+ {t("filters.weightKg")}: {selectedRole.weight_min ?? "—"} -{" "}
+ {selectedRole.weight_max ?? "—"}
+
+ )}
+
+ {t("projects.newProject.fields.headcount")}:{" "}
+ {selectedRole.number_of_person || 1}
+
+
+ ) : null}
+
+ {projectRequests.length > 0 ? (
+
+
+ {t("projects.requestUsersTitle")}
+
+
+
+ ) : null}
+
+
);
}
diff --git a/src/app/(projects)/projects/new/[id]/page.tsx b/src/app/(projects)/projects/new/[id]/page.tsx
new file mode 100644
index 0000000..635aa96
--- /dev/null
+++ b/src/app/(projects)/projects/new/[id]/page.tsx
@@ -0,0 +1,153 @@
+"use client";
+
+import Container from "@/components/elements/Container";
+import { IProjectType, Project } from "@/types/types";
+import useAxios from "@/hooks/useAxios";
+import React, { useEffect, useState } from "react";
+import MainProjectCard from "@/components/projects/MainProjectCard";
+import RoundedDiv from "@/components/elements/RoundedDiv";
+import RoundedButton from "@/components/elements/RoundedButton";
+import LocalePageShell from "@/components/i18n/LocalePageShell";
+import { useTranslation } from "react-i18next";
+import toast from "react-hot-toast";
+import { AxiosError } from "axios";
+
+interface ProjectPaymentProps {
+ params: Promise<{ id: string }>;
+}
+
+function ProjectPaymentPage({ params }: ProjectPaymentProps) {
+ const { t } = useTranslation("common");
+ const resolvedParams = React.use(params);
+ const { request, loading } = useAxios();
+ const { id } = resolvedParams;
+ const [project, setProject] = useState();
+ const [typeList, setTypeList] = useState(null);
+ const [price, setPrice] = useState("");
+ const [paying, setPaying] = useState(false);
+
+ const getDisplayTypeLabel = (projectType?: string) => {
+ if (projectType === "normal" || projectType === "free") {
+ return t("projects.newProject.displaySimple");
+ }
+ if (projectType === "force") {
+ return t("projects.newProject.displayUrgent");
+ }
+ return t("projects.newProject.displayHighlight");
+ };
+
+ useEffect(() => {
+ const fetchProject = async () => {
+ try {
+ const response = await request<{ project: Project }>(
+ "GET",
+ `/projects/get/web/${id}`
+ );
+ setProject(response?.project);
+ } catch {
+ toast.error(t("projects.notFound"));
+ }
+ };
+ const fetchTypes = async () => {
+ try {
+ const response = await request<{ projectTypes: IProjectType[] }>(
+ "GET",
+ "/projects/types"
+ );
+ setTypeList(response?.projectTypes || null);
+ } catch (err) {
+ console.log(err);
+ }
+ };
+ void fetchProject();
+ void fetchTypes();
+ }, [id, request, t]);
+
+ useEffect(() => {
+ if (project?.project_type && typeList) {
+ const foundType = typeList.find(
+ (item) => item.name === project.project_type
+ );
+ if (foundType) setPrice(String(foundType.price));
+ }
+ }, [project, typeList]);
+
+ const payHandler = async () => {
+ if (paying || !project?._id) return;
+ setPaying(true);
+ try {
+ const itemName = project.project_type || "normal";
+ const response = await request<{
+ authority?: string;
+ type?: string;
+ message?: string;
+ }>(
+ "POST",
+ "/projects/initiate-payment-web",
+ {
+ item_name: itemName,
+ projectId: id,
+ },
+ { noToast: true }
+ );
+
+ if (response?.type === "free" || itemName === "free") {
+ window.location.href = `/projects/payment/success?projectId=${id}`;
+ return;
+ }
+
+ const authority = response?.authority;
+ if (authority) {
+ // لینک خارجی — router.push ممکن است در سافاری/نکست کار نکند
+ window.location.href = `https://www.zarinpal.com/pg/StartPay/${authority}`;
+ return;
+ }
+
+ toast.error(response?.message || t("projects.payment.initFailed"));
+ } catch (error: unknown) {
+ const axiosErr = error as AxiosError<{ message?: string }>;
+ const serverMsg = axiosErr?.response?.data?.message;
+ console.log(error);
+ toast.error(serverMsg || t("projects.payment.initFailed"));
+ } finally {
+ setPaying(false);
+ }
+ };
+
+ return (
+
+
+
+ {t("projects.payment.title")}
+
+
+ {project &&
}
+
+
+ {getDisplayTypeLabel(project?.project_type)}:{" "}
+ {Number(price || 0).toLocaleString()} {t("settings.toman")}
+
+
+ {t("projects.payment.payableAmount", {
+ amount: Number(price || 0).toLocaleString(),
+ })}
+
+ void payHandler()}
+ disabled={paying || loading || !project}
+ className="h-9 w-32"
+ >
+ {paying
+ ? t("common.loading")
+ : t("projects.payment.title")}
+
+
+
+
+
+ );
+}
+
+export default ProjectPaymentPage;
diff --git a/src/app/(projects)/projects/payment/failed/page.tsx b/src/app/(projects)/projects/payment/failed/page.tsx
new file mode 100644
index 0000000..e2fae21
--- /dev/null
+++ b/src/app/(projects)/projects/payment/failed/page.tsx
@@ -0,0 +1,142 @@
+"use client";
+
+import Container from "@/components/elements/Container";
+import { IProjectType, Project } from "@/types/types";
+import useAxios from "@/hooks/useAxios";
+import React, { useEffect, useState } from "react";
+import RoundedDiv from "@/components/elements/RoundedDiv";
+import RoundedButton from "@/components/elements/RoundedButton";
+import { useSearchParams } from "next/navigation";
+import Image from "next/image";
+import MainProjectCard from "@/components/projects/MainProjectCard";
+import { useTranslation } from "react-i18next";
+import LocalePageShell from "@/components/i18n/LocalePageShell";
+import toast from "react-hot-toast";
+
+function FailedProject() {
+ const { t } = useTranslation("common");
+ const { request } = useAxios();
+ const searchParams = useSearchParams();
+ const projectId = searchParams.get("projectId");
+
+ const [project, setProject] = useState();
+ const [typeList, setTypeList] = useState(null);
+ const [price, setPrice] = useState("");
+
+ const getDisplayTypeLabel = (projectType?: string) => {
+ if (projectType === "normal" || projectType === "free") {
+ return t("projects.newProject.displaySimple");
+ }
+ if (projectType === "force") {
+ return t("projects.newProject.displayUrgent");
+ }
+ return t("projects.newProject.displayHighlight");
+ };
+
+ useEffect(() => {
+ if (!projectId) return;
+ const fetchAd = async () => {
+ const response = await request<{ project: Project }>(
+ "GET",
+ `/projects/get/web/${projectId}`
+ );
+ setProject(response?.project);
+ };
+ const fetchStates = async () => {
+ try {
+ const response = await request<{ projectTypes: IProjectType[] }>(
+ "GET",
+ "/projects/types"
+ );
+ setTypeList(response?.projectTypes || null);
+ } catch (err) {
+ console.log(err);
+ }
+ };
+ void fetchAd();
+ void fetchStates();
+ }, [projectId, request]);
+
+ useEffect(() => {
+ if (project?.project_type && typeList) {
+ const foundType = typeList.find(
+ (item) => item.name === project.project_type
+ );
+ if (foundType) setPrice(String(foundType.price));
+ }
+ }, [project, typeList]);
+
+ const payHandler = async () => {
+ try {
+ const response = await request<{
+ authority?: string;
+ type?: string;
+ message?: string;
+ }>(
+ "POST",
+ "/projects/initiate-payment-web",
+ {
+ item_name: project?.project_type || "normal",
+ projectId: projectId,
+ },
+ { noToast: true }
+ );
+ if (response.type === "free") {
+ window.location.href = `/projects/payment/success?projectId=${projectId}`;
+ } else if (response.authority) {
+ window.location.href = `https://www.zarinpal.com/pg/StartPay/${response.authority}`;
+ } else {
+ toast.error(response?.message || t("projects.payment.initFailed"));
+ }
+ } catch (error: unknown) {
+ const axiosErr = error as { response?: { data?: { message?: string } } };
+ toast.error(
+ axiosErr?.response?.data?.message || t("projects.payment.initFailed")
+ );
+ }
+ };
+
+ return (
+
+
+
+ {t("projects.payment.failed")}
+
+
+
+ {t("projects.payment.failedHint1")}
+ {t("projects.payment.failedHint2")}
+
+
+ {project &&
}
+
+
+ {getDisplayTypeLabel(project?.project_type)}:{" "}
+ {Number(price).toLocaleString()} {t("settings.toman")}
+
+
+ {t("projects.payment.payableAmount", {
+ amount: Number(price).toLocaleString(),
+ })}
+
+
+ {t("projects.payment.retry")}
+
+
+
+
+
+ );
+}
+
+export default FailedProject;
diff --git a/src/app/(projects)/projects/payment/success/page.tsx b/src/app/(projects)/projects/payment/success/page.tsx
new file mode 100644
index 0000000..fb0a464
--- /dev/null
+++ b/src/app/(projects)/projects/payment/success/page.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import Container from "@/components/elements/Container";
+import { IProjectType, Project } from "@/types/types";
+import useAxios from "@/hooks/useAxios";
+import React, { useEffect, useState } from "react";
+import RoundedDiv from "@/components/elements/RoundedDiv";
+import RoundedButton from "@/components/elements/RoundedButton";
+import { useSearchParams } from "next/navigation";
+import Image from "next/image";
+import Link from "next/link";
+import MainProjectCard from "@/components/projects/MainProjectCard";
+import { useTranslation } from "react-i18next";
+import LocalePageShell from "@/components/i18n/LocalePageShell";
+
+function SuccessProject() {
+ const { t } = useTranslation("common");
+ const { request } = useAxios();
+ const searchParams = useSearchParams();
+ const projectId = searchParams.get("projectId");
+
+ const [project, setProject] = useState();
+ const [typeList, setTypeList] = useState(null);
+ const [price, setPrice] = useState("");
+
+ const getDisplayTypeLabel = (projectType?: string) => {
+ if (projectType === "normal" || projectType === "free") {
+ return t("projects.newProject.displaySimple");
+ }
+ if (projectType === "force") {
+ return t("projects.newProject.displayUrgent");
+ }
+ return t("projects.newProject.displayHighlight");
+ };
+
+ useEffect(() => {
+ if (!projectId) return;
+ const fetchAd = async () => {
+ const response = await request<{ project: Project }>(
+ "GET",
+ `/projects/get/web/${projectId}`
+ );
+ setProject(response?.project);
+ };
+ const fetchStates = async () => {
+ try {
+ const response = await request<{ projectTypes: IProjectType[] }>(
+ "GET",
+ "/projects/types"
+ );
+ setTypeList(response?.projectTypes || null);
+ } catch (err) {
+ console.log(err);
+ }
+ };
+ void fetchAd();
+ void fetchStates();
+ }, [projectId, request]);
+
+ useEffect(() => {
+ if (project?.project_type && typeList) {
+ const foundType = typeList.find(
+ (item) => item.name === project.project_type
+ );
+ if (foundType) setPrice(String(foundType.price));
+ }
+ }, [project, typeList]);
+
+ return (
+
+
+
+ {t("projects.payment.success")}
+
+
+
+
+
+ {project &&
}
+
+
+ {getDisplayTypeLabel(project?.project_type)}:{" "}
+ {Number(price).toLocaleString()} {t("settings.toman")}
+
+
{t("projects.payment.reviewNotice")}
+
+
+ {t("projects.payment.workroom")}
+
+
+
+
+
+
+ );
+}
+
+export default SuccessProject;
diff --git a/src/app/(projects)/projects/specialists/page.tsx b/src/app/(projects)/projects/specialists/page.tsx
new file mode 100644
index 0000000..fb2ad43
--- /dev/null
+++ b/src/app/(projects)/projects/specialists/page.tsx
@@ -0,0 +1,480 @@
+"use client";
+
+import React, { useCallback, useRef, useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { AnimatePresence, motion, PanInfo } from "framer-motion";
+import FilterModal from "@/components/models/FilterModal";
+import BoldIcon from "@/components/ui/BoldIcon";
+import Container from "@/components/elements/Container";
+import LocalePageShell from "@/components/i18n/LocalePageShell";
+import useAxios from "@/hooks/useAxios";
+import { buildStorageUrl } from "@/components/main/BaseUrl";
+import { cn } from "@/lib/utils";
+import { useTranslation } from "react-i18next";
+import type { SuggestedFollowUser } from "@/components/posts/FollowingSuggestionsCarousel";
+import { Post } from "@/types/types";
+import { validateMinMaxPairs } from "@/lib/rangeValidation";
+import toast from "react-hot-toast";
+
+const SWIPE_THRESHOLD = 72;
+
+const slideVariants = {
+ enter: (direction: number) => ({
+ x: direction > 0 ? "105%" : "-105%",
+ opacity: 0.35,
+ scale: 0.9,
+ }),
+ center: { x: 0, opacity: 1, scale: 1 },
+ exit: (direction: number) => ({
+ x: direction > 0 ? "-105%" : "105%",
+ opacity: 0.35,
+ scale: 0.9,
+ }),
+};
+
+function getDisplayName(user: SuggestedFollowUser, fallback: string) {
+ return (
+ [user.first_name, user.last_name].filter(Boolean).join(" ") ||
+ user.user_name ||
+ fallback
+ );
+}
+
+function SpecialistCard({
+ user,
+ variant,
+ onSkip,
+ userFallback,
+ viewProfileLabel,
+}: {
+ user: SuggestedFollowUser;
+ variant: "main" | "peek";
+ onSkip?: () => void;
+ userFallback: string;
+ viewProfileLabel: string;
+}) {
+ const displayName = getDisplayName(user, userFallback);
+ const previewPath = user.previewPost?.files?.[0]?.path;
+ const previewUrl = previewPath ? buildStorageUrl(previewPath) : null;
+ const profileUrl = user.profile_image
+ ? buildStorageUrl(user.profile_image)
+ : null;
+ const isMain = variant === "main";
+ const profileHref = user.user_name ? `/users/${user.user_name}` : "#";
+
+ return (
+
+ {previewUrl ? (
+ user.previewPost?.type === "video" ? (
+
+ ) : (
+
+ )
+ ) : (
+
+ )}
+
+
+
+ {isMain && onSkip ? (
+
+ ) : null}
+
+
+
+ {profileUrl ? (
+
+ ) : (
+
+ {displayName.charAt(0)}
+
+ )}
+
+
+ {isMain ? (
+ <>
+
+
{displayName}
+ {user.user_name ? (
+
@{user.user_name}
+ ) : null}
+
+ {user.expertise ? (
+
{user.expertise}
+ ) : null}
+
+ {viewProfileLabel}
+
+ >
+ ) : (
+
+ {displayName}
+
+ )}
+
+
+ );
+}
+
+export default function SpecialistsPage() {
+ const { t } = useTranslation("common");
+ const router = useRouter();
+ const { request } = useAxios();
+ const [phase, setPhase] = useState<"filter" | "results">("filter");
+ const [showFilter, setShowFilter] = useState(true);
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [[index, direction], setSlide] = useState<[number, number]>([0, 0]);
+ const applyingFilterRef = useRef(false);
+
+ const [stateId, setStateId] = useState("");
+ const [cityId, setCityId] = useState("");
+ const [rateFilter, setRateFilter] = useState("");
+ const [userLevel, setUserLevel] = useState("");
+ const [selectedExpertise, setSelectedExpertise] = useState("");
+ const [heightMin, setHeightMin] = useState("");
+ const [heightMax, setHeightMax] = useState("");
+ const [weightMin, setWeightMin] = useState("");
+ const [weightMax, setWeightMax] = useState("");
+ const [sizeMin, setSizeMin] = useState("");
+ const [sizeMax, setSizeMax] = useState("");
+ const [hairColor, setHairColor] = useState("");
+ const [eyeColor, setEyeColor] = useState("");
+
+ const clearFilters = () => {
+ setStateId("");
+ setCityId("");
+ setRateFilter("");
+ setUserLevel("");
+ setSelectedExpertise("");
+ setHeightMin("");
+ setHeightMax("");
+ setWeightMin("");
+ setWeightMax("");
+ setSizeMin("");
+ setSizeMax("");
+ setHairColor("");
+ setEyeColor("");
+ };
+
+ const fetchUsers = useCallback(async () => {
+ setLoading(true);
+ try {
+ const qs = new URLSearchParams({ page: "1", limit: "20" });
+ if (selectedExpertise) qs.set("expertise", selectedExpertise);
+ if (stateId) qs.set("province", stateId);
+ if (cityId) qs.set("city", cityId);
+ if (userLevel) qs.set("userLevel", userLevel);
+ if (rateFilter) qs.set("rateFilter", rateFilter);
+ if (heightMin) qs.set("heightMin", heightMin);
+ if (heightMax) qs.set("heightMax", heightMax);
+ if (weightMin) qs.set("weightMin", weightMin);
+ if (weightMax) qs.set("weightMax", weightMax);
+ if (sizeMin) qs.set("sizeMin", sizeMin);
+ if (sizeMax) qs.set("sizeMax", sizeMax);
+ if (hairColor) qs.set("hair_color", hairColor);
+ if (eyeColor) qs.set("eye_color", eyeColor);
+
+ const response = await request<{
+ posts?: Post[];
+ feedMeta?: { suggestedUsers?: SuggestedFollowUser[] };
+ }>("GET", `/users/web?${qs.toString()}`, null, { noToast: true });
+
+ const suggested = response?.feedMeta?.suggestedUsers;
+ if (Array.isArray(suggested) && suggested.length) {
+ setUsers(suggested);
+ } else {
+ const posts = response?.posts || [];
+ const map = new Map();
+ posts.forEach((post) => {
+ const id = String(post.userId || post.user_id || "");
+ if (!id || map.has(id)) return;
+ map.set(id, {
+ _id: id,
+ user_name: post.user_name,
+ first_name: post.first_name,
+ last_name: post.last_name,
+ expertise: post.expertise,
+ profile_image: post.profile_image,
+ previewPost: {
+ _id: post._id,
+ type: post.type,
+ files: post.files,
+ caption: post.caption,
+ },
+ is_following: post.is_following,
+ });
+ });
+ setUsers(Array.from(map.values()));
+ }
+ setSlide([0, 0]);
+ } catch {
+ setUsers([]);
+ } finally {
+ setLoading(false);
+ }
+ }, [
+ request,
+ selectedExpertise,
+ stateId,
+ cityId,
+ userLevel,
+ rateFilter,
+ heightMin,
+ heightMax,
+ weightMin,
+ weightMax,
+ sizeMin,
+ sizeMax,
+ hairColor,
+ eyeColor,
+ ]);
+
+ const handleFilterChange = () => {
+ const invalid = validateMinMaxPairs([
+ { min: heightMin, max: heightMax },
+ { min: weightMin, max: weightMax },
+ { min: sizeMin, max: sizeMax },
+ ]);
+ if (invalid) {
+ toast.error(t("filters.maxMustBeGreater"));
+ return;
+ }
+ // FilterModal بعد از اعمال، setShowFilterModal(false) میزند —
+ // با ref جلوی برگشت اشتباه به /projects را میگیریم
+ applyingFilterRef.current = true;
+ setPhase("results");
+ setShowFilter(false);
+ void fetchUsers();
+ };
+
+ const current = users[index];
+ const paginate = useCallback(
+ (step: number) => {
+ if (users.length <= 1) return;
+ setSlide(([i]) => [
+ (i + step + users.length) % users.length,
+ step > 0 ? 1 : -1,
+ ]);
+ },
+ [users.length]
+ );
+
+ const handleDragEnd = (_: unknown, info: PanInfo) => {
+ if (info.offset.x < -SWIPE_THRESHOLD) paginate(1);
+ else if (info.offset.x > SWIPE_THRESHOLD) paginate(-1);
+ };
+
+ const prevUser =
+ users.length > 1
+ ? users[(index - 1 + users.length) % users.length]
+ : null;
+ const nextUser =
+ users.length > 1 ? users[(index + 1) % users.length] : null;
+
+ return (
+
+
+
+
+
+ {t("projects.specialists.title")}
+
+
+
+
+ {phase === "filter" || showFilter ? (
+ {
+ if (!open) {
+ if (applyingFilterRef.current) {
+ applyingFilterRef.current = false;
+ setShowFilter(false);
+ return;
+ }
+ if (phase === "results") {
+ setShowFilter(false);
+ } else {
+ router.push("/projects");
+ }
+ } else {
+ setShowFilter(true);
+ }
+ }}
+ showFilterModal={showFilter || phase === "filter"}
+ setStateId={setStateId}
+ setCityId={setCityId}
+ setRateFilter={setRateFilter}
+ setUserLevel={setUserLevel}
+ selectedExpertise={selectedExpertise}
+ setSelectedExpertise={setSelectedExpertise}
+ heightMin={heightMin}
+ setHeightMin={setHeightMin}
+ heightMax={heightMax}
+ setHeightMax={setHeightMax}
+ weightMin={weightMin}
+ setWeightMin={setWeightMin}
+ weightMax={weightMax}
+ setWeightMax={setWeightMax}
+ sizeMin={sizeMin}
+ setSizeMin={setSizeMin}
+ sizeMax={sizeMax}
+ setSizeMax={setSizeMax}
+ hairColor={hairColor}
+ setHairColor={setHairColor}
+ eyeColor={eyeColor}
+ setEyeColor={setEyeColor}
+ stateId={stateId}
+ cityId={cityId}
+ rateFilter={rateFilter}
+ userLevel={userLevel}
+ handleFilterChange={handleFilterChange}
+ clearFilters={clearFilters}
+ />
+ ) : null}
+
+ {phase === "results" && !showFilter ? (
+
+
+ {t("projects.specialists.resultsHint")}
+
+
+
+ {loading ? (
+
+ {t("projects.specialists.loading")}
+
+ ) : !users.length || !current ? (
+
+ {t("projects.specialists.empty")}
+
+ ) : (
+
+ {prevUser ? (
+
+ ) : null}
+ {nextUser ? (
+
+ ) : null}
+
+
+ paginate(1)}
+ userFallback={t("common.user")}
+ viewProfileLabel={t("projects.specialists.viewProfile")}
+ />
+
+
+
+ )}
+
+ ) : null}
+
+
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index 751e860..5e1834e 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -235,6 +235,17 @@ select {
width: 100%;
padding-right: 48px;
}
+
+.project-date-field .rmdp-container,
+.project-date-field .rmdp-input {
+ width: 100% !important;
+}
+
+.project-date-field .rmdp-input {
+ text-align: center !important;
+ padding-left: 2.75rem !important;
+ padding-right: 2.75rem !important;
+}
.address-page .mapboxgl-map {
max-height: 210px;
height: 100%;
@@ -661,6 +672,36 @@ select {
animation: check-pop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
+@keyframes nearby-radar-sweep {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes nearby-blip-glow {
+ 0%,
+ 100% {
+ box-shadow: 0 0 4px 1px rgba(57, 255, 20, 0.55);
+ opacity: 0.85;
+ }
+ 50% {
+ box-shadow: 0 0 10px 3px rgba(57, 255, 20, 0.95);
+ opacity: 1;
+ }
+}
+
+.nearby-radar-sweep {
+ animation: nearby-radar-sweep 4s linear infinite;
+ transform-origin: center center;
+}
+
+.nearby-blip-glow {
+ animation: nearby-blip-glow 1.8s ease-in-out infinite;
+}
+
@keyframes story-ring-spin {
to {
transform: rotate(360deg);
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index c7fd1b7..5782b63 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -5,14 +5,16 @@ import { getSiteSeoMeta, getSeoPage } from "@/lib/i18n/seo";
import { getLanguageDefinition } from "@/lib/i18n/registry";
import { getServerLanguage } from "@/lib/i18n/server";
import SiteJsonLd from "@/components/seo/SiteJsonLd";
-
+import Script from 'next/script';
import "./globals.css";
import Layout from "@/components/Layout";
import RegisterSW from "@/components/RegisterSW";
import ClientErrorBoundary from "@/components/ClientErrorBoundary";
const iranSansFont = localFont({
- src: "./../../public/fonts/IRANSansX-Regular.woff",
+ src: "../../public/fonts/IRANSansX-Regular.woff",
+ display: "swap",
+ fallback: ["Tahoma", "Arial", "sans-serif"],
});
export async function generateMetadata(): Promise {
@@ -22,7 +24,7 @@ export async function generateMetadata(): Promise {
return {
title: {
- default: site.name,
+ default: home.title || site.name,
template: `%s | ${site.titleSuffix}`,
},
description: home.description,
@@ -99,7 +101,20 @@ export default async function RootLayout({
{children}
-