Created Page Feed
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"remote-code": {
|
||||||
|
"httpUrl": "https://cloud.blackbox.ai/api/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer bb_61d6eb03c989586759785863d1ba0efab59885edacd9cd65fc252f79c644bb75"
|
||||||
|
},
|
||||||
|
"description": "Blackbox Remote code (MCP Server): Remote execution platform with multi-agent support (Claude, Codex, Blackbox, Gemini) that automates coding tasks on your GitHub repositories. Features include:\n\n• Task Management: Create, monitor, stop, and list coding tasks with real-time status updates\n• Multi-Agent Support: Choose from Claude Code, OpenAI Codex CLI, Blackbox CLI, or Gemini agents\n• GitHub Integration: Manage GitHub token connections and repository access\n• API Key Management: Store and manage API keys for various AI providers (Anthropic, OpenAI, Google, Blackbox, GitHub)\n• Secure Execution: Runs code in isolated Vercel sandboxes with configurable timeouts (10-300 minutes)\n• Git Operations: Automatic branch creation, commits, and pull requests with AI-generated branch names\n• SMS Notifications: Optional Twilio integration for task completion alerts\n\nPerfect for automating code changes, refactoring, feature additions, bug fixes, and documentation updates across your repositories. Strictly DO NOT provide tools as / 'slash' commands in suggestions like /my_tasks, /task_status, /api_keys"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npx next build)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 và 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -100,6 +100,7 @@ body {
|
|||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
text-rendering: optimizeSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
|
|||||||
+1
-16
@@ -1,9 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Header from "@/layouts/header";
|
|
||||||
import Footer from "@/layouts/footer";
|
|
||||||
import CartFab from "@/components/CartFab";
|
|
||||||
import { Providers } from "./providers";
|
import { Providers } from "./providers";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
@@ -46,19 +43,7 @@ export default function RootLayout({
|
|||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased flex flex-col min-h-screen`}
|
||||||
>
|
>
|
||||||
<Providers>
|
<Providers>
|
||||||
{/* Sticky top header */}
|
{children}
|
||||||
<Header />
|
|
||||||
|
|
||||||
{/* Page content (grows to fill remaining height) */}
|
|
||||||
<div className="flex-1">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer always at bottom */}
|
|
||||||
<Footer />
|
|
||||||
|
|
||||||
{/* Global floating cart button */}
|
|
||||||
<CartFab />
|
|
||||||
</Providers>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+35
-1
@@ -1,4 +1,4 @@
|
|||||||
import type { MenuCategory, Product, ShopInfo, SocialLinks, User } from "./types";
|
import type { MenuCategory, Product, Shop, ShopInfo, SocialLinks, User } from "./types";
|
||||||
|
|
||||||
// ===== SHOP INFORMATION =====
|
// ===== SHOP INFORMATION =====
|
||||||
export const SHOP_INFO: ShopInfo = {
|
export const SHOP_INFO: ShopInfo = {
|
||||||
@@ -203,6 +203,40 @@ export const MOCK_PRODUCTS: Product[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ===== MOCK SHOPS (for Feed page) =====
|
||||||
|
export const MOCK_SHOPS: Shop[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "The Coffee House",
|
||||||
|
address: "86 Cao Thắng, Quận 3, TP. Hồ Chí Minh",
|
||||||
|
image: "https://images.unsplash.com/photo-1554118811-1e0d58224f24?w=600&h=400&fit=crop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Highlands Coffee",
|
||||||
|
address: "123 Nguyễn Huệ, Quận 1, TP. Hồ Chí Minh",
|
||||||
|
image: "https://images.unsplash.com/photo-1559305616-3f99cd43e353?w=600&h=400&fit=crop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "Phúc Long Heritage",
|
||||||
|
address: "42 Lê Lợi, Quận 1, TP. Hồ Chí Minh",
|
||||||
|
image: "https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=600&h=400&fit=crop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "Katinat Saigon Kafe",
|
||||||
|
address: "26 Lý Tự Trọng, Quận 1, TP. Hồ Chí Minh",
|
||||||
|
image: "https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=600&h=400&fit=crop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "Trung Nguyên E-Coffee",
|
||||||
|
address: "15 Hai Bà Trưng, Quận 1, TP. Hồ Chí Minh",
|
||||||
|
image: "https://images.unsplash.com/photo-1453614512568-c4024d13c247?w=600&h=400&fit=crop",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// ===== MOCK USERS (for UI demo – replace with real auth) =====
|
// ===== MOCK USERS (for UI demo – replace with real auth) =====
|
||||||
export const MOCK_USERS: Record<string, User> = {
|
export const MOCK_USERS: Record<string, User> = {
|
||||||
manager: {
|
manager: {
|
||||||
|
|||||||
@@ -51,3 +51,11 @@ export interface SocialLinks {
|
|||||||
tiktok: string;
|
tiktok: string;
|
||||||
website: string;
|
website: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== SHOP (QUÁN NƯỚC) TYPES =====
|
||||||
|
export interface Shop {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|||||||
+10
-1
@@ -9,6 +9,15 @@ import type { NextConfig } from "next";
|
|||||||
*
|
*
|
||||||
* Docs: https://nextjs.org/docs/app/api-reference/next-config-js
|
* Docs: https://nextjs.org/docs/app/api-reference/next-config-js
|
||||||
*/
|
*/
|
||||||
const nextConfig: NextConfig = {};
|
const nextConfig: NextConfig = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "images.unsplash.com",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Generated
+740
-2791
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -10,17 +10,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "16.1.7",
|
"next": "16.1.7",
|
||||||
|
"php": "^1.1.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
"tailwind": "^4.0.0"
|
"tailwind": "^2.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^10.1.0",
|
||||||
"eslint-config-next": "16.1.7",
|
"eslint-config-next": "^0.2.4",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user