L1f/src/pages/SearchPage.tsx

324 lines
13 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Helmet } from 'react-helmet-async';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
import MenuItem from '@mui/material/MenuItem';
import Chip from '@mui/material/Chip';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import FilterListIcon from '@mui/icons-material/FilterList';
import CloseIcon from '@mui/icons-material/Close';
import SearchIcon from '@mui/icons-material/Search';
import InputAdornment from '@mui/material/InputAdornment';
import CircularProgress from '@mui/material/CircularProgress';
import { PageContainer, SectionWrapper, ProfileCard, SectionHeader, LuxeButton } from '@/components/ui';
import { SearchFilterPanel } from '@/components/search/SearchFilterPanel';
import { useSearchStore } from '@/store';
import { useMediaQuery } from '@/hooks';
import { profileApi } from '@/api/profile';
import type { Profile } from '@/types';
export function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const { filters, setFilters, resetFilters } = useSearchStore();
const [mobileOpen, setMobileOpen] = useState(false);
const isMdUp = useMediaQuery('(min-width:900px)');
// Sync URL query param to search store on load
const queryParam = searchParams.get('q') ?? '';
const cityParam = searchParams.get('city') ?? '';
const categoryParam = searchParams.get('category') ?? '';
useEffect(() => {
const initialFilters: any = {};
if (queryParam) initialFilters.query = queryParam;
if (cityParam) initialFilters.city = cityParam;
if (categoryParam) initialFilters.categories = [categoryParam];
if (Object.keys(initialFilters).length > 0) {
setFilters(initialFilters);
}
}, [queryParam, cityParam, categoryParam, setFilters]);
const [profiles, setProfiles] = useState<Profile[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchProfiles = async () => {
try {
setLoading(true);
const params: any = {};
if (filters.query) params.query = filters.query;
if (filters.city) params.city = filters.city;
if (filters.categories && filters.categories.length > 0) {
params.category = filters.categories[0];
}
if (filters.ageMin !== undefined) params.ageMin = filters.ageMin;
if (filters.ageMax !== undefined) params.ageMax = filters.ageMax;
if (filters.priceMin !== undefined) params.priceMin = filters.priceMin;
if (filters.priceMax !== undefined) params.priceMax = filters.priceMax;
if (filters.verified) params.verified = 'true';
if (filters.premium) params.featured = 'true';
if (filters.sortBy) params.sortBy = filters.sortBy;
const res = await profileApi.searchProfiles(params);
if (res.success && res.data) {
const mapped = res.data.map((listing: any) => {
const primaryImg = listing.gallery?.find((g: any) => g.isPrimary)?.url || listing.gallery?.[0]?.url || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=600&h=800&fit=crop';
return {
id: listing.id,
slug: listing.slug,
name: listing.title,
age: listing.age || 21,
city: listing.city?.name || 'New York',
area: listing.area?.name || '',
avatar: primaryImg,
coverImage: primaryImg,
rating: listing.advertiser?.rating || 5.0,
reviewCount: listing.advertiser?.reviewCount || 0,
isVerified: listing.isVerified || false,
isPremium: listing.isPremium || false,
isOnline: listing.isOnline || true,
priceFrom: listing.price || 200,
currency: listing.currency || 'USD',
categories: listing.category ? [listing.category.name] : ['VIP Escorts'],
languages: listing.languages || ['English'],
height: listing.height || '170 cm',
tagline: listing.tagline || '',
};
});
setProfiles(mapped);
}
} catch (err) {
console.error('Failed to load profiles:', err);
} finally {
setLoading(false);
}
};
fetchProfiles();
}, [filters]);
const sortedResults = profiles;
const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setFilters({ query: val || undefined });
// Update URL params
const nextParams = new URLSearchParams(searchParams);
if (val) {
nextParams.set('q', val);
} else {
nextParams.delete('q');
}
setSearchParams(nextParams);
};
const handleSortChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFilters({ sortBy: e.target.value as any });
};
const handleRemoveFilter = (key: keyof typeof filters, value?: any) => {
if (key === 'categories' && value) {
setFilters({ categories: filters.categories?.filter((c) => c !== value) });
} else if (key === 'languages' && value) {
setFilters({ languages: filters.languages?.filter((l) => l !== value) });
} else {
setFilters({ [key]: undefined });
}
};
const activeFilterChips = [];
if (filters.city) activeFilterChips.push({ label: `City: ${filters.city}`, onClick: () => handleRemoveFilter('city') });
if (filters.verified) activeFilterChips.push({ label: 'Verified', onClick: () => handleRemoveFilter('verified') });
if (filters.online) activeFilterChips.push({ label: 'Online Now', onClick: () => handleRemoveFilter('online') });
if (filters.premium) activeFilterChips.push({ label: 'Premium Showcase', onClick: () => handleRemoveFilter('premium') });
if (filters.ageMin || filters.ageMax) {
activeFilterChips.push({
label: `Age: ${filters.ageMin ?? 18}-${filters.ageMax ?? 50}`,
onClick: () => { setFilters({ ageMin: undefined, ageMax: undefined }); }
});
}
if (filters.priceMin || filters.priceMax) {
activeFilterChips.push({
label: `Price: $${filters.priceMin ?? 100}-$${filters.priceMax ?? 2000}`,
onClick: () => { setFilters({ priceMin: undefined, priceMax: undefined }); }
});
}
filters.categories?.forEach((cat) => {
activeFilterChips.push({ label: `Category: ${cat}`, onClick: () => handleRemoveFilter('categories', cat) });
});
filters.languages?.forEach((lang) => {
activeFilterChips.push({ label: `Lang: ${lang}`, onClick: () => handleRemoveFilter('languages', lang) });
});
return (
<>
<Helmet>
<title>{filters.query ? `Search: ${filters.query}` : 'Premium Companions Directory'} Luxe</title>
<meta name="description" content="Browse our luxury companion directory with advanced filters to find the perfect wellness or dining partner." />
</Helmet>
<SectionWrapper sx={{ pt: 5, pb: 10 }}>
<PageContainer>
{/* Header */}
<SectionHeader
title="Premium Companion Directory"
subtitle="Explore high-end companions, verified hosts, and premium wellness providers."
/>
{/* Controls Bar */}
<Box sx={{ display: 'flex', gap: 2, mb: 4, alignItems: 'center', flexWrap: 'wrap' }}>
<TextField
placeholder="Search by name, city, keyword..."
value={filters.query ?? ''}
onChange={handleSearchInputChange}
size="small"
sx={{ flex: 1, minWidth: 260, '& .MuiOutlinedInput-root': { borderRadius: 3 } }}
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<SearchIcon color="action" />
</InputAdornment>
),
}
}}
/>
<Box sx={{ display: 'flex', gap: 2, width: { xs: '100%', sm: 'auto' }, justifyContent: 'space-between', alignItems: 'center' }}>
<TextField
select
size="small"
label="Sort By"
value={filters.sortBy ?? 'relevance'}
onChange={handleSortChange}
sx={{ minWidth: 160, '& .MuiOutlinedInput-root': { borderRadius: 3 } }}
>
<MenuItem value="relevance">Premium First</MenuItem>
<MenuItem value="rating">Highest Rated</MenuItem>
<MenuItem value="price_asc">Price: Low to High</MenuItem>
<MenuItem value="price_desc">Price: High to Low</MenuItem>
<MenuItem value="newest">New Listings</MenuItem>
</TextField>
{!isMdUp && (
<LuxeButton
variant="outlined"
startIcon={<FilterListIcon />}
onClick={() => setMobileOpen(true)}
sx={{ py: 1 }}
>
Filters
</LuxeButton>
)}
</Box>
</Box>
{/* Active Filters Display */}
{activeFilterChips.length > 0 && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3, alignItems: 'center' }}>
<Typography variant="body2" color="text.secondary" fontWeight={600} sx={{ mr: 1 }}>
Active Filters:
</Typography>
{activeFilterChips.map((chip, idx) => (
<Chip
key={idx}
label={chip.label}
onDelete={chip.onClick}
size="small"
color="secondary"
variant="outlined"
sx={{ borderRadius: 2 }}
/>
))}
<LuxeButton variant="text" size="small" onClick={resetFilters} sx={{ ml: 'auto', p: 0, minWidth: 'auto' }}>
Reset Filters
</LuxeButton>
</Box>
)}
{/* Directory Listings and Filter Sidebar Grid */}
<Grid container spacing={4}>
{/* Desktop Filters Panel */}
{isMdUp && (
<Grid size={{ md: 3.5, lg: 3 }}>
<SearchFilterPanel />
</Grid>
)}
{/* Profiles Directory */}
<Grid size={{ xs: 12, md: 8.5, lg: 9 }}>
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 2, fontWeight: 600 }}>
Showing {sortedResults.length} premium profile{sortedResults.length !== 1 && 's'}
</Typography>
{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 12 }}>
<CircularProgress color="secondary" />
</Box>
) : sortedResults.length > 0 ? (
<Grid container spacing={3}>
{sortedResults.map((profile, i) => (
<Grid key={profile.id} size={{ xs: 12, sm: 6, lg: 4 }}>
<ProfileCard profile={profile} index={i} />
</Grid>
))}
</Grid>
) : (
<Box
sx={{
py: 12,
textAlign: 'center',
border: '1px dashed rgba(255, 255, 255, 0.1)',
borderRadius: 4,
bgcolor: 'background.paper',
}}
>
<Typography variant="h6" fontWeight={600} gutterBottom>
No Companions Found
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
We couldn't find any profiles matching your filters. Try clearing some options or changing your search terms.
</Typography>
<LuxeButton variant="contained" onClick={resetFilters}>
Clear All Filters
</LuxeButton>
</Box>
)}
</Grid>
</Grid>
</PageContainer>
</SectionWrapper>
{/* Mobile Drawer Filter Panel */}
<Drawer
anchor="right"
open={mobileOpen}
onClose={() => setMobileOpen(false)}
slotProps={{
backdrop: { sx: { backdropFilter: 'blur(4px)' } }
}}
PaperProps={{
sx: { width: '100%', maxWidth: 360, bgcolor: 'background.default', p: 0 }
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Typography variant="h6" fontWeight={700}>Filter Options</Typography>
<IconButton onClick={() => setMobileOpen(false)} size="small" aria-label="Close filters">
<CloseIcon />
</IconButton>
</Box>
<Box sx={{ p: 2, overflowY: 'auto', height: 'calc(100% - 64px)' }}>
<SearchFilterPanel />
</Box>
</Drawer>
</>
);
}
export default SearchPage;