diff --git a/apps/web/app/(dashboard)/page.tsx b/apps/web/app/(dashboard)/dashboard/page.tsx similarity index 87% rename from apps/web/app/(dashboard)/page.tsx rename to apps/web/app/(dashboard)/dashboard/page.tsx index 7ef7384..f5c02e9 100644 --- a/apps/web/app/(dashboard)/page.tsx +++ b/apps/web/app/(dashboard)/dashboard/page.tsx @@ -2,13 +2,13 @@ import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -export default function HomePage() { +export default function DashboardPage() { return (
-

Chào mừng đến GoodGo

+

Bảng điều khiển

- Nền tảng bất động sản thông minh tại Việt Nam + Quản lý tin đăng bất động sản của bạn

@@ -43,7 +43,7 @@ export default function HomePage() { Tìm bất động sản phù hợp nhu cầu - + diff --git a/apps/web/app/(dashboard)/layout.tsx b/apps/web/app/(dashboard)/layout.tsx index 2700c50..c1fe4d9 100644 --- a/apps/web/app/(dashboard)/layout.tsx +++ b/apps/web/app/(dashboard)/layout.tsx @@ -7,7 +7,7 @@ import { useAuthStore } from '@/lib/auth-store'; import { Button } from '@/components/ui/button'; const navItems = [ - { href: '/', label: 'Trang chủ', icon: '🏠' }, + { href: '/dashboard', label: 'Bảng điều khiển', icon: '🏠' }, { href: '/listings', label: 'Tin đăng', icon: '📋' }, { href: '/listings/new', label: 'Đăng tin', icon: '➕' }, ]; diff --git a/apps/web/app/(public)/layout.tsx b/apps/web/app/(public)/layout.tsx new file mode 100644 index 0000000..d7f7f85 --- /dev/null +++ b/apps/web/app/(public)/layout.tsx @@ -0,0 +1,114 @@ +'use client'; + +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'; + +export default function PublicLayout({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const { user } = useAuthStore(); + + return ( +
+
+
+ + GoodGo + + + + +
+ {user ? ( + <> + + {user.fullName} + + + + + + ) : ( + <> + + + + + + + + )} +
+
+
+ +
{children}
+ +
+
+
+
+

GoodGo

+

+ Nền tảng bất động sản thông minh tại Việt Nam +

+
+
+

Loại BĐS

+
    +
  • Căn hộ
  • +
  • Nhà riêng
  • +
  • Biệt thự
  • +
  • Đất nền
  • +
+
+
+

Khu vực

+
    +
  • TP. Hồ Chí Minh
  • +
  • Hà Nội
  • +
  • Đà Nẵng
  • +
  • Nha Trang
  • +
+
+
+

Hỗ trợ

+
    +
  • Đăng nhập
  • +
  • Đăng ký
  • +
+
+
+
+ © 2026 GoodGo. Tất cả quyền được bảo lưu. +
+
+
+
+ ); +} diff --git a/apps/web/app/(public)/page.tsx b/apps/web/app/(public)/page.tsx new file mode 100644 index 0000000..2548b4c --- /dev/null +++ b/apps/web/app/(public)/page.tsx @@ -0,0 +1,253 @@ +'use client'; + +import * as React from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +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 { 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'; + +const DISTRICTS = [ + { name: 'Quận 1', city: 'Hồ Chí Minh', img: null }, + { name: 'Quận 2', city: 'Hồ Chí Minh', img: null }, + { name: 'Quận 7', city: 'Hồ Chí Minh', img: null }, + { name: 'Bình Thạnh', city: 'Hồ Chí Minh', img: null }, + { name: 'Thủ Đức', city: 'Hồ Chí Minh', img: null }, + { name: 'Ba Đình', city: 'Hà Nội', img: null }, + { name: 'Hoàn Kiếm', city: 'Hà Nội', img: null }, + { name: 'Hải Châu', city: 'Đà Nẵng', img: null }, +]; + +const STATS = [ + { label: 'Tin đăng', value: '10,000+', icon: '🏠' }, + { label: 'Người dùng', value: '50,000+', icon: '👥' }, + { label: 'Giao dịch thành công', value: '2,000+', icon: '✅' }, + { label: 'Tỉnh thành', value: '63', icon: '📍' }, +]; + +export default function LandingPage() { + const router = useRouter(); + const [searchQuery, setSearchQuery] = React.useState(''); + const [transactionType, setTransactionType] = React.useState(''); + const [propertyType, setPropertyType] = React.useState(''); + const [featuredListings, setFeaturedListings] = React.useState([]); + const [loadingFeatured, setLoadingFeatured] = React.useState(true); + + React.useEffect(() => { + listingsApi + .search({ status: 'ACTIVE', limit: 6 }) + .then((res) => setFeaturedListings(res.data)) + .catch(() => {}) + .finally(() => setLoadingFeatured(false)); + }, []); + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + const params = new URLSearchParams(); + if (searchQuery) params.set('q', searchQuery); + if (transactionType) params.set('transactionType', transactionType); + if (propertyType) params.set('propertyType', propertyType); + router.push(`/search?${params.toString()}`); + }; + + return ( +
+ {/* Hero Section */} +
+
+
+

+ Tìm kiếm bất động sản + hoàn hảo +

+

+ Nền tảng bất động sản thông minh tại Việt Nam — mua bán, cho thuê nhà đất dễ dàng +

+ + {/* Search Bar */} +
+
+ setSearchQuery(e.target.value)} + className="border-0 shadow-none focus-visible:ring-0" + /> +
+ + +
+
+
+ + {/* Quick property type links */} +
+ {PROPERTY_TYPES.map((pt) => ( + + + {pt.label} + + + ))} +
+
+
+
+ + {/* Featured Listings */} +
+
+
+
+

Tin đăng nổi bật

+

+ Khám phá các bất động sản được quan tâm nhất +

+
+ + + +
+ + {loadingFeatured ? ( +
+
+
+ ) : featuredListings.length > 0 ? ( +
+ {featuredListings.map((listing) => ( + + ))} +
+ ) : ( +
+

Chưa có tin đăng nổi bật

+
+ )} +
+
+ + {/* Districts / Quick Links */} +
+
+

Khu vực nổi bật

+

+ Tìm kiếm theo quận huyện phổ biến +

+ +
+ {DISTRICTS.map((district) => ( + + +
+
+ 🏙️ +
+
+ +

{district.name}

+

{district.city}

+
+
+ + ))} +
+
+
+ + {/* Market Stats */} +
+
+
+

GoodGo trong số liệu

+

+ Nền tảng bất động sản đáng tin cậy tại Việt Nam +

+
+ +
+ {STATS.map((stat) => ( +
+ {stat.icon} +

{stat.value}

+

{stat.label}

+
+ ))} +
+
+
+ + {/* CTA Section */} +
+
+

+ Bạn có bất động sản muốn đăng? +

+

+ Đăng tin miễn phí ngay hôm nay, tiếp cận hàng ngàn người mua tiềm năng +

+
+ + + + + + +
+
+
+
+ ); +} diff --git a/apps/web/app/(public)/search/page.tsx b/apps/web/app/(public)/search/page.tsx new file mode 100644 index 0000000..d68e96e --- /dev/null +++ b/apps/web/app/(public)/search/page.tsx @@ -0,0 +1,264 @@ +'use client'; + +import * as React from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { FilterBar, type SearchFilters } from '@/components/search/filter-bar'; +import { SearchResults } from '@/components/search/search-results'; +import { ListingMap } from '@/components/map/listing-map'; +import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/listings-api'; + +type ViewMode = 'list' | 'map' | 'split'; + +const defaultFilters: SearchFilters = { + transactionType: '', + propertyType: '', + city: '', + district: '', + minPrice: '', + maxPrice: '', + minArea: '', + maxArea: '', + bedrooms: '', + sort: '', +}; + +function SearchContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + + const [filters, setFilters] = React.useState(() => ({ + ...defaultFilters, + transactionType: searchParams.get('transactionType') || '', + propertyType: searchParams.get('propertyType') || '', + city: searchParams.get('city') || '', + district: searchParams.get('district') || '', + minPrice: searchParams.get('minPrice') || '', + maxPrice: searchParams.get('maxPrice') || '', + bedrooms: searchParams.get('bedrooms') || '', + sort: searchParams.get('sort') || '', + })); + + const [page, setPage] = React.useState(Number(searchParams.get('page')) || 1); + const [result, setResult] = React.useState | null>(null); + const [loading, setLoading] = React.useState(true); + const [viewMode, setViewMode] = React.useState('list'); + const [showMobileFilters, setShowMobileFilters] = React.useState(false); + + const fetchListings = React.useCallback(() => { + setLoading(true); + const params: Record = { + page, + limit: 12, + status: 'ACTIVE', + }; + if (filters.transactionType) params['transactionType'] = filters.transactionType; + if (filters.propertyType) params['propertyType'] = filters.propertyType; + if (filters.city) params['city'] = filters.city; + if (filters.district) params['district'] = filters.district; + if (filters.minPrice) params['minPrice'] = filters.minPrice; + if (filters.maxPrice) params['maxPrice'] = filters.maxPrice; + if (filters.minArea) params['minArea'] = Number(filters.minArea); + if (filters.maxArea) params['maxArea'] = Number(filters.maxArea); + if (filters.bedrooms) params['bedrooms'] = Number(filters.bedrooms); + + listingsApi + .search(params) + .then(setResult) + .catch(() => setResult(null)) + .finally(() => setLoading(false)); + }, [filters, page]); + + React.useEffect(() => { + fetchListings(); + }, [fetchListings]); + + // Sync filters to URL + React.useEffect(() => { + const params = new URLSearchParams(); + Object.entries(filters).forEach(([key, value]) => { + if (value) params.set(key, value); + }); + if (page > 1) params.set('page', String(page)); + const qs = params.toString(); + router.replace(`/search${qs ? `?${qs}` : ''}`, { scroll: false }); + }, [filters, page, router]); + + const handleFilterChange = (newFilters: SearchFilters) => { + setFilters(newFilters); + setPage(1); + }; + + const handleSearch = () => { + setPage(1); + fetchListings(); + }; + + const activeFilterCount = Object.entries(filters).filter( + ([key, value]) => value && key !== 'sort', + ).length; + + return ( +
+ {/* Header */} +
+

