'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)}
/>
))}
)}
);
};