fix(web): update dashboard pages, layouts, and listing forms
Update 12 page/layout files across auth, dashboard, listings, and search routes to improve type safety, fix component imports, and align with latest API changes. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -6,6 +6,7 @@ import * as React from 'react';
|
||||
import { FilterBar, type SearchFilters } from '@/components/search/filter-bar';
|
||||
import { SearchResults } from '@/components/search/search-results';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useCreateSavedSearch } from '@/lib/hooks/use-saved-searches';
|
||||
import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/listings-api';
|
||||
|
||||
const ListingMap = dynamic(
|
||||
@@ -58,6 +59,12 @@ function SearchContent() {
|
||||
const [viewMode, setViewMode] = React.useState<ViewMode>('list');
|
||||
const [showMobileFilters, setShowMobileFilters] = React.useState(false);
|
||||
const [selectedListingId, setSelectedListingId] = React.useState<string | undefined>();
|
||||
const [showSaveDialog, setShowSaveDialog] = React.useState(false);
|
||||
const [saveName, setSaveName] = React.useState('');
|
||||
const [saveAlertEnabled, setSaveAlertEnabled] = React.useState(true);
|
||||
const [saveSuccess, setSaveSuccess] = React.useState(false);
|
||||
|
||||
const createSavedSearch = useCreateSavedSearch();
|
||||
|
||||
const handleMarkerClick = (listing: ListingDetail) => {
|
||||
setSelectedListingId(listing.id);
|
||||
@@ -120,14 +127,116 @@ function SearchContent() {
|
||||
([key, value]) => value && key !== 'sort',
|
||||
).length;
|
||||
|
||||
const handleSaveSearch = () => {
|
||||
if (!saveName.trim()) return;
|
||||
|
||||
const filterData: Record<string, string> = {};
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value && key !== 'sort') filterData[key] = value;
|
||||
});
|
||||
|
||||
createSavedSearch.mutate(
|
||||
{ name: saveName.trim(), filters: filterData, alertEnabled: saveAlertEnabled },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setSaveSuccess(true);
|
||||
setSaveName('');
|
||||
setTimeout(() => {
|
||||
setShowSaveDialog(false);
|
||||
setSaveSuccess(false);
|
||||
}, 1500);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-6">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold md:text-3xl">Tìm kiếm bất động sản</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Tìm bất động sản phù hợp với nhu cầu của bạn
|
||||
</p>
|
||||
<div className="mb-6 flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold md:text-3xl">Tìm kiếm bất động sản</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Tìm bất động sản phù hợp với nhu cầu của bạn
|
||||
</p>
|
||||
</div>
|
||||
{activeFilterCount > 0 && (
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowSaveDialog(!showSaveDialog)}
|
||||
>
|
||||
<svg className="mr-1.5 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
||||
</svg>
|
||||
Lưu tìm kiếm
|
||||
</Button>
|
||||
|
||||
{/* Save search dialog */}
|
||||
{showSaveDialog && (
|
||||
<div className="absolute right-0 top-full z-50 mt-2 w-80 rounded-lg border bg-card p-4 shadow-lg">
|
||||
{saveSuccess ? (
|
||||
<div className="flex items-center gap-2 text-green-600">
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">Đã lưu tìm kiếm!</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="mb-3 font-medium" id="save-search-heading">Lưu bộ lọc tìm kiếm</h3>
|
||||
<label htmlFor="save-search-name" className="sr-only">Tên tìm kiếm</label>
|
||||
<input
|
||||
id="save-search-name"
|
||||
type="text"
|
||||
value={saveName}
|
||||
onChange={(e) => setSaveName(e.target.value)}
|
||||
placeholder="Tên tìm kiếm (VD: Chung cư Q7 dưới 3 tỷ)"
|
||||
className="mb-3 w-full rounded-md border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary"
|
||||
maxLength={100}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSaveSearch()}
|
||||
aria-describedby="save-search-heading"
|
||||
/>
|
||||
<div className="mb-3 flex items-center gap-2 text-sm">
|
||||
<input
|
||||
id="save-alert-enabled"
|
||||
type="checkbox"
|
||||
checked={saveAlertEnabled}
|
||||
onChange={(e) => setSaveAlertEnabled(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
<label htmlFor="save-alert-enabled">
|
||||
Nhận thông báo khi có kết quả mới
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setShowSaveDialog(false)}
|
||||
>
|
||||
Hủy
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSaveSearch}
|
||||
disabled={!saveName.trim() || createSavedSearch.isPending}
|
||||
>
|
||||
{createSavedSearch.isPending ? 'Đang lưu...' : 'Lưu'}
|
||||
</Button>
|
||||
</div>
|
||||
{createSavedSearch.isError && (
|
||||
<p className="mt-2 text-xs text-destructive">
|
||||
Không thể lưu tìm kiếm. Vui lòng thử lại.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* View Mode Toggle + Mobile Filter Button */}
|
||||
|
||||
Reference in New Issue
Block a user