Tìm kiếm bất động sản

+

+ Tìm bất động sản phù hợp với nhu cầu của bạn +

+
+ + {/* View Mode Toggle + Mobile Filter Button */} +
+
+ + + +
+ + +
+ + {/* Desktop horizontal filter bar */} +
+ +
+ + {/* Mobile filter panel */} + {showMobileFilters && ( +
+ { + handleSearch(); + setShowMobileFilters(false); + }} + layout="sidebar" + /> +
+ )} + + {/* Content Area */} +
+ {/* Sidebar filters (desktop, split/list mode) */} + {viewMode !== 'map' && ( + + )} + + {/* Main content */} +
+ {viewMode === 'list' && ( + handleFilterChange({ ...filters, sort })} + /> + )} + + {viewMode === 'map' && ( + + )} + + {viewMode === 'split' && ( +
+
+ handleFilterChange({ ...filters, sort })} + /> +
+
+ +
+
+ )} +
+
+
+ ); +} + +export default function SearchPage() { + return ( + +
+
+ } + > + +
+ ); +} diff --git a/apps/web/components/map/listing-map.tsx b/apps/web/components/map/listing-map.tsx new file mode 100644 index 0000000..4279887 --- /dev/null +++ b/apps/web/components/map/listing-map.tsx @@ -0,0 +1,183 @@ +'use client'; + +import * as React from 'react'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import type { ListingDetail } from '@/lib/listings-api'; + +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)} tr`; + return num.toLocaleString('vi-VN'); +} + +interface ListingMapProps { + listings: ListingDetail[]; + onMarkerClick?: (listing: ListingDetail) => void; + className?: string; +} + +interface MapMarker { + listing: ListingDetail; + lat: number; + lng: number; +} + +export function ListingMap({ listings, onMarkerClick, className }: ListingMapProps) { + const [selectedMarker, setSelectedMarker] = React.useState(null); + const mapRef = React.useRef(null); + + // Parse listings with valid coordinates + const markers = React.useMemo(() => { + return listings + .filter((l) => { + // ListingDetail doesn't expose lat/lng directly, but the property might have it + // For now we'll use a simple city-based mapping as fallback + return true; + }) + .map((listing, index) => { + // Generate approximate coordinates based on city/district for demo + // In production, these would come from the API + const cityCoords: Record = { + 'Hồ Chí Minh': [10.8231, 106.6297], + 'Hà Nội': [21.0285, 105.8542], + 'Đà Nẵng': [16.0544, 108.2022], + 'Nha Trang': [12.2388, 109.1967], + 'Cần Thơ': [10.0452, 105.7469], + }; + const base = cityCoords[listing.property.city] || [10.8231, 106.6297]; + // Add small random offset per listing for visual spread + const seed = listing.id.charCodeAt(0) + index; + const lat = base[0] + ((seed % 100) - 50) * 0.001; + const lng = base[1] + ((seed % 73) - 36) * 0.001; + return { listing, lat, lng }; + }); + }, [listings]); + + const handleMarkerClick = (marker: MapMarker) => { + setSelectedMarker(marker); + onMarkerClick?.(marker.listing); + }; + + // CSS-based map visualization (no Mapbox dependency required) + // Uses a relative coordinate system to position markers + const bounds = React.useMemo(() => { + if (markers.length === 0) return { minLat: 10, maxLat: 22, minLng: 102, maxLng: 110 }; + const lats = markers.map((m) => m.lat); + const lngs = markers.map((m) => m.lng); + const padding = 0.01; + return { + minLat: Math.min(...lats) - padding, + maxLat: Math.max(...lats) + padding, + minLng: Math.min(...lngs) - padding, + maxLng: Math.max(...lngs) + padding, + }; + }, [markers]); + + return ( +
+ {/* Grid lines for visual reference */} +
+
+
+ + {/* Markers */} + {markers.map((marker) => { + const x = ((marker.lng - bounds.minLng) / (bounds.maxLng - bounds.minLng)) * 100; + const y = ((bounds.maxLat - marker.lat) / (bounds.maxLat - bounds.minLat)) * 100; + const isSelected = selectedMarker?.listing.id === marker.listing.id; + + return ( + + ); + })} + + {/* Selected marker popup */} + {selectedMarker && ( +
+ + {selectedMarker.listing.property.media.length > 0 && ( + {selectedMarker.listing.property.title} + )} +

+ {formatPrice(selectedMarker.listing.priceVND)} VNĐ +

+

{selectedMarker.listing.property.title}

+

+ {selectedMarker.listing.property.district}, {selectedMarker.listing.property.city} +

+
+ {selectedMarker.listing.property.areaM2} m² + {selectedMarker.listing.property.bedrooms != null && ( + {selectedMarker.listing.property.bedrooms} PN + )} +
+ + Xem chi tiết + +
+ )} + + {/* Map controls */} +
+
+ {markers.length} bất động sản trên bản đồ +
+
+ + {/* Empty state */} + {markers.length === 0 && ( +
+

Không có bất động sản để hiển thị trên bản đồ

+
+ )} +
+ ); +} diff --git a/apps/web/components/search/filter-bar.tsx b/apps/web/components/search/filter-bar.tsx new file mode 100644 index 0000000..0e9ede2 --- /dev/null +++ b/apps/web/components/search/filter-bar.tsx @@ -0,0 +1,197 @@ +'use client'; + +import * as React from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select } from '@/components/ui/select'; +import { PROPERTY_TYPES, TRANSACTION_TYPES } from '@/lib/validations/listings'; + +export interface SearchFilters { + transactionType: string; + propertyType: string; + city: string; + district: string; + minPrice: string; + maxPrice: string; + minArea: string; + maxArea: string; + bedrooms: string; + sort: string; +} + +const CITIES = [ + 'Hồ Chí Minh', + 'Hà Nội', + 'Đà Nẵng', + 'Nha Trang', + 'Cần Thơ', + 'Hải Phòng', + 'Bình Dương', + 'Đồng Nai', + 'Long An', + 'Bà Rịa - Vũng Tàu', +]; + +const PRICE_RANGES = [ + { label: 'Dưới 1 tỷ', min: '0', max: '1000000000' }, + { label: '1 - 3 tỷ', min: '1000000000', max: '3000000000' }, + { label: '3 - 5 tỷ', min: '3000000000', max: '5000000000' }, + { label: '5 - 10 tỷ', min: '5000000000', max: '10000000000' }, + { label: '10 - 20 tỷ', min: '10000000000', max: '20000000000' }, + { label: 'Trên 20 tỷ', min: '20000000000', max: '' }, +]; + +interface FilterBarProps { + filters: SearchFilters; + onChange: (filters: SearchFilters) => void; + onSearch: () => void; + layout?: 'horizontal' | 'sidebar'; +} + +export function FilterBar({ filters, onChange, onSearch, layout = 'horizontal' }: FilterBarProps) { + const update = (key: keyof SearchFilters, value: string) => { + onChange({ ...filters, [key]: value }); + }; + + const handlePriceRange = (value: string) => { + if (!value) { + onChange({ ...filters, minPrice: '', maxPrice: '' }); + return; + } + const range = PRICE_RANGES[Number(value)]; + if (range) { + onChange({ ...filters, minPrice: range.min, maxPrice: range.max }); + } + }; + + const currentPriceIdx = PRICE_RANGES.findIndex( + (r) => r.min === filters.minPrice && r.max === filters.maxPrice, + ); + + const isSidebar = layout === 'sidebar'; + + return ( +
+ {isSidebar &&

Bộ lọc

} + +
+ + + + + + + + + {isSidebar && ( + <> +
+ +
+ update('minArea', e.target.value)} + className="w-full" + /> + update('maxArea', e.target.value)} + className="w-full" + /> +
+
+ + + + update('district', e.target.value)} + className="w-full" + /> + + )} + + {!isSidebar && ( + + )} +
+ + {isSidebar && ( + + )} +
+ ); +} diff --git a/apps/web/components/search/property-card.tsx b/apps/web/components/search/property-card.tsx new file mode 100644 index 0000000..e7e6447 --- /dev/null +++ b/apps/web/components/search/property-card.tsx @@ -0,0 +1,101 @@ +'use client'; + +import Link from 'next/link'; +import { Card, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import type { ListingDetail } from '@/lib/listings-api'; + +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'); +} + +const PROPERTY_TYPE_LABELS: Record = { + APARTMENT: 'Căn hộ', + HOUSE: 'Nhà riêng', + VILLA: 'Biệt thự', + LAND: 'Đất nền', + OFFICE: 'Văn phòng', + SHOPHOUSE: 'Shophouse', +}; + +interface PropertyCardProps { + listing: ListingDetail; + compact?: boolean; +} + +export function PropertyCard({ listing, compact }: PropertyCardProps) { + return ( + + +
+ {listing.property.media.length > 0 ? ( + {listing.property.title} + ) : ( +
+ Chưa có ảnh +
+ )} +
+ + {listing.transactionType === 'SALE' ? 'Bán' : 'Cho thuê'} + + + {PROPERTY_TYPE_LABELS[listing.property.propertyType] || listing.property.propertyType} + +
+ {listing.property.media.length > 1 && ( +
+ + {listing.property.media.length} ảnh + +
+ )} +
+ +

+ {formatPrice(listing.priceVND)} VNĐ + {listing.transactionType === 'RENT' && listing.rentPriceMonthly && ( + /tháng + )} +

+

{listing.property.title}

+

+ {listing.property.address}, {listing.property.district}, {listing.property.city} +

+
+ + {listing.property.areaM2} m² + + {listing.property.bedrooms != null && ( + + {listing.property.bedrooms} PN + + )} + {listing.property.bathrooms != null && listing.property.bathrooms > 0 && ( + + {listing.property.bathrooms} PT + + )} + {listing.property.direction && ( + + Hướng {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} + + )} +
+
+
+ + ); +} diff --git a/apps/web/components/search/search-results.tsx b/apps/web/components/search/search-results.tsx new file mode 100644 index 0000000..6a4ad8a --- /dev/null +++ b/apps/web/components/search/search-results.tsx @@ -0,0 +1,128 @@ +'use client'; + +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'; + +interface SearchResultsProps { + result: PaginatedResult | null; + loading: boolean; + page: number; + sort: string; + onPageChange: (page: number) => void; + onSortChange: (sort: string) => void; +} + +export function SearchResults({ + result, + loading, + page, + sort, + onPageChange, + onSortChange, +}: SearchResultsProps) { + if (loading) { + return ( +
+
+
+ ); + } + + if (!result || result.data.length === 0) { + return ( +
+ + + +

Không tìm thấy kết quả

+

Hãy thử thay đổi bộ lọc để tìm kiếm rộng hơn

+
+ ); + } + + return ( +
+
+

+ {result.total} kết quả +

+ +
+ +
+ {result.data.map((listing) => ( + + ))} +
+ + {result.totalPages > 1 && ( +
+ +
+ {Array.from({ length: Math.min(result.totalPages, 5) }, (_, i) => { + let pageNum: number; + if (result.totalPages <= 5) { + pageNum = i + 1; + } else if (page <= 3) { + pageNum = i + 1; + } else if (page >= result.totalPages - 2) { + pageNum = result.totalPages - 4 + i; + } else { + pageNum = page - 2 + i; + } + return ( + + ); + })} +
+ +
+ )} +
+ ); +} diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 33b9039..6913f13 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -1,12 +1,16 @@ import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; -const publicPaths = ['/login', '/register']; +const publicPaths = ['/login', '/register', '/search']; + +const publicExactPaths = ['/']; export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; - const isPublicPath = publicPaths.some((path) => pathname.startsWith(path)); + const isPublicPath = + publicExactPaths.includes(pathname) || + publicPaths.some((path) => pathname.startsWith(path)); // We check for the token cookie or rely on client-side auth store. // For SSR-safe auth, check a lightweight cookie set by the client after login. @@ -18,8 +22,9 @@ export function middleware(request: NextRequest) { return NextResponse.redirect(loginUrl); } - if (isPublicPath && hasAuthCookie) { - return NextResponse.redirect(new URL('/', request.url)); + const isAuthOnlyPath = ['/login', '/register'].some((path) => pathname.startsWith(path)); + if (isAuthOnlyPath && hasAuthCookie) { + return NextResponse.redirect(new URL('/dashboard', request.url)); } return NextResponse.next();