тут нихуя не работает, ебаный рот этого казино
This commit is contained in:
1 parent
847102e843
commit
8c4c3f2e38
56 files changed
+1448
-267
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.
File renamed without changes.
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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.
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
<ProductFilters />
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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">Результаты поиска для "{searchTerm}"</h1>
|
||||
{filteredProducts.length > 0 ? (
|
||||
<ProductGrid products={filteredProducts} />
|
||||
) : (
|
||||
<p className="text-center text-gray-500">По вашему запросу ничего не найдено</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
Подробнее
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,33 +1,89 @@
|
||||
"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>
|
||||
<CatalogMenu />
|
||||
<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">
|
||||
<Button variant="ghost" size="icon">
|
||||
<Package2 className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Heart className="h-5 w-5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ShoppingCart className="h-5 w-5" />
|
||||
<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" className="relative" asChild>
|
||||
<Link href="/favorites">
|
||||
<Heart 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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
<span className="text-xl font-bold">{product.price} ₽</span>
|
||||
<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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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, '[]')
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Generated
+323
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface Product {
|
||||
id: number
|
||||
title: string
|
||||
price: number
|
||||
image: string
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user