'use client'; import { Suspense, type FunctionComponent, type ReactNode } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useLocale, useTranslations } from 'next-intl'; import { Box, InputAdornment, Paper, Skeleton, Stack, TextField, ToggleButton, ToggleButtonGroup, Typography, } from '@mui/material'; import { AppButton, AppIcon, AppLoading, CategoryTile } from '@/components'; import CascadingRegionSelect from '@/components/geography/CascadingRegionSelect'; import { ROUTES } from '@/constants'; import { useServiceCategories } from '@/services/catalog'; import { pickCatalogName } from '@/services/catalog/names'; import { useNurseSearch } from '@/services/search'; import { filtersToSearchParams } from '@/services/search/filterParams'; import type { NurseGender } from '@/services/search/types'; import { useSearchFilters } from './useSearchFilters'; /** * C1 — Search & filter (جستجو و فیلتر): the discovery entry screen. Pick a care category (reusing the * f4 catalog grid), a city (reusing the f3 cascading region picker; district optional = whole city), * the **prominent same-gender facet**, and an optional Toman price range; a live result count drives the * "مشاهده N پرستار" CTA into C2. Availability (date) is intent-only at MVP — it is carried to booking, * never used to hard-filter results. `useSearchParams` needs a Suspense boundary under static rendering. */ export default function SearchPage() { return ( }> ); } const GENDER_OPTIONS: readonly (NurseGender | 'any')[] = ['female', 'male', 'any']; function SearchFilterScreen() { const t = useTranslations('search'); const router = useRouter(); const locale = useLocale(); const params = useSearchParams(); const initialCategoryRaw = Number(params.get('category_id')); const initialCategoryId = Number.isInteger(initialCategoryRaw) && initialCategoryRaw > 0 ? initialCategoryRaw : undefined; const controller = useSearchFilters(initialCategoryId); const { data, isFetching } = useNurseSearch(controller.filters); const count = data?.total; const goToResults = () => { const query = filtersToSearchParams(controller.filters); if (controller.dateIntent) query.set('date', controller.dateIntent); router.push(`/${locale}${ROUTES.SEARCH_RESULTS}?${query.toString()}`); }; const ctaLabel = !controller.isReady ? t('cta_choose_category_city') : isFetching || count == null ? t('cta_loading') : t('cta_view_results', { count }); return ( {t('title')} {t('subtitle')} { if (value != null) controller.setGender(value === 'any' ? undefined : value); }} > {GENDER_OPTIONS.map((option) => ( {t(`gender_${option}`)} ))} controller.setDateIntent(event.target.value)} slotProps={{ inputLabel: { shrink: true } }} /> {ctaLabel} ); } const FilterSection: FunctionComponent<{ title: string; hint?: string; children: ReactNode }> = ({ title, hint, children, }) => ( {title} {hint ? ( {hint} ) : null} {children} ); const PriceField: FunctionComponent<{ label: string; value: string; onChange: (value: string) => void; adornment: string; }> = ({ label, value, onChange, adornment }) => ( onChange(event.target.value)} inputMode="numeric" fullWidth slotProps={{ input: { endAdornment: {adornment} }, }} /> ); /** The reused f4 category grid (data-driven from the cached catalog reference data), with selection. */ const CategorySelect: FunctionComponent<{ selectedId: number | null; onSelect: (id: number) => void }> = ({ selectedId, onSelect, }) => { const t = useTranslations('search'); const locale = useLocale(); const { data, isLoading, isError } = useServiceCategories(); const categories = data?.items ?? []; return ( {isLoading ? ( {[0, 1, 2, 3].map((key) => ( ))} ) : isError ? ( {t('categories_error')} ) : ( {categories.map((category) => ( onSelect(category.id)} /> ))} )} ); };