fix: production readiness — resolve build, lint, and code quality issues

- Fix Next.js build failure: remove duplicate route at (dashboard)/listings/[id]
  that conflicted with (public)/listings/[id] (same URL path in two route groups)
- Fix 772 ESLint errors: auto-fix import ordering (import-x/order), remove unused
  imports/variables, convert empty interfaces to type aliases, replace require()
  with ESM imports, fix consistent-type-imports violations
- Add CLAUDE.md for developer onboarding documentation
- All checks pass: 0 lint errors, typecheck clean, 230 tests passing, build success

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 07:15:06 +07:00
parent afa70320f5
commit 2502aa69b7
239 changed files with 746 additions and 984 deletions

View File

@@ -1,7 +1,5 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import Image from 'next/image';
import {
CheckCircle,
XCircle,
@@ -12,11 +10,11 @@ import {
ShieldCheck,
X,
} from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import Image from 'next/image';
import { useEffect, useState, useCallback } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card';
import {
Dialog,
DialogContent,
@@ -25,6 +23,8 @@ import {
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { adminApi, type KycQueueItem, type PaginatedResult } from '@/lib/admin-api';
function kycStatusBadge(status: string) {

View File

@@ -1,6 +1,5 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import {
CheckCircle,
XCircle,
@@ -10,11 +9,10 @@ import {
AlertTriangle,
X,
} from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useEffect, useState, useCallback } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card';
import {
Dialog,
DialogContent,
@@ -23,6 +21,8 @@ import {
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { adminApi, type ModerationQueueItem, type PaginatedResult } from '@/lib/admin-api';
function formatPrice(price: number): string {

View File

@@ -1,6 +1,5 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import {
Users,
Home,
@@ -13,8 +12,9 @@ import {
TrendingUp,
RefreshCw,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useEffect, useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { adminApi, type DashboardStats, type RevenueStatsItem } from '@/lib/admin-api';
interface StatCardProps {

View File

@@ -1,6 +1,5 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import {
Search,
RefreshCw,
@@ -11,12 +10,10 @@ import {
Eye,
X,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useEffect, useState, useCallback } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select } from '@/components/ui/select';
import { Card, CardContent } from '@/components/ui/card';
import {
Dialog,
DialogContent,
@@ -25,6 +22,9 @@ import {
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Select } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import {
adminApi,
type UserListItem,

View File

@@ -1,8 +1,5 @@
'use client';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import {
LayoutDashboard,
Users,
@@ -12,10 +9,12 @@ import {
Menu,
X,
} from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/lib/utils';
import { useAuthStore } from '@/lib/auth-store';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { useAuthStore } from '@/lib/auth-store';
import { cn } from '@/lib/utils';
const adminNavItems = [
{ href: '/admin', label: 'Dashboard', icon: LayoutDashboard },

View File

@@ -1,19 +1,18 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Loader2 } from 'lucide-react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { OAuthButtons } from '@/components/auth/oauth-buttons';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { OAuthButtons } from '@/components/auth/oauth-buttons';
import { loginSchema, type LoginFormData } from '@/lib/validations/auth';
import { useAuthStore } from '@/lib/auth-store';
import { loginSchema, type LoginFormData } from '@/lib/validations/auth';
export default function LoginPage() {
const router = useRouter();

View File

@@ -1,19 +1,18 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Loader2 } from 'lucide-react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { OAuthButtons } from '@/components/auth/oauth-buttons';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { OAuthButtons } from '@/components/auth/oauth-buttons';
import { registerSchema, type RegisterFormData } from '@/lib/validations/auth';
import { useAuthStore } from '@/lib/auth-store';
import { registerSchema, type RegisterFormData } from '@/lib/validations/auth';
export default function RegisterPage() {
const router = useRouter();

View File

@@ -1,16 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import {
analyticsApi,
type MarketReportDistrict,
type HeatmapDataPoint,
type DistrictStats,
type PriceTrendPoint,
} from '@/lib/analytics-api';
import {
BarChart,
Bar,
@@ -23,6 +13,16 @@ import {
ResponsiveContainer,
Legend,
} from 'recharts';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import {
analyticsApi,
type MarketReportDistrict,
type HeatmapDataPoint,
type DistrictStats,
type PriceTrendPoint,
} from '@/lib/analytics-api';
const CITIES = ['Ho Chi Minh', 'Ha Noi', 'Da Nang'];
const CURRENT_PERIOD = '2026-Q1';

View File

@@ -1,18 +1,8 @@
'use client';
import { useEffect, useState } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ListingStatusBadge } from '@/components/listings/listing-status-badge';
import {
analyticsApi,
type MarketReportDistrict,
type HeatmapDataPoint,
} from '@/lib/analytics-api';
import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/listings-api';
import { useEffect, useState } from 'react';
import {
BarChart,
Bar,
@@ -22,6 +12,15 @@ import {
Tooltip,
ResponsiveContainer,
} from 'recharts';
import { ListingStatusBadge } from '@/components/listings/listing-status-badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
analyticsApi,
type MarketReportDistrict,
type HeatmapDataPoint,
} from '@/lib/analytics-api';
import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/listings-api';
const CITY = 'Ho Chi Minh';
const PERIOD = '2026-Q1';

View File

@@ -2,9 +2,9 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/utils';
import { useAuthStore } from '@/lib/auth-store';
import { Button } from '@/components/ui/button';
import { useAuthStore } from '@/lib/auth-store';
import { cn } from '@/lib/utils';
const navItems = [
{ href: '/dashboard', label: 'Bảng điều khiển', icon: '🏠' },

View File

@@ -1,23 +1,23 @@
'use client';
import * as React from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { useParams, useRouter } from 'next/navigation';
import * as React from 'react';
import { useForm } from 'react-hook-form';
import {
StepBasicInfo,
StepLocation,
StepDetails,
StepPricing,
} from '@/components/listings/listing-form-steps';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { listingsApi, type ListingDetail } from '@/lib/listings-api';
import {
createListingSchema,
type CreateListingFormData,
} from '@/lib/validations/listings';
import { listingsApi, type ListingDetail } from '@/lib/listings-api';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
export default function EditListingPage() {
const { id } = useParams<{ id: string }>();

View File

@@ -1,10 +0,0 @@
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Chi tiết tin đăng',
description: 'Xem chi tiết bất động sản trên GoodGo.',
};
export default function ListingDetailLayout({ children }: { children: React.ReactNode }) {
return children;
}

View File

@@ -1,226 +0,0 @@
'use client';
import * as React from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ImageGallery } from '@/components/listings/image-gallery';
import { ListingStatusBadge } from '@/components/listings/listing-status-badge';
import { listingsApi, type ListingDetail } from '@/lib/listings-api';
import { PROPERTY_TYPES, DIRECTIONS, TRANSACTION_TYPES } from '@/lib/validations/listings';
function formatPrice(priceVND: string): string {
const num = Number(priceVND);
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} tỷ`;
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} triệu`;
return num.toLocaleString('vi-VN');
}
function getLabel(list: readonly { value: string; label: string }[], value: string | null) {
if (!value) return '—';
return list.find((item) => item.value === value)?.label ?? value;
}
export default function ListingDetailPage() {
const { id } = useParams<{ id: string }>();
const [listing, setListing] = React.useState<ListingDetail | null>(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
listingsApi
.getById(id)
.then(setListing)
.catch((err) => setError(err instanceof Error ? err.message : 'Không tải được tin đăng'))
.finally(() => setLoading(false));
}, [id]);
if (loading) {
return (
<div className="flex min-h-[400px] items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
);
}
if (error || !listing) {
return (
<div className="flex min-h-[400px] flex-col items-center justify-center space-y-4">
<p className="text-destructive">{error || 'Không tìm thấy tin đăng'}</p>
<Link href="/listings">
<Button variant="outline">Quay lại danh sách</Button>
</Link>
</div>
);
}
const { property, seller, agent } = listing;
return (
<div className="mx-auto max-w-5xl space-y-6">
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="mb-2 flex items-center gap-2">
<ListingStatusBadge status={listing.status} />
<Badge variant="outline">
{getLabel(TRANSACTION_TYPES, listing.transactionType)}
</Badge>
<Badge variant="outline">
{getLabel(PROPERTY_TYPES, property.propertyType)}
</Badge>
</div>
<h1 className="text-2xl font-bold">{property.title}</h1>
<p className="mt-1 text-muted-foreground">
{property.address}, {property.ward}, {property.district}, {property.city}
</p>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-primary">{formatPrice(listing.priceVND)} VNĐ</p>
{listing.pricePerM2 && (
<p className="text-sm text-muted-foreground">
~{listing.pricePerM2.toLocaleString('vi-VN')} VNĐ/m²
</p>
)}
{listing.rentPriceMonthly && (
<p className="text-sm text-muted-foreground">
Thuê: {formatPrice(listing.rentPriceMonthly)}/tháng
</p>
)}
</div>
</div>
{/* Image gallery */}
<ImageGallery media={property.media} />
<div className="grid gap-6 lg:grid-cols-3">
{/* Main content */}
<div className="space-y-6 lg:col-span-2">
{/* Key specs */}
<Card>
<CardHeader>
<CardTitle>Thông tin chung</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
<InfoItem label="Diện tích" value={`${property.areaM2}`} />
<InfoItem label="Phòng ngủ" value={property.bedrooms != null ? `${property.bedrooms}` : '—'} />
<InfoItem label="Phòng tắm" value={property.bathrooms != null ? `${property.bathrooms}` : '—'} />
<InfoItem label="Số tầng" value={property.floors != null ? `${property.floors}` : '—'} />
<InfoItem label="Hướng" value={getLabel(DIRECTIONS, property.direction)} />
<InfoItem label="Năm xây" value={property.yearBuilt ? `${property.yearBuilt}` : '—'} />
<InfoItem label="Pháp lý" value={property.legalStatus || '—'} />
<InfoItem label="Dự án" value={property.projectName || '—'} />
</div>
</CardContent>
</Card>
{/* Description */}
<Card>
<CardHeader>
<CardTitle> tả</CardTitle>
</CardHeader>
<CardContent>
<p className="whitespace-pre-wrap text-sm leading-relaxed">{property.description}</p>
</CardContent>
</Card>
{/* Amenities */}
{property.amenities && property.amenities.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Tiện ích</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{property.amenities.map((a) => (
<Badge key={a} variant="secondary">
{a}
</Badge>
))}
</div>
</CardContent>
</Card>
)}
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Seller info */}
<Card>
<CardHeader>
<CardTitle>Liên hệ</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<p className="font-medium">{seller.fullName}</p>
<p className="text-sm text-muted-foreground">{seller.phone}</p>
</div>
<Button className="w-full">Gọi ngay</Button>
<Button variant="outline" className="w-full">
Nhắn tin
</Button>
</CardContent>
</Card>
{/* Agent info */}
{agent && (
<Card>
<CardHeader>
<CardTitle>Môi giới</CardTitle>
</CardHeader>
<CardContent>
{agent.agency && <p className="text-sm text-muted-foreground">{agent.agency}</p>}
{listing.commissionPct != null && (
<p className="mt-1 text-sm">Hoa hồng: {listing.commissionPct}%</p>
)}
</CardContent>
</Card>
)}
{/* Stats */}
<Card>
<CardHeader>
<CardTitle>Thống </CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Lượt xem</span>
<span className="font-medium">{listing.viewCount}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Lượt lưu</span>
<span className="font-medium">{listing.saveCount}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Liên hệ</span>
<span className="font-medium">{listing.inquiryCount}</span>
</div>
{listing.publishedAt && (
<div className="flex justify-between">
<span className="text-muted-foreground">Đăng ngày</span>
<span className="font-medium">
{new Date(listing.publishedAt).toLocaleDateString('vi-VN')}
</span>
</div>
)}
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
}
function InfoItem({ label, value }: { label: string; value: string }) {
return (
<div>
<p className="text-xs text-muted-foreground">{label}</p>
<p className="font-medium">{value}</p>
</div>
);
}

View File

@@ -1,11 +1,9 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { useRouter } from 'next/navigation';
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { ImageUpload, type ImageFile } from '@/components/listings/image-upload';
import {
StepBasicInfo,
@@ -13,6 +11,10 @@ import {
StepDetails,
StepPricing,
} from '@/components/listings/listing-form-steps';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { listingsApi, type CreateListingPayload, type Direction } from '@/lib/listings-api';
import { cn } from '@/lib/utils';
import {
createListingSchema,
listingBasicSchema,
@@ -21,8 +23,6 @@ import {
listingPricingSchema,
type CreateListingFormData,
} from '@/lib/validations/listings';
import { listingsApi, type CreateListingPayload, type Direction } from '@/lib/listings-api';
import { cn } from '@/lib/utils';
const STEPS = [
{ title: 'Thông tin', schemaKeys: Object.keys(listingBasicSchema.shape) },

View File

@@ -1,18 +1,16 @@
'use client';
import * as React from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Select } from '@/components/ui/select';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import * as React from 'react';
import { ListingStatusBadge } from '@/components/listings/listing-status-badge';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Select } from '@/components/ui/select';
import {
listingsApi,
type ListingDetail,
type ListingStatus,
type PaginatedResult,
} from '@/lib/listings-api';
import { PROPERTY_TYPES, TRANSACTION_TYPES, LISTING_STATUSES } from '@/lib/validations/listings';

View File

@@ -2,9 +2,9 @@
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/utils';
import { useAuthStore } from '@/lib/auth-store';
import { Button } from '@/components/ui/button';
import { useAuthStore } from '@/lib/auth-store';
import { cn } from '@/lib/utils';
export default function PublicLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();

View File

@@ -1,13 +1,13 @@
'use client';
import * as React from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import * as React from 'react';
import { ImageGallery } from '@/components/listings/image-gallery';
import { ListingMap } from '@/components/map/listing-map';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { listingsApi, type ListingDetail } from '@/lib/listings-api';
import { PROPERTY_TYPES, DIRECTIONS, TRANSACTION_TYPES } from '@/lib/validations/listings';

View File

@@ -1,14 +1,14 @@
'use client';
import * as React from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import * as React from 'react';
import { PropertyCard } from '@/components/search/property-card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Select } from '@/components/ui/select';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { PropertyCard } from '@/components/search/property-card';
import { listingsApi, type ListingDetail } from '@/lib/listings-api';
import { PROPERTY_TYPES, TRANSACTION_TYPES } from '@/lib/validations/listings';
@@ -34,7 +34,7 @@ export default function LandingPage() {
const router = useRouter();
const [searchQuery, setSearchQuery] = React.useState('');
const [transactionType, setTransactionType] = React.useState('');
const [propertyType, setPropertyType] = React.useState('');
const [propertyType, _setPropertyType] = React.useState('');
const [featuredListings, setFeaturedListings] = React.useState<ListingDetail[]>([]);
const [loadingFeatured, setLoadingFeatured] = React.useState(true);
const [featuredError, setFeaturedError] = React.useState(false);

View File

@@ -1,11 +1,11 @@
'use client';
import * as React from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import * as React from 'react';
import { ListingMap } from '@/components/map/listing-map';
import { FilterBar, type SearchFilters } from '@/components/search/filter-bar';
import { SearchResults } from '@/components/search/search-results';
import { ListingMap } from '@/components/map/listing-map';
import { Button } from '@/components/ui/button';
import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/listings-api';
type ViewMode = 'list' | 'map' | 'split';

View File

@@ -1,8 +1,8 @@
'use client';
import { useEffect, useRef } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Loader2 } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useRef } from 'react';
import { useAuthStore } from '@/lib/auth-store';
export default function GoogleCallbackPage() {

View File

@@ -1,8 +1,8 @@
'use client';
import { useEffect, useRef } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Loader2 } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useRef } from 'react';
import { useAuthStore } from '@/lib/auth-store';
export default function ZaloCallbackPage() {

View File

@@ -1,9 +1,9 @@
'use client';
import * as React from 'react';
import Image from 'next/image';
import { cn } from '@/lib/utils';
import * as React from 'react';
import type { PropertyMedia } from '@/lib/listings-api';
import { cn } from '@/lib/utils';
interface ImageGalleryProps {
media: PropertyMedia[];

View File

@@ -1,8 +1,8 @@
'use client';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
interface ImageFile {
file: File;
@@ -79,8 +79,7 @@ export function ImageUpload({ images, onChange, maxFiles = 20, className }: Imag
return () => {
images.forEach((img) => URL.revokeObjectURL(img.preview));
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, []); // intentionally empty: runs only on unmount to revoke object URLs
return (
<div className={cn('space-y-4', className)}>

View File

@@ -1,15 +1,15 @@
'use client';
import type { UseFormRegister, FieldErrors } from 'react-hook-form';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import {
TRANSACTION_TYPES,
PROPERTY_TYPES,
DIRECTIONS,
} from '@/lib/validations/listings';
import type { UseFormRegister, FieldErrors } from 'react-hook-form';
import type { CreateListingFormData } from '@/lib/validations/listings';
interface StepProps {

View File

@@ -1,6 +1,6 @@
import { Badge } from '@/components/ui/badge';
import { LISTING_STATUSES } from '@/lib/validations/listings';
import type { ListingStatus } from '@/lib/listings-api';
import { LISTING_STATUSES } from '@/lib/validations/listings';
interface ListingStatusBadgeProps {
status: ListingStatus;

View File

@@ -1,9 +1,8 @@
'use client';
import * as React from 'react';
import mapboxgl from 'mapbox-gl';
import * as React from 'react';
import 'mapbox-gl/dist/mapbox-gl.css';
import { Badge } from '@/components/ui/badge';
import type { ListingDetail } from '@/lib/listings-api';
function formatPrice(priceVND: string): string {

View File

@@ -2,8 +2,8 @@
import Image from 'next/image';
import Link from 'next/link';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import type { ListingDetail } from '@/lib/listings-api';
function formatPrice(priceVND: string): string {

View File

@@ -3,8 +3,8 @@
import * as React from 'react';
import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
import { PropertyCard } from './property-card';
import type { ListingDetail, PaginatedResult } from '@/lib/listings-api';
import { PropertyCard } from './property-card';
interface SearchResultsProps {
result: PaginatedResult<ListingDetail> | null;

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { cn } from '@/lib/utils';
const badgeVariants = cva(

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { cn } from '@/lib/utils';
const buttonVariants = cva(

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {

View File

@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { cn } from '@/lib/utils';
const labelVariants = cva(

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {}
export type SelectProps = React.SelectHTMLAttributes<HTMLSelectElement>;
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, ...props }, ref) => {

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand';
import { authApi, type UserProfile, type LoginPayload, type RegisterPayload } from './auth-api';
import { ApiError } from './api-client';
import { authApi, type UserProfile, type LoginPayload, type RegisterPayload } from './auth-api';
function hasAuthCookie(): boolean {
if (typeof document === 'undefined') return false;

File diff suppressed because one or more lines are too long