Created Page Feed

This commit is contained in:
Thanh Quy - wolf
2026-03-29 20:05:53 +07:00
parent e95a64cd2c
commit e2aeebd0e1
16 changed files with 1088 additions and 2812 deletions
+188
View File
@@ -0,0 +1,188 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { MOCK_SHOPS } from "@/lib/constants";
export default function FeedPage() {
const [searchName, setSearchName] = useState("");
const [searchAddress, setSearchAddress] = useState("");
const filteredShops = MOCK_SHOPS.filter((shop) => {
const matchesName =
searchName.trim() === "" ||
shop.name.toLowerCase().includes(searchName.toLowerCase());
const matchesAddress =
searchAddress.trim() === "" ||
shop.address.toLowerCase().includes(searchAddress.toLowerCase());
return matchesName && matchesAddress;
});
return (
<main className="bg-background min-h-[calc(100vh-var(--spacing-header-height))]">
<div className="max-w-screen-xl mx-auto px-4 md:px-6 lg:px-8 py-8">
{/* Page title */}
<div className="mb-8">
<h1 className="text-2xl md:text-3xl font-bold text-foreground">
Khám phá quán nước
</h1>
<p className="text-sm text-(--color-text-muted) mt-1">
Tìm chọn quán yêu thích của bạn
</p>
</div>
{/* Shop cards grid */}
{filteredShops.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-10">
{filteredShops.map((shop) => (
<div
key={shop.id}
className="bg-(--color-bg-card) rounded-2xl border border-(--color-border)
shadow-[0_2px_12px_var(--color-shadow-sm)]
overflow-hidden
hover:shadow-[0_4px_20px_var(--color-shadow-md)]
hover:-translate-y-1
transition-all duration-250"
>
{/* Shop image */}
<div className="relative w-full h-48 sm:h-52">
<Image
src={shop.image}
alt={shop.name}
fill
className="object-cover"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
</div>
{/* Card body */}
<div className="p-4">
{/* Name + View menu button */}
<div className="flex items-center justify-between gap-3 mb-2">
<h3 className="font-bold text-base text-foreground truncate">
{shop.name}
</h3>
<Link
href="/"
className="shrink-0 inline-flex items-center gap-1.5
px-3.5 py-2 rounded-xl text-xs font-semibold
bg-(--color-primary) text-white
hover:bg-(--color-primary-dark)
active:scale-95
transition-all duration-150 no-underline"
>
<i className="fa-solid fa-book-open text-[10px]"></i>
Xem menu
</Link>
</div>
{/* Address */}
<div className="flex items-start gap-2 text-sm text-(--color-text-muted)">
<i className="fa-solid fa-location-dot mt-0.5 shrink-0 text-(--color-accent)"></i>
<span>{shop.address}</span>
</div>
</div>
</div>
))}
</div>
) : (
/* Empty state */
<div className="flex flex-col items-center justify-center py-24 gap-4 text-(--color-text-muted) mb-10">
<i className="fa-solid fa-store text-5xl opacity-30"></i>
<p className="text-base font-medium">
Không tìm thấy quán nào phù hợp
</p>
<button
onClick={() => {
setSearchName("");
setSearchAddress("");
}}
className="text-sm text-(--color-primary) hover:underline cursor-pointer
border-none bg-transparent"
>
Xóa bộ lọc
</button>
</div>
)}
{/* Filter / Search bar */}
<div
className="sticky bottom-0 bg-(--color-bg-card) border border-(--color-border)
rounded-2xl shadow-[0_-2px_16px_var(--color-shadow-sm)]
p-4 md:p-5"
>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3">
{/* Label */}
<div className="flex items-center gap-2 shrink-0 text-sm font-semibold text-(--color-text-secondary)">
<i className="fa-solid fa-filter text-(--color-primary)"></i>
<span>Lọc quán</span>
</div>
{/* Search by name */}
<div className="relative flex-1 min-w-0">
<i
className="fa-solid fa-store absolute left-3 top-1/2 -translate-y-1/2
text-xs text-(--color-text-muted) pointer-events-none"
></i>
<input
type="text"
value={searchName}
onChange={(e) => setSearchName(e.target.value)}
placeholder="Tìm theo tên quán..."
className="w-full pl-9 pr-9 py-2.5 text-sm rounded-xl border outline-none
bg-background text-foreground
border-(--color-border) placeholder:text-(--color-text-muted)
focus:border-(--color-primary) focus:ring-2
focus:ring-(--color-primary) focus:ring-opacity-20
transition-all duration-150"
/>
{searchName && (
<button
onClick={() => setSearchName("")}
aria-label="Xóa tìm kiếm tên"
className="absolute right-3 top-1/2 -translate-y-1/2 text-(--color-text-muted)
hover:text-(--color-primary) transition-colors duration-150
cursor-pointer border-none bg-transparent p-0"
>
<i className="fa-solid fa-xmark text-sm"></i>
</button>
)}
</div>
{/* Search by address */}
<div className="relative flex-1 min-w-0">
<i
className="fa-solid fa-location-dot absolute left-3 top-1/2 -translate-y-1/2
text-xs text-(--color-text-muted) pointer-events-none"
></i>
<input
type="text"
value={searchAddress}
onChange={(e) => setSearchAddress(e.target.value)}
placeholder="Tìm theo địa chỉ..."
className="w-full pl-9 pr-9 py-2.5 text-sm rounded-xl border outline-none
bg-background text-foreground
border-(--color-border) placeholder:text-(--color-text-muted)
focus:border-(--color-primary) focus:ring-2
focus:ring-(--color-primary) focus:ring-opacity-20
transition-all duration-150"
/>
{searchAddress && (
<button
onClick={() => setSearchAddress("")}
aria-label="Xóa tìm kiếm địa chỉ"
className="absolute right-3 top-1/2 -translate-y-1/2 text-(--color-text-muted)
hover:text-(--color-primary) transition-colors duration-150
cursor-pointer border-none bg-transparent p-0"
>
<i className="fa-solid fa-xmark text-sm"></i>
</button>
)}
</div>
</div>
</div>
</div>
</main>
);
}
+58
View File
@@ -0,0 +1,58 @@
import Image from "next/image";
import Link from "next/link";
export default function FeedLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
{/* Custom Drinkool header — no login button */}
<header
className="w-full sticky top-0 z-50
bg-(--color-bg-header) border-b border-(--color-border)
shadow-[0_1px_8px_var(--color-shadow-sm)]
h-(--spacing-header-height)"
>
<div
className="h-full px-6 md:px-8 lg:px-12
flex items-center gap-4
max-w-screen-2xl mx-auto"
>
<Link
href="/feed"
className="flex items-center gap-3 shrink-0 no-underline group"
>
{/* Logo */}
<div className="relative w-10 h-10 md:w-11 md:h-11 shrink-0">
<Image
src="/imgs/logo.png"
alt="Logo Drinkool"
fill
className="object-contain transition-transform duration-200 group-hover:scale-105"
sizes="44px"
priority
/>
</div>
{/* Brand name */}
<span
className="font-bold text-lg md:text-xl
text-(--color-primary-dark)
group-hover:text-(--color-primary)
transition-colors duration-150"
>
Drinkool
</span>
</Link>
</div>
</header>
{/* Page content */}
<div className="flex-1">{children}</div>
{/* No footer */}
</>
);
}
+25
View File
@@ -0,0 +1,25 @@
import Header from "@/layouts/header";
import Footer from "@/layouts/footer";
import CartFab from "@/components/CartFab";
export default function MainLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
{/* Sticky top header */}
<Header />
{/* Page content (grows to fill remaining height) */}
<div className="flex-1">{children}</div>
{/* Footer always at bottom */}
<Footer />
{/* Global floating cart button */}
<CartFab />
</>
);
}
+1
View File
@@ -100,6 +100,7 @@ body {
color: var(--color-text-primary);
font-family: Arial, Helvetica, sans-serif;
scroll-behavior: smooth;
text-rendering: optimizeSpeed;
}
/* ============================================================
+1 -16
View File
@@ -1,9 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Header from "@/layouts/header";
import Footer from "@/layouts/footer";
import CartFab from "@/components/CartFab";
import { Providers } from "./providers";
const geistSans = Geist({
@@ -46,19 +43,7 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen`}
>
<Providers>
{/* Sticky top header */}
<Header />
{/* Page content (grows to fill remaining height) */}
<div className="flex-1">
{children}
</div>
{/* Footer always at bottom */}
<Footer />
{/* Global floating cart button */}
<CartFab />
{children}
</Providers>
</body>
</html>