тут нихуя не работает, ебаный рот этого казино

This commit is contained in:
User committed 2025-01-08 15:14:15 +03:00
1 parent 847102e843
commit 8c4c3f2e38
56 files changed
+1439 -258

No files matched your search

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+19
View File
@@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from 'next/server'
import { getCart, saveCart, clearCart, CartItem } from '@/lib/cartStorage'
export async function GET() {
const cart = getCart()
return NextResponse.json(cart)
}
export async function POST(request: NextRequest) {
const cartItems: CartItem[] = await request.json()
saveCart(cartItems)
return NextResponse.json({ message: 'Cart updated successfully' })
}
export async function DELETE() {
clearCart()
return NextResponse.json({ message: 'Cart cleared successfully' })
}
@@ -0,0 +1,12 @@
import { NextRequest, NextResponse } from 'next/server'
import { draftMode } from 'next/headers'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
draftMode().disable()
return NextResponse.redirect(new URL(`/product/${id}`, request.url))
}
+23
View File
@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const secret = searchParams.get('secret')
const id = searchParams.get('id')
// Check the secret and next parameters
// This secret should be only known to this API route and the CMS
if (secret !== process.env.PREVIEW_SECRET || !id) {
return NextResponse.json({ message: 'Invalid token' }, { status: 401 })
}
// Enable Preview Mode by setting the cookies
const res = NextResponse.next()
res.cookies.set('__prerender_bypass', process.env.PRERENDER_BYPASS_TOKEN || '')
res.cookies.set('__next_preview_data', process.env.PREVIEW_DATA_TOKEN || '')
// Redirect to the path from the fetched post
// We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
return NextResponse.redirect(new URL(`/product/${id}`, request.url))
}
+3 -8
View File
@@ -4,17 +4,12 @@ import { CartSummary } from "@/components/cart-summary"
export default function CartPage() {
return (
<div className="container mx-auto px-4 py-8">
<div className="flex gap-8">
<h1 className="text-2xl font-bold mb-6">Корзина</h1>
<div className="flex flex-col md:flex-row gap-8">
<div className="flex-1">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold">Корзина</h1>
<button className="text-blue-600 hover:underline">
Поделиться
</button>
</div>
<CartItems />
</div>
<div className="w-80">
<div className="w-full md:w-80">
<CartSummary />
</div>
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+10
View File
@@ -0,0 +1,10 @@
import { FavoriteItems } from "@/components/favorite-items"
export default function FavoritesPage() {
return (
<div className="container mx-auto px-4 py-8">
<FavoriteItems />
</div>
)
}
Binary file not shown.
Binary file not shown.
-78
View File
@@ -1,78 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: Arial, Helvetica, sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
+24 -24
View File
@@ -1,35 +1,35 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import type { Metadata } from "next"
import { Inter } from 'next/font/google'
import { Header } from "@/components/header"
import { CartProvider } from "@/contexts/cart-context"
import { FavoritesProvider } from "@/contexts/favorites-context"
import { AuthProvider } from "@/contexts/auth-context"
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
title: "Online Store",
description: "E-commerce platform",
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<body className={inter.className}>
<AuthProvider>
<CartProvider>
<FavoritesProvider>
<Header />
<main className="min-h-screen bg-gray-50 px-4 sm:px-6 lg:px-8">{children}</main>
</FavoritesProvider>
</CartProvider>
</AuthProvider>
</body>
</html>
);
)
}
+52
View File
@@ -0,0 +1,52 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useAuth } from "@/contexts/auth-context"
export default function LoginPage() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const router = useRouter()
const { login } = useAuth()
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// In a real application, you would validate credentials here
login()
router.push("/cart")
}
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-2xl font-bold mb-6">Войти</h1>
<form onSubmit={handleSubmit} className="space-y-4 max-w-md mx-auto">
<div>
<Label htmlFor="email">Email</Label>
<Input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="password">Пароль</Label>
<Input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<Button type="submit" className="w-full">Войти</Button>
</form>
</div>
)
}
+6 -8
View File
@@ -1,23 +1,21 @@
import { ProductGrid } from "@/components/product-grid"
import { Header } from "@/components/header"
import { Banner } from "@/components/banner"
import { ProductFilters } from "@/components/product-filters"
import { Footer } from "@/components/footer"
import { SAMPLE_PRODUCTS } from "@/lib/sample-products"
export default function HomePage() {
return (
<>
<div className="container mx-auto px-4 py-8">
<Header/>
<div className="container mx-auto py-8">
<Banner />
<div className="my-8 flex gap-8">
<div className="my-8 flex flex-col md:flex-row gap-8">
<div className="w-full md:w-64">
<ProductFilters />
</div>
<div className="flex-1">
<h2 className="text-2xl font-bold mb-6">Специальные предложения!</h2>
<ProductGrid />
<ProductGrid products={SAMPLE_PRODUCTS} />
</div>
</div>
</div>
+50
View File
@@ -0,0 +1,50 @@
import { notFound } from 'next/navigation'
import { ProductDetail } from "@/components/product-detail"
import { SAMPLE_PRODUCTS } from "@/lib/sample-products"
import { draftMode } from 'next/headers'
async function getProduct(id: string, isDraft: boolean) {
// In a real application, you would fetch the product from an API or database
// For this example, we'll use the SAMPLE_PRODUCTS
const product = SAMPLE_PRODUCTS.find(p => p.id === parseInt(id))
if (!product) {
return null
}
if (isDraft) {
// Simulate draft data
return {
...product,
title: `[Draft] ${product.title}`,
price: product.price * 0.9, // 10% discount in draft mode
}
}
return product
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const draftModeData = draftMode()
const isDraftMode = draftModeData ? draftModeData.isEnabled : false
const product = await getProduct(params.id, isDraftMode)
if (!product) {
notFound()
}
return (
<div className="container mx-auto px-4 py-8">
<ProductDetail product={product} />
{isDraftMode && (
<div className="fixed bottom-0 left-0 w-full bg-yellow-400 text-black p-2 text-center">
Preview Mode Enabled - {' '}
<a className="underline" href={`/api/disable-preview?id=${params.id}`}>
Exit Preview Mode
</a>
</div>
)}
</div>
)
}
+11
View File
@@ -0,0 +1,11 @@
import { UserProfile } from "@/components/user-profile"
export default function ProfilePage() {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-2xl font-bold mb-6">Личный кабинет</h1>
<UserProfile />
</div>
)
}
+11
View File
@@ -0,0 +1,11 @@
import { RegisterForm } from "@/components/register-form"
export default function RegisterPage() {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-2xl font-bold mb-6">Регистрация</h1>
<RegisterForm />
</div>
)
}
+25
View File
@@ -0,0 +1,25 @@
import { ProductGrid } from "@/components/product-grid"
import { SAMPLE_PRODUCTS } from "@/lib/sample-products"
export default function SearchPage({
searchParams
}: {
searchParams: { q: string }
}) {
const searchTerm = searchParams.q || ''
const filteredProducts = SAMPLE_PRODUCTS.filter(product =>
product.title.toLowerCase().includes(searchTerm.toLowerCase())
)
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-2xl font-bold mb-6">Результаты поиска для &quot;{searchTerm}&quot;</h1>
{filteredProducts.length > 0 ? (
<ProductGrid products={filteredProducts} />
) : (
<p className="text-center text-gray-500">По вашему запросу ничего не найдено</p>
)}
</div>
)
}
+1 -1
View File
@@ -15,7 +15,7 @@ export function Banner() {
<div className="max-w-lg text-white">
<h1 className="text-4xl font-bold mb-4">САМОЕ ЗАВЕТНОЕ!</h1>
<p className="text-xl mb-6">
Исполняйте мечты с Eternos
Исполняйте мечты с Ozon Рассрочкой
</p>
<Button variant="secondary" size="lg">
Подробнее
+31 -32
View File
@@ -5,25 +5,12 @@ import { Checkbox } from "./ui/checkbox"
import { Button } from "./ui/button"
import { Minus, Plus, Heart, Trash } from 'lucide-react'
import Image from "next/image"
const SAMPLE_ITEMS = [
{
id: 1,
name: "Бумага офисная SvetoCopy, 500 листов, А4",
price: 352,
oldPrice: 399,
image: "/placeholder.svg",
},
{
id: 2,
name: "Глюкометр Диаконт Концепт + тест-полоски",
price: 1450,
oldPrice: 1599,
image: "/placeholder.svg",
}
]
import { useCart } from "@/contexts/cart-context"
import { useFavorites } from "@/contexts/favorites-context"
export function CartItems() {
const { items, removeFromCart, addToCart, removeAllFromCart, updateQuantity } = useCart()
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites()
const [selectedItems, setSelectedItems] = useState<number[]>([])
const toggleItem = (id: number) => {
@@ -34,13 +21,25 @@ export function CartItems() {
)
}
const handleToggleFavorite = (item: typeof items[0]) => {
if (isFavorite(item.id)) {
removeFromFavorites(item.id)
} else {
addToFavorites(item)
}
}
const handleUpdateQuantity = (id: number, newQuantity: number) => {
updateQuantity(id, newQuantity)
}
return (
<div className="space-y-4">
<div className="flex items-center gap-2 mb-4">
<Checkbox
checked={selectedItems.length === SAMPLE_ITEMS.length}
checked={selectedItems.length === items.length}
onCheckedChange={(checked) => {
setSelectedItems(checked ? SAMPLE_ITEMS.map(item => item.id) : [])
setSelectedItems(checked ? items.map(item => item.id) : [])
}}
/>
<span>Выбрать все</span>
@@ -50,43 +49,43 @@ export function CartItems() {
</button>
)}
</div>
{SAMPLE_ITEMS.map((item) => (
{items.map((item) => (
<div key={item.id} className="flex gap-4 p-4 bg-white rounded-lg">
<Checkbox
checked={selectedItems.includes(item.id)}
onCheckedChange={() => toggleItem(item.id)}
/>
<Image
src={item.image}
alt={item.name}
src={item.image || "/placeholder.svg"}
alt={item.title}
width={100}
height={100}
className="object-cover"
/>
<div className="flex-1">
<h3 className="font-medium">{item.name}</h3>
<h3 className="font-medium">{item.title}</h3>
<div className="flex gap-4 mt-4">
<div className="flex items-center gap-2">
<Button variant="outline" size="icon">
<Button variant="outline" size="icon" onClick={() => handleUpdateQuantity(item.id, item.quantity - 1)}>
<Minus className="h-4 w-4" />
</Button>
<span className="w-8 text-center">1</span>
<Button variant="outline" size="icon">
<span className="w-8 text-center">{item.quantity}</span>
<Button variant="outline" size="icon" onClick={() => handleUpdateQuantity(item.id, item.quantity + 1)}>
<Plus className="h-4 w-4" />
</Button>
</div>
<Button variant="ghost" size="icon">
<Heart className="h-4 w-4" />
<Button variant="ghost" size="icon" onClick={() => handleToggleFavorite(item)}>
<Heart className={`h-4 w-4 ${isFavorite(item.id) ? 'fill-red-500 text-red-500' : ''}`} />
</Button>
<Button variant="ghost" size="icon">
<Button variant="ghost" size="icon" onClick={() => removeAllFromCart(item.id)}>
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
<div className="text-right">
<div className="text-lg font-bold">{item.price} ₽</div>
<div className="text-sm text-muted-foreground line-through">
{item.oldPrice} ₽
<div className="text-lg font-bold">{item.price * item.quantity} ₽</div>
<div className="text-sm text-muted-foreground">
{item.price} ₽ за шт.
</div>
</div>
</div>
+34 -10
View File
@@ -1,25 +1,49 @@
"use client"
import { useCart } from "@/contexts/cart-context"
import { useAuth } from "@/contexts/auth-context"
import { Button } from "./ui/button"
import { useRouter } from "next/navigation"
export function CartSummary() {
const { items, getTotalItems } = useCart()
const { isLoggedIn } = useAuth()
const router = useRouter()
const totalPrice = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
const handleCheckout = () => {
if (!isLoggedIn) {
router.push('/login')
} else {
router.push('/checkout')
}
}
if (getTotalItems() === 0) {
return (
<div className="bg-white p-4 rounded-lg">
<h2 className="text-lg font-semibold mb-4">Ваша корзина</h2>
<p className="text-gray-500">В вашей корзине пока нет товаров</p>
</div>
)
}
return (
<div className="bg-white p-4 rounded-lg">
<h2 className="text-lg font-semibold mb-4">Ваша корзина</h2>
<div className="space-y-2 mb-4">
<div className="flex justify-between">
<span>Товары (2)</span>
<span>10 236 ₽</span>
</div>
<div className="flex justify-between text-green-600">
<span>Скидка</span>
<span>- 4 387 ₽</span>
<span>Товары ({getTotalItems()})</span>
<span>{totalPrice} ₽</span>
</div>
</div>
<div className="flex justify-between text-lg font-bold mb-4">
<span>С Ozon Картой</span>
<span>5 467 ₽</span>
<span>Итого</span>
<span>{totalPrice} ₽</span>
</div>
<Button className="w-full" size="lg">
Перейти к оформлению
<Button className="w-full" size="lg" onClick={handleCheckout}>
{isLoggedIn ? "Перейти к оформлению" : "Войти для оформления"}
</Button>
</div>
)
@@ -0,0 +1,50 @@
"use client"
import { useFavorites } from "@/contexts/favorites-context"
import { useCart } from "@/contexts/cart-context"
import { Button } from "./ui/button"
import { ShoppingCart, Trash } from 'lucide-react'
import Image from "next/image"
export function FavoriteItems() {
const { items, removeFromFavorites } = useFavorites()
const { addToCart } = useCart()
if (items.length === 0) {
return (
<div className="text-center py-12">
<h2 className="text-2xl font-semibold mb-4">Избранное</h2>
<p className="text-gray-500">У вас пока нет избранных товаров</p>
</div>
)
}
return (
<div className="space-y-4">
{items.map((item) => (
<div key={item.id} className="flex gap-4 p-4 bg-white rounded-lg">
<Image
src={item.image || "/placeholder.svg"}
alt={item.title}
width={100}
height={100}
className="object-cover"
/>
<div className="flex-1">
<h3 className="font-medium">{item.title}</h3>
<div className="text-lg font-bold mt-2">{item.price} ₽</div>
<div className="flex gap-4 mt-4">
<Button onClick={() => addToCart(item)}>
<ShoppingCart className="mr-2 h-4 w-4" /> В корзину
</Button>
<Button variant="outline" size="icon" onClick={() => removeFromFavorites(item.id)}>
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
</div>
))}
</div>
)
}
@@ -0,0 +1,59 @@
"use client"
import React, { createContext, useContext, useState, useCallback } from 'react'
type FavoriteItem = {
id: number
title: string
price: number
}
type FavoritesContextType = {
items: FavoriteItem[]
addToFavorites: (item: FavoriteItem) => void
removeFromFavorites: (id: number) => void
isFavorite: (id: number) => boolean
getTotalFavorites: () => number
}
const FavoritesContext = createContext<FavoritesContextType | undefined>(undefined)
export const useFavorites = () => {
const context = useContext(FavoritesContext)
if (!context) {
throw new Error('useFavorites must be used within a FavoritesProvider')
}
return context
}
export const FavoritesProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [items, setItems] = useState<FavoriteItem[]>([])
const addToFavorites = useCallback((newItem: FavoriteItem) => {
setItems(currentItems => {
if (!currentItems.some(item => item.id === newItem.id)) {
return [...currentItems, newItem]
}
return currentItems
})
}, [])
const removeFromFavorites = useCallback((id: number) => {
setItems(currentItems => currentItems.filter(item => item.id !== id))
}, [])
const isFavorite = useCallback((id: number) => {
return items.some(item => item.id === id)
}, [items])
const getTotalFavorites = useCallback(() => {
return items.length
}, [items])
return (
<FavoritesContext.Provider value={{ items, addToFavorites, removeFromFavorites, isFavorite, getTotalFavorites }}>
{children}
</FavoritesContext.Provider>
)
}
+1 -1
View File
@@ -5,7 +5,7 @@ export function Footer() {
return (
<footer className="bg-white border-t mt-16">
<div className="container mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
<div>
<h3 className="font-semibold mb-4">О компании</h3>
<ul className="space-y-2">
+63 -7
View File
@@ -1,32 +1,88 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import { Search } from "./search"
import { Button } from "./ui/button"
import { ShoppingCart, Heart, Package2 } from 'lucide-react'
import { ShoppingCart, Heart, Package2, User, Menu } from 'lucide-react'
import { CatalogMenu } from "./catalog-menu"
import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet"
import { useCart } from "@/contexts/cart-context"
import { useFavorites } from "@/contexts/favorites-context"
import { Badge } from "./ui/badge"
export function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const { getTotalItems } = useCart()
const { getTotalFavorites } = useFavorites()
return (
<header className="border-b sticky top-0 bg-white z-50">
<div className="container mx-auto px-4">
<div className="flex h-16 items-center justify-between">
<div className="flex items-center gap-8">
<div className="flex items-center gap-4">
<Link href="/" className="text-2xl font-bold text-blue-600">
Eternos
STORE
</Link>
<div className="hidden md:block">
<CatalogMenu />
</div>
</div>
<div className="hidden md:block flex-1 max-w-xl mx-4">
<Search />
</div>
<div className="flex items-center gap-4">
<div className="hidden md:flex items-center gap-4">
<Button variant="ghost" size="icon" asChild>
<Link href="/profile">
<User className="h-5 w-5" />
</Link>
</Button>
<Button variant="ghost" size="icon">
<Package2 className="h-5 w-5" />
</Button>
<Button variant="ghost" size="icon">
<Button variant="ghost" size="icon" className="relative" asChild>
<Link href="/favorites">
<Heart className="h-5 w-5" />
</Button>
<Button variant="ghost" size="icon">
<ShoppingCart className="h-5 w-5" />
{getTotalFavorites() > 0 && (
<Badge variant="destructive" className="absolute -top-2 -right-2 h-5 w-5 flex items-center justify-center p-0">
{getTotalFavorites()}
</Badge>
)}
</Link>
</Button>
</div>
<Button variant="ghost" size="icon" asChild className="relative">
<Link href="/cart">
<ShoppingCart className="h-5 w-5" />
{getTotalItems() > 0 && (
<Badge variant="destructive" className="absolute -top-2 -right-2 h-5 w-5 flex items-center justify-center p-0">
{getTotalItems()}
</Badge>
)}
</Link>
</Button>
<Sheet open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden">
<Menu className="h-5 w-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[300px] sm:w-[400px]">
<nav className="flex flex-col gap-4">
<Link href="/" onClick={() => setIsMenuOpen(false)}>Главная</Link>
<Link href="/profile" onClick={() => setIsMenuOpen(false)}>Профиль</Link>
<Link href="/cart" onClick={() => setIsMenuOpen(false)}>Корзина</Link>
<div className="md:hidden">
<CatalogMenu />
</div>
</nav>
</SheetContent>
</Sheet>
</div>
</div>
<div className="md:hidden mt-2 mb-4">
<Search />
</div>
</div>
</header>
+47 -24
View File
@@ -1,24 +1,54 @@
"use client"
import Image from "next/image"
import { Heart } from 'lucide-react'
import Link from "next/link"
import { Heart, ShoppingCart } from 'lucide-react'
import { Button } from "./ui/button"
import { Badge } from "./ui/badge"
import { useCart } from "@/contexts/cart-context"
import { useFavorites } from "@/contexts/favorites-context"
interface ProductCardProps {
product: {
id: number
title: string
price: number
oldPrice: number
discount: number
image: string
isHotDeal?: boolean
isSale?: boolean
}
}
export function ProductCard({ product }: ProductCardProps) {
const { addToCart, removeFromCart } = useCart()
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites()
const handleAddToCart = (e: React.MouseEvent) => {
e.preventDefault()
addToCart({
id: product.id,
title: product.title,
price: product.price,
})
}
const handleRemoveFromCart = (e: React.MouseEvent) => {
e.preventDefault()
removeFromCart(product.id)
}
const handleToggleFavorite = (e: React.MouseEvent) => {
e.preventDefault()
if (isFavorite(product.id)) {
removeFromFavorites(product.id)
} else {
addToFavorites({
id: product.id,
title: product.title,
price: product.price,
})
}
}
return (
<div className="group relative bg-white rounded-lg p-2 transition-shadow hover:shadow-lg">
<Link href={`/product/${product.id}`} className="group relative bg-white rounded-lg p-2 transition-shadow hover:shadow-lg block">
<div className="relative aspect-square mb-2">
<Image
src={product.image}
@@ -30,31 +60,24 @@ export function ProductCard({ product }: ProductCardProps) {
variant="ghost"
size="icon"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={handleToggleFavorite}
>
<Heart className="h-5 w-5" />
<Heart className={`h-5 w-5 ${isFavorite(product.id) ? 'fill-red-500 text-red-500' : ''}`} />
</Button>
{product.isHotDeal && (
<div className="absolute top-2 left-2 bg-pink-500 text-white px-2 py-1 text-xs font-medium rounded">
НАРАСХВАТ
</div>
)}
</div>
<div className="space-y-2">
<div className="flex items-baseline gap-2">
<span className="text-xl font-bold">{product.price} ₽</span>
<span className="text-sm text-muted-foreground line-through">
{product.oldPrice} ₽
</span>
<span className="text-sm text-red-500">-{product.discount}%</span>
</div>
<h3 className="text-sm line-clamp-2">{product.title}</h3>
{product.isSale && (
<Badge variant="destructive" className="mt-2">
Распродажа
</Badge>
)}
<div className="flex gap-2">
<Button onClick={handleAddToCart} className="flex-1">
<ShoppingCart className="mr-2 h-4 w-4" /> В корзину
</Button>
<Button onClick={handleRemoveFromCart} variant="outline" size="icon">
-
</Button>
</div>
</div>
</Link>
)
}
@@ -0,0 +1,97 @@
"use client"
import Image from "next/image"
import { useState } from "react"
import { Heart, ShoppingCart, Minus, Plus } from 'lucide-react'
import { Button } from "./ui/button"
import { useCart } from "@/contexts/cart-context"
import { useFavorites } from "@/contexts/favorites-context"
import { Product } from "@/types/product"
interface ProductDetailProps {
product: Product
}
export function ProductDetail({ product }: ProductDetailProps) {
const [quantity, setQuantity] = useState(1)
const { addToCart } = useCart()
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites()
const handleAddToCart = () => {
addToCart({
id: product.id,
title: product.title,
price: product.price,
quantity: quantity
})
}
const handleToggleFavorite = () => {
if (isFavorite(product.id)) {
removeFromFavorites(product.id)
} else {
addToFavorites({
id: product.id,
title: product.title,
price: product.price,
})
}
}
return (
<div className="flex flex-col md:flex-row gap-8">
<div className="md:w-1/2">
<Image
src={product.image}
alt={product.title}
width={500}
height={500}
className="w-full h-auto object-cover rounded-lg"
/>
</div>
<div className="md:w-1/2 space-y-4">
<h1 className="text-3xl font-bold">{product.title}</h1>
{product.title.startsWith('[Draft]') && (
<div className="bg-yellow-200 text-yellow-800 px-2 py-1 rounded inline-block">
Draft Version
</div>
)}
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold">{product.price} ₽</span>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
onClick={() => setQuantity(prev => Math.max(1, prev - 1))}
>
<Minus className="h-4 w-4" />
</Button>
<span className="w-8 text-center">{quantity}</span>
<Button
variant="outline"
size="icon"
onClick={() => setQuantity(prev => prev + 1)}
>
<Plus className="h-4 w-4" />
</Button>
</div>
<Button onClick={handleAddToCart} className="flex-1">
<ShoppingCart className="mr-2 h-4 w-4" /> Добавить в корзину
</Button>
<Button variant="outline" size="icon" onClick={handleToggleFavorite}>
<Heart className={`h-5 w-5 ${isFavorite(product.id) ? 'fill-red-500 text-red-500' : ''}`} />
</Button>
</div>
<div className="border-t pt-4">
<h2 className="text-xl font-semibold mb-2">Описание</h2>
<p className="text-gray-600">
Подробное описание товара. Здесь может быть длинный текст с характеристиками и особенностями продукта.
</p>
</div>
</div>
</div>
)
}
+7 -60
View File
@@ -1,67 +1,14 @@
import { ProductCard } from "./product-card"
import { Product } from "@/types/product"
const SAMPLE_PRODUCTS = [
{
id: 1,
title: "Кофе растворимый Жокей Крепкий, 3 в 1, с сахаром",
price: 89,
oldPrice: 172,
discount: 48,
image: "/placeholder.svg",
isHotDeal: true,
isSale: true,
},
{
id: 2,
title: "Рексона Дезодорант женский твердый стик",
price: 235,
oldPrice: 397,
discount: 40,
image: "/placeholder.svg",
isHotDeal: true,
},
{
id: 3,
title: "Сухой корм Мираторг MEAT с нежной телятиной",
price: 187,
oldPrice: 294,
discount: 36,
image: "/placeholder.svg",
isHotDeal: true,
},
{
id: 4,
title: "Сухой корм KITEKAT™ для взрослых кошек «Мясной пир»",
price: 174,
oldPrice: 209,
discount: 16,
image: "/placeholder.svg",
isHotDeal: true,
},
{
id: 5,
title: "Специальное чистящее средство для стиральных машин",
price: 197,
oldPrice: 469,
discount: 57,
image: "/placeholder.svg",
isHotDeal: true,
},
{
id: 6,
title: "Крем для лица увлажняющий",
price: 184,
oldPrice: 413,
discount: 55,
image: "/placeholder.svg",
isHotDeal: true,
},
]
interface ProductGridProps {
products: Product[]
}
export function ProductGrid() {
export function ProductGrid({ products }: ProductGridProps) {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
{SAMPLE_PRODUCTS.map((product) => (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
@@ -0,0 +1,71 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import Link from "next/link"
export function RegisterForm() {
const [formData, setFormData] = useState({
email: "",
password: "",
confirmPassword: "",
})
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
// Here you would typically send the data to your backend
console.log("Form submitted:", formData)
}
return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-md mx-auto">
<div>
<Label htmlFor="email">Email</Label>
<Input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</div>
<div>
<Label htmlFor="password">Пароль</Label>
<Input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
required
/>
</div>
<div>
<Label htmlFor="confirmPassword">Подтвердите пароль</Label>
<Input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
required
/>
</div>
<Button type="submit" className="w-full">Зарегистрироваться</Button>
<div className="text-center">
<Link href="/recover-password" className="text-blue-600 hover:underline">
Забыли пароль?
</Link>
</div>
</form>
)
}
+23 -4
View File
@@ -1,17 +1,36 @@
"use client"
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { SearchIcon } from 'lucide-react'
import { Input } from "./ui/input"
import { Button } from "./ui/button"
export function Search() {
const [searchTerm, setSearchTerm] = useState('')
const router = useRouter()
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
if (searchTerm.trim()) {
router.push(`/search?q=${encodeURIComponent(searchTerm.trim())}`)
}
}
return (
<div className="relative w-full max-w-lg">
<SearchIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<form onSubmit={handleSearch} className="relative w-full max-w-lg">
<Input
type="search"
placeholder="Искать на Store"
className="pl-8 w-full"
className="pl-10 pr-20 w-full"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Button type="submit" className="absolute right-1 top-1/2 transform -translate-y-1/2">
Найти
</Button>
</form>
)
}
+26
View File
@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
+7
View File
@@ -0,0 +1,7 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
@@ -0,0 +1,32 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import Link from "next/link"
export function UserProfile() {
const [isLoggedIn, setIsLoggedIn] = useState(false)
if (!isLoggedIn) {
return (
<div className="space-y-4 max-w-md mx-auto">
<Button asChild className="w-full">
<Link href="/login">Войти</Link>
</Button>
<Button asChild variant="outline" className="w-full">
<Link href="/register">Зарегистрироваться</Link>
</Button>
</div>
)
}
return (
<div className="space-y-4 max-w-md mx-auto">
<h2 className="text-xl font-semibold">Добро пожаловать, Иван Иванов!</h2>
<Button onClick={() => setIsLoggedIn(false)} className="w-full">Выйти</Button>
</div>
)
}
+33
View File
@@ -0,0 +1,33 @@
"use client"
import React, { createContext, useContext, useState } from 'react'
type AuthContextType = {
isLoggedIn: boolean
login: () => void
logout: () => void
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export const useAuth = () => {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false)
const login = () => setIsLoggedIn(true)
const logout = () => setIsLoggedIn(false)
return (
<AuthContext.Provider value={{ isLoggedIn, login, logout }}>
{children}
</AuthContext.Provider>
)
}
+99
View File
@@ -0,0 +1,99 @@
"use client"
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'
import { CartItem, saveCart, getCart, clearCart } from '@/lib/cartStorage'
type CartContextType = {
items: CartItem[]
addToCart: (item: Omit<CartItem, 'quantity'>) => void
removeFromCart: (id: number) => void
removeAllFromCart: (id: number) => void
updateQuantity: (id: number, quantity: number) => void
clearCart: () => void
getTotalItems: () => number
}
const CartContext = createContext<CartContextType | undefined>(undefined)
export const useCart = () => {
const context = useContext(CartContext)
if (!context) {
throw new Error('useCart must be used within a CartProvider')
}
return context
}
export const CartProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [items, setItems] = useState<CartItem[]>([])
useEffect(() => {
const savedCart = getCart()
if (savedCart.length > 0) {
setItems(savedCart)
}
}, [])
useEffect(() => {
saveCart(items)
}, [items])
const addToCart = useCallback((newItem: Omit<CartItem, 'quantity'>) => {
setItems(currentItems => {
const existingItem = currentItems.find(item => item.id === newItem.id)
if (existingItem) {
return currentItems.map(item =>
item.id === newItem.id ? { ...item, quantity: item.quantity + 1 } : item
)
}
return [...currentItems, { ...newItem, quantity: 1 }]
})
}, [])
const removeFromCart = useCallback((id: number) => {
setItems(currentItems => {
const existingItem = currentItems.find(item => item.id === id)
if (existingItem && existingItem.quantity > 1) {
return currentItems.map(item =>
item.id === id ? { ...item, quantity: item.quantity - 1 } : item
)
}
return currentItems.filter(item => item.id !== id)
})
}, [])
const removeAllFromCart = useCallback((id: number) => {
setItems(currentItems => currentItems.filter(item => item.id !== id))
}, [])
const updateQuantity = useCallback((id: number, quantity: number) => {
setItems(currentItems =>
currentItems.map(item =>
item.id === id ? { ...item, quantity: Math.max(1, quantity) } : item
)
)
}, [])
const clearCartItems = useCallback(() => {
setItems([])
clearCart()
}, [])
const getTotalItems = useCallback(() => {
return items.reduce((total, item) => total + item.quantity, 0)
}, [items])
return (
<CartContext.Provider value={{
items,
addToCart,
removeFromCart,
removeAllFromCart,
updateQuantity,
clearCart: clearCartItems,
getTotalItems
}}>
{children}
</CartContext.Provider>
)
}
@@ -0,0 +1,59 @@
"use client"
import React, { createContext, useContext, useState, useCallback } from 'react'
type FavoriteItem = {
id: number
title: string
price: number
}
type FavoritesContextType = {
items: FavoriteItem[]
addToFavorites: (item: FavoriteItem) => void
removeFromFavorites: (id: number) => void
isFavorite: (id: number) => boolean
getTotalFavorites: () => number
}
const FavoritesContext = createContext<FavoritesContextType | undefined>(undefined)
export const useFavorites = () => {
const context = useContext(FavoritesContext)
if (!context) {
throw new Error('useFavorites must be used within a FavoritesProvider')
}
return context
}
export const FavoritesProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [items, setItems] = useState<FavoriteItem[]>([])
const addToFavorites = useCallback((newItem: FavoriteItem) => {
setItems(currentItems => {
if (!currentItems.some(item => item.id === newItem.id)) {
return [...currentItems, newItem]
}
return currentItems
})
}, [])
const removeFromFavorites = useCallback((id: number) => {
setItems(currentItems => currentItems.filter(item => item.id !== id))
}, [])
const isFavorite = useCallback((id: number) => {
return items.some(item => item.id === id)
}, [items])
const getTotalFavorites = useCallback(() => {
return items.length
}, [items])
return (
<FavoritesContext.Provider value={{ items, addToFavorites, removeFromFavorites, isFavorite, getTotalFavorites }}>
{children}
</FavoritesContext.Provider>
)
}
+39
View File
@@ -0,0 +1,39 @@
import fs from 'fs'
import path from 'path'
const DATA_DIR = path.join(process.cwd(), 'data')
const CART_FILE_PATH = path.join(DATA_DIR, 'cart.json')
export interface CartItem {
id: number
title: string
price: number
quantity: number
}
const ensureDataDir = () => {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true })
}
}
export const saveCart = (cartItems: CartItem[]): void => {
ensureDataDir()
const cartData = JSON.stringify(cartItems, null, 2)
fs.writeFileSync(CART_FILE_PATH, cartData)
}
export const getCart = (): CartItem[] => {
ensureDataDir()
if (!fs.existsSync(CART_FILE_PATH)) {
return []
}
const cartData = fs.readFileSync(CART_FILE_PATH, 'utf-8')
return JSON.parse(cartData)
}
export const clearCart = (): void => {
ensureDataDir()
fs.writeFileSync(CART_FILE_PATH, '[]')
}
+16
View File
@@ -0,0 +1,16 @@
import Cookies from 'js-cookie';
export const updateCartCookie = (cartItems) => {
const cartData = JSON.stringify(cartItems);
Cookies.set('cart', cartData, { expires: 7 }); // Cookie expires in 7 days
};
export const getCartFromCookie = () => {
const cartData = Cookies.get('cart');
return cartData ? JSON.parse(cartData) : [];
};
export const clearCartCookie = () => {
Cookies.remove('cart');
};
+60
View File
@@ -0,0 +1,60 @@
import { Product } from "@/types/product"
export const SAMPLE_PRODUCTS: Product[] = [
{
id: 1,
title: "Кофе растворимый Жокей Крепкий, 3 в 1, с сахаром",
price: 89,
oldPrice: 172,
discount: 48,
image: "/placeholder.svg",
isHotDeal: true,
isSale: true,
},
{
id: 2,
title: "Рексона Дезодорант женский твердый стик",
price: 235,
oldPrice: 397,
discount: 40,
image: "/placeholder.svg",
isHotDeal: true,
},
{
id: 3,
title: "Сухой корм Мираторг MEAT с нежной телятиной",
price: 187,
oldPrice: 294,
discount: 36,
image: "/placeholder.svg",
isHotDeal: true,
},
{
id: 4,
title: "Сухой корм KITEKAT™ для взрослых кошек «Мясной пир»",
price: 174,
oldPrice: 209,
discount: 16,
image: "/placeholder.svg",
isHotDeal: true,
},
{
id: 5,
title: "Специальное чистящее средство для стиральных машин",
price: 197,
oldPrice: 469,
discount: 57,
image: "/placeholder.svg",
isHotDeal: true,
},
{
id: 6,
title: "Крем для лица увлажняющий",
price: 184,
oldPrice: 413,
discount: 55,
image: "/placeholder.svg",
isHotDeal: true,
},
]
+2 -1
View File
@@ -1,6 +1,7 @@
import { clsx, type ClassValue } from "clsx"
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+323
View File
@@ -13,14 +13,20 @@
"@nextui-org/theme": "^2.4.5",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^11.16.0",
"fs": "^0.0.1-security",
"fs-react": "^0.0.4",
"js-cookie": "^3.0.5",
"lucide-react": "^0.469.0",
"next": "14.2.16",
"path": "^0.12.7",
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^2.6.0",
@@ -890,6 +896,42 @@
}
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz",
"integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.3",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.1",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-portal": "1.1.3",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "^2.6.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
@@ -932,6 +974,46 @@
}
}
},
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
"integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz",
"integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
@@ -950,6 +1032,29 @@
}
}
},
"node_modules/@radix-ui/react-label": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz",
"integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-navigation-menu": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.3.tgz",
@@ -986,6 +1091,30 @@
}
}
},
"node_modules/@radix-ui/react-portal": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
"integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
@@ -1871,6 +2000,18 @@
"dev": true,
"license": "Python-2.0"
},
"node_modules/aria-hidden": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
"integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@@ -2535,6 +2676,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -3418,6 +3565,18 @@
}
}
},
"node_modules/fs": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
"integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==",
"license": "ISC"
},
"node_modules/fs-react": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/fs-react/-/fs-react-0.0.4.tgz",
"integrity": "sha512-F7Yy/gDd3ewTusPCnqwT2C/qGsC1aDHq4uekrEjeFQdDfztqenSP1gAVUFpju41Tvnh+D/pTLsNGkdRP5teY7g==",
"license": "ISC"
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -3504,6 +3663,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -4320,6 +4488,15 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4931,6 +5108,16 @@
"node": ">=6"
}
},
"node_modules/path": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
"integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
"license": "MIT",
"dependencies": {
"process": "^0.11.1",
"util": "^0.10.3"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5181,6 +5368,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -5255,6 +5451,75 @@
"dev": true,
"license": "MIT"
},
"node_modules/react-remove-scroll": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz",
"integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
"react-style-singleton": "^2.2.1",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.3",
"use-sidecar": "^1.1.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
"license": "MIT",
"dependencies": {
"react-style-singleton": "^2.2.2",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
"license": "MIT",
"dependencies": {
"get-nonce": "^1.0.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -6320,12 +6585,70 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-callback-ref": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sidecar": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
"license": "MIT",
"dependencies": {
"detect-node-es": "^1.1.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"license": "MIT",
"dependencies": {
"inherits": "2.0.3"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/util/node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
"license": "ISC"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+6
View File
@@ -14,14 +14,20 @@
"@nextui-org/theme": "^2.4.5",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^11.16.0",
"fs": "^0.0.1-security",
"fs-react": "^0.0.4",
"js-cookie": "^3.0.5",
"lucide-react": "^0.469.0",
"next": "14.2.16",
"path": "^0.12.7",
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^2.6.0",
+7
View File
@@ -0,0 +1,7 @@
export interface Product {
id: number
title: string
price: number
image: string
}