feat(web): dashboard gets Dự án + KCN nav; listings pages use list layout
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 5s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m0s
Deploy / Build API Image (push) Failing after 11s
Deploy / Build Web Image (push) Failing after 9s
Deploy / Build AI Services Image (push) Failing after 9s
E2E Tests / Playwright E2E (push) Failing after 7s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 2s
Security Scanning / Trivy Scan — Web Image (push) Failing after 25s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 24s
Security Scanning / Trivy Filesystem Scan (push) Failing after 35s
Deploy / Deploy to Staging (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 0s
Deploy / Rollback Staging (push) Has been skipped
Security Scanning / Trivy Scan — API Image (push) Failing after 26s
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 5s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m0s
Deploy / Build API Image (push) Failing after 11s
Deploy / Build Web Image (push) Failing after 9s
Deploy / Build AI Services Image (push) Failing after 9s
E2E Tests / Playwright E2E (push) Failing after 7s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 2s
Security Scanning / Trivy Scan — Web Image (push) Failing after 25s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 24s
Security Scanning / Trivy Filesystem Scan (push) Failing after 35s
Deploy / Deploy to Staging (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 0s
Deploy / Rollback Staging (push) Has been skipped
Security Scanning / Trivy Scan — API Image (push) Failing after 26s
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Three asks after a walk-through of the dashboard: 1. Dashboard navigation was missing direct entry points to the two catalog surfaces (Dự án, Khu Công Nghiệp) even though both exist at /du-an and /khu-cong-nghiep. Users landing in the dashboard had to go back out to the public header to reach them. 2. The "Tin đăng" (dashboard listings) page defaulted to a 3-column grid which shows only a handful of properties per viewport. Scanning many listings at once is easier as a vertical list of horizontal rows. 3. The public /search results used the same 3-column grid via PropertyCard. Asked to flip to list there too. Changes - (dashboard)/layout.tsx: new `catalogs` nav group with Building2 + Factory icons pointing at /du-an and /khu-cong-nghiep. Primary desktop nav also exposes both so they're reachable without opening the hamburger. Uses existing `nav.projects` / `nav.industrialParks` i18n keys plus a new `dashboard.catalogs` label in vi/en. - (dashboard)/listings/page.tsx: default viewMode flipped from 'grid' to 'list'. The list mode renders a horizontal row per listing (thumbnail + title/location + price + badges + engagement counters) inside an <ul>. Toggle button relabelled "Danh sách". - components/search/search-results.tsx + property-card.tsx: add a `layout?: 'card' | 'list'` prop to PropertyCard. When `list`, the card renders as a horizontal row with 224px thumbnail on sm+, stacked on mobile. SearchResults wraps items in a <ul><li> and asks for list layout. Default card layout preserved so other callers (compare, related, etc.) keep their vertical card view. No API / DB changes. Typecheck clean for the touched surfaces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,9 @@ import {
|
|||||||
BarChart3,
|
BarChart3,
|
||||||
Bookmark,
|
Bookmark,
|
||||||
Bot,
|
Bot,
|
||||||
|
Building2,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
|
Factory,
|
||||||
FileText,
|
FileText,
|
||||||
Gem,
|
Gem,
|
||||||
Home,
|
Home,
|
||||||
@@ -82,6 +84,13 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
{ href: '/listings/new', label: t('dashboard.createListing'), icon: Plus },
|
{ href: '/listings/new', label: t('dashboard.createListing'), icon: Plus },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t('dashboard.catalogs'),
|
||||||
|
items: [
|
||||||
|
{ href: '/du-an', label: t('nav.projects'), icon: Building2 },
|
||||||
|
{ href: '/khu-cong-nghiep', label: t('nav.industrialParks'), icon: Factory },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'CRM',
|
label: 'CRM',
|
||||||
items: [
|
items: [
|
||||||
@@ -112,9 +121,9 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
|||||||
const primaryNav: NavItem[] = [
|
const primaryNav: NavItem[] = [
|
||||||
{ href: '/dashboard', label: t('dashboard.title'), icon: Home },
|
{ href: '/dashboard', label: t('dashboard.title'), icon: Home },
|
||||||
{ href: '/listings', label: t('dashboard.listings'), icon: List },
|
{ href: '/listings', label: t('dashboard.listings'), icon: List },
|
||||||
{ href: '/listings/new', label: t('dashboard.createListing'), icon: Plus },
|
{ href: '/du-an', label: t('nav.projects'), icon: Building2 },
|
||||||
|
{ href: '/khu-cong-nghiep', label: t('nav.industrialParks'), icon: Factory },
|
||||||
{ href: '/inquiries', label: t('dashboard.inquiries'), icon: MessageSquare },
|
{ href: '/inquiries', label: t('dashboard.inquiries'), icon: MessageSquare },
|
||||||
{ href: '/leads', label: t('dashboard.leads'), icon: Target },
|
|
||||||
{ href: '/analytics', label: t('dashboard.analytics'), icon: BarChart3 },
|
{ href: '/analytics', label: t('dashboard.analytics'), icon: BarChart3 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ function formatDate(dateStr: string | null): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type ViewMode = 'grid' | 'table';
|
type ViewMode = 'list' | 'table';
|
||||||
|
|
||||||
export default function ListingsPage() {
|
export default function ListingsPage() {
|
||||||
const [viewMode, setViewMode] = React.useState<ViewMode>('grid');
|
const [viewMode, setViewMode] = React.useState<ViewMode>('list');
|
||||||
const [filters, setFilters] = React.useState({
|
const [filters, setFilters] = React.useState({
|
||||||
transactionType: '',
|
transactionType: '',
|
||||||
propertyType: '',
|
propertyType: '',
|
||||||
@@ -147,11 +147,11 @@ export default function ListingsPage() {
|
|||||||
|
|
||||||
<div className="ml-auto flex gap-1">
|
<div className="ml-auto flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === 'grid' ? 'default' : 'outline'}
|
variant={viewMode === 'list' ? 'default' : 'outline'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setViewMode('grid')}
|
onClick={() => setViewMode('list')}
|
||||||
>
|
>
|
||||||
Lưới
|
Danh sách
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={viewMode === 'table' ? 'default' : 'outline'}
|
variant={viewMode === 'table' ? 'default' : 'outline'}
|
||||||
@@ -177,65 +177,75 @@ export default function ListingsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === 'grid' ? (
|
) : viewMode === 'list' ? (
|
||||||
/* Grid View */
|
/* List View — each row is a horizontal card (image | content | stats) */
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<ul className="flex flex-col gap-3">
|
||||||
{result.data.map((listing) => (
|
{result.data.map((listing) => (
|
||||||
<Link key={listing.id} href={`/listings/${listing.id}`}>
|
<li key={listing.id}>
|
||||||
<Card className="h-full overflow-hidden transition-shadow hover:shadow-md">
|
<Link href={`/listings/${listing.id}`} className="group block">
|
||||||
<div className="relative aspect-[4/3] bg-muted">
|
<Card className="overflow-hidden transition-shadow hover:shadow-md">
|
||||||
{(listing.property.media?.length ?? 0) > 0 ? (
|
<div className="flex flex-col sm:flex-row">
|
||||||
<Image
|
<div className="relative h-40 w-full shrink-0 bg-muted sm:h-32 sm:w-48">
|
||||||
src={listing.property.media![0]?.url ?? ''}
|
{(listing.property.media?.length ?? 0) > 0 ? (
|
||||||
alt={listing.property.title}
|
<Image
|
||||||
fill
|
src={listing.property.media![0]?.url ?? ''}
|
||||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
alt={listing.property.title}
|
||||||
className="object-cover"
|
fill
|
||||||
placeholder="blur"
|
sizes="(max-width: 640px) 100vw, 192px"
|
||||||
blurDataURL={shimmerBlurDataURL()}
|
className="object-cover"
|
||||||
/>
|
placeholder="blur"
|
||||||
) : (
|
blurDataURL={shimmerBlurDataURL()}
|
||||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
/>
|
||||||
Chưa có ảnh
|
) : (
|
||||||
|
<div className="flex h-full items-center justify-center text-xs text-muted-foreground">
|
||||||
|
Chưa có ảnh
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="absolute left-2 top-2">
|
||||||
|
<ListingStatusBadge status={listing.status} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<CardContent className="flex flex-1 flex-col gap-2 p-4">
|
||||||
<div className="absolute left-2 top-2">
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||||
<ListingStatusBadge status={listing.status} />
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="line-clamp-1 font-semibold group-hover:text-primary">
|
||||||
|
{listing.property.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-0.5 line-clamp-1 text-sm text-muted-foreground">
|
||||||
|
{listing.property.district}, {listing.property.city}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="shrink-0 text-lg font-bold text-primary">
|
||||||
|
{formatPrice(listing.priceVND)} VND
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{listing.property.areaM2} m²
|
||||||
|
</Badge>
|
||||||
|
{listing.property.bedrooms != null && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{listing.property.bedrooms} PN
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{listing.property.bathrooms != null && listing.property.bathrooms > 0 && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{listing.property.bathrooms} PT
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto flex flex-wrap gap-3 text-xs text-muted-foreground">
|
||||||
|
<span>{listing.viewCount} lượt xem</span>
|
||||||
|
<span>{listing.inquiryCount} liên hệ</span>
|
||||||
|
<span>{listing.saveCount} đã lưu</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
<CardContent className="p-4">
|
</Link>
|
||||||
<p className="text-lg font-bold text-primary">
|
</li>
|
||||||
{formatPrice(listing.priceVND)} VND
|
|
||||||
</p>
|
|
||||||
<h3 className="mt-1 line-clamp-1 font-medium">{listing.property.title}</h3>
|
|
||||||
<p className="mt-1 line-clamp-1 text-sm text-muted-foreground">
|
|
||||||
{listing.property.district}, {listing.property.city}
|
|
||||||
</p>
|
|
||||||
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{listing.property.areaM2} m²
|
|
||||||
</Badge>
|
|
||||||
{listing.property.bedrooms != null && (
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{listing.property.bedrooms} PN
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{listing.property.bathrooms != null && listing.property.bathrooms > 0 && (
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{listing.property.bathrooms} PT
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 flex gap-3 text-xs text-muted-foreground">
|
|
||||||
<span>{listing.viewCount} lượt xem</span>
|
|
||||||
<span>{listing.inquiryCount} liên hệ</span>
|
|
||||||
<span>{listing.saveCount} đã lưu</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
/* Table View */
|
/* Table View */
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -19,22 +19,133 @@ const PROPERTY_TYPE_LABELS: Record<string, string> = {
|
|||||||
interface PropertyCardProps {
|
interface PropertyCardProps {
|
||||||
listing: ListingDetail;
|
listing: ListingDetail;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
|
/** 'card' (default, vertical) or 'list' (horizontal row) */
|
||||||
|
layout?: 'card' | 'list';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PropertyCard({ listing, compact }: PropertyCardProps) {
|
export function PropertyCard({ listing, compact, layout = 'card' }: PropertyCardProps) {
|
||||||
const transactionLabel = listing.transactionType === 'SALE' ? 'Bán' : 'Cho thuê';
|
const transactionLabel = listing.transactionType === 'SALE' ? 'Bán' : 'Cho thuê';
|
||||||
const propertyTypeLabel = PROPERTY_TYPE_LABELS[listing.property.propertyType] || listing.property.propertyType;
|
const propertyTypeLabel = PROPERTY_TYPE_LABELS[listing.property.propertyType] || listing.property.propertyType;
|
||||||
|
const mediaCount = listing.property.media?.length ?? 0;
|
||||||
|
const firstImage = listing.property.media?.[0]?.url;
|
||||||
|
const directionLabel =
|
||||||
|
listing.property.direction === 'NORTH' ? 'Bắc' :
|
||||||
|
listing.property.direction === 'SOUTH' ? 'Nam' :
|
||||||
|
listing.property.direction === 'EAST' ? 'Đông' :
|
||||||
|
listing.property.direction === 'WEST' ? 'Tây' :
|
||||||
|
listing.property.direction;
|
||||||
|
|
||||||
|
const ariaLabel = `${listing.property.title} — ${transactionLabel} ${propertyTypeLabel}, ${formatPrice(listing.priceVND)} VNĐ`;
|
||||||
|
|
||||||
|
if (layout === 'list') {
|
||||||
|
return (
|
||||||
|
<article aria-label={ariaLabel}>
|
||||||
|
<Link href={`/listings/${listing.id}`} className="group block">
|
||||||
|
<Card className="overflow-hidden transition-shadow hover:shadow-md">
|
||||||
|
<div className="flex flex-col sm:flex-row">
|
||||||
|
<div className="relative h-44 w-full shrink-0 bg-muted sm:h-36 sm:w-56">
|
||||||
|
{firstImage ? (
|
||||||
|
<Image
|
||||||
|
src={firstImage}
|
||||||
|
alt={`Ảnh bất động sản: ${listing.property.title}`}
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 640px) 100vw, 224px"
|
||||||
|
className="object-cover transition-transform group-hover:scale-105"
|
||||||
|
placeholder="blur"
|
||||||
|
blurDataURL={shimmerBlurDataURL()}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex h-full items-center justify-center text-sm text-muted-foreground"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
Chưa có ảnh
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="absolute left-2 top-2 flex gap-1">
|
||||||
|
<Badge variant="default" className="text-xs">
|
||||||
|
{transactionLabel}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{propertyTypeLabel}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{mediaCount > 1 && (
|
||||||
|
<div className="absolute bottom-2 right-2">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-none bg-black/50 text-xs text-white"
|
||||||
|
aria-label={`${mediaCount} ảnh`}
|
||||||
|
>
|
||||||
|
{mediaCount} ảnh
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="absolute right-2 top-2">
|
||||||
|
<AddToCompareButton listingId={listing.id} compact />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardContent className="flex flex-1 flex-col gap-2 p-4">
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="line-clamp-1 font-semibold group-hover:text-primary">
|
||||||
|
{listing.property.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-0.5 line-clamp-1 text-sm text-muted-foreground">
|
||||||
|
{listing.property.address}, {listing.property.district}, {listing.property.city}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="shrink-0 text-lg font-bold text-primary">
|
||||||
|
{formatPrice(listing.priceVND)} VNĐ
|
||||||
|
{listing.transactionType === 'RENT' && listing.rentPriceMonthly && (
|
||||||
|
<span className="text-sm font-normal text-muted-foreground">/tháng</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul className="flex flex-wrap gap-1.5" aria-label="Thông tin bất động sản">
|
||||||
|
<li>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{listing.property.areaM2} m²
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
{listing.property.bedrooms != null && (
|
||||||
|
<li>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{listing.property.bedrooms} PN
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{listing.property.bathrooms != null && listing.property.bathrooms > 0 && (
|
||||||
|
<li>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{listing.property.bathrooms} PT
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{listing.property.direction && (
|
||||||
|
<li>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
Hướng {directionLabel}
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article aria-label={ariaLabel}>
|
||||||
aria-label={`${listing.property.title} — ${transactionLabel} ${propertyTypeLabel}, ${formatPrice(listing.priceVND)} VNĐ`}
|
|
||||||
>
|
|
||||||
<Link href={`/listings/${listing.id}`}>
|
<Link href={`/listings/${listing.id}`}>
|
||||||
<Card className="group h-full overflow-hidden transition-shadow hover:shadow-md">
|
<Card className="group h-full overflow-hidden transition-shadow hover:shadow-md">
|
||||||
<div className={`relative bg-muted ${compact ? 'aspect-[16/10]' : 'aspect-[4/3]'}`}>
|
<div className={`relative bg-muted ${compact ? 'aspect-[16/10]' : 'aspect-[4/3]'}`}>
|
||||||
{(listing.property.media?.length ?? 0) > 0 ? (
|
{firstImage ? (
|
||||||
<Image
|
<Image
|
||||||
src={listing.property.media![0]?.url ?? ''}
|
src={firstImage}
|
||||||
alt={`Ảnh bất động sản: ${listing.property.title}`}
|
alt={`Ảnh bất động sản: ${listing.property.title}`}
|
||||||
fill
|
fill
|
||||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||||
@@ -55,10 +166,10 @@ export function PropertyCard({ listing, compact }: PropertyCardProps) {
|
|||||||
{propertyTypeLabel}
|
{propertyTypeLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
{(listing.property.media?.length ?? 0) > 1 && (
|
{mediaCount > 1 && (
|
||||||
<div className="absolute bottom-2 right-2">
|
<div className="absolute bottom-2 right-2">
|
||||||
<Badge variant="outline" className="bg-black/50 text-xs text-white border-none" aria-label={`${listing.property.media!.length} ảnh`}>
|
<Badge variant="outline" className="bg-black/50 text-xs text-white border-none" aria-label={`${mediaCount} ảnh`}>
|
||||||
{listing.property.media!.length} ảnh
|
{mediaCount} ảnh
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -100,11 +211,7 @@ export function PropertyCard({ listing, compact }: PropertyCardProps) {
|
|||||||
{listing.property.direction && (
|
{listing.property.direction && (
|
||||||
<li>
|
<li>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
Hướng {listing.property.direction === 'NORTH' ? 'Bắc' :
|
Hướng {directionLabel}
|
||||||
listing.property.direction === 'SOUTH' ? 'Nam' :
|
|
||||||
listing.property.direction === 'EAST' ? 'Đông' :
|
|
||||||
listing.property.direction === 'WEST' ? 'Tây' :
|
|
||||||
listing.property.direction}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -90,11 +90,13 @@ export function SearchResults({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
<ul className="flex flex-col gap-3">
|
||||||
{result.data.map((listing) => (
|
{result.data.map((listing) => (
|
||||||
<PropertyCard key={listing.id} listing={listing} />
|
<li key={listing.id}>
|
||||||
|
<PropertyCard listing={listing} layout="list" />
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
|
|
||||||
{result.totalPages > 1 && (
|
{result.totalPages > 1 && (
|
||||||
<div className="flex items-center justify-center gap-2 pt-4">
|
<div className="flex items-center justify-center gap-2 pt-4">
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"listings": "Listings",
|
"listings": "Listings",
|
||||||
"createListing": "Create listing",
|
"createListing": "Create listing",
|
||||||
|
"catalogs": "Catalogs",
|
||||||
"inquiries": "Inquiries",
|
"inquiries": "Inquiries",
|
||||||
"leads": "Leads",
|
"leads": "Leads",
|
||||||
"analytics": "Analytics",
|
"analytics": "Analytics",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"title": "Bảng điều khiển",
|
"title": "Bảng điều khiển",
|
||||||
"listings": "Tin đăng",
|
"listings": "Tin đăng",
|
||||||
"createListing": "Đăng tin",
|
"createListing": "Đăng tin",
|
||||||
|
"catalogs": "Danh mục",
|
||||||
"inquiries": "Liên hệ",
|
"inquiries": "Liên hệ",
|
||||||
"leads": "Lead",
|
"leads": "Lead",
|
||||||
"analytics": "Phân tích",
|
"analytics": "Phân tích",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user