тут нихуя не работает, ебаный рот этого казино
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() {
|
export default function CartPage() {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<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-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 />
|
<CartItems />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-80">
|
<div className="w-full md:w-80">
|
||||||
<CartSummary />
|
<CartSummary />
|
||||||
</div>
|
</div>
|
||||||
</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 type { Metadata } from "next"
|
||||||
import localFont from "next/font/local";
|
import { Inter } from 'next/font/google'
|
||||||
import "./globals.css";
|
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({
|
const inter = Inter({ subsets: ["latin"] })
|
||||||
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",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Online Store",
|
||||||
description: "Generated by create next app",
|
description: "E-commerce platform",
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
}>) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body className={inter.className}>
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
<AuthProvider>
|
||||||
>
|
<CartProvider>
|
||||||
{children}
|
<FavoritesProvider>
|
||||||
|
<Header />
|
||||||
|
<main className="min-h-screen bg-gray-50 px-4 sm:px-6 lg:px-8">{children}</main>
|
||||||
|
</FavoritesProvider>
|
||||||
|
</CartProvider>
|
||||||
|
</AuthProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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 { ProductGrid } from "@/components/product-grid"
|
||||||
import { Header } from "@/components/header"
|
|
||||||
|
|
||||||
import { Banner } from "@/components/banner"
|
import { Banner } from "@/components/banner"
|
||||||
import { ProductFilters } from "@/components/product-filters"
|
import { ProductFilters } from "@/components/product-filters"
|
||||||
import { Footer } from "@/components/footer"
|
import { Footer } from "@/components/footer"
|
||||||
|
import { SAMPLE_PRODUCTS } from "@/lib/sample-products"
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div className="container mx-auto py-8">
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<Header/>
|
|
||||||
|
|
||||||
<Banner />
|
<Banner />
|
||||||
<div className="my-8 flex gap-8">
|
<div className="my-8 flex flex-col md:flex-row gap-8">
|
||||||
<ProductFilters />
|
<div className="w-full md:w-64">
|
||||||
|
<ProductFilters />
|
||||||
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-2xl font-bold mb-6">Специальные предложения!</h2>
|
<h2 className="text-2xl font-bold mb-6">Специальные предложения!</h2>
|
||||||
<ProductGrid />
|
<ProductGrid products={SAMPLE_PRODUCTS} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div className="max-w-lg text-white">
|
||||||
<h1 className="text-4xl font-bold mb-4">САМОЕ ЗАВЕТНОЕ!</h1>
|
<h1 className="text-4xl font-bold mb-4">САМОЕ ЗАВЕТНОЕ!</h1>
|
||||||
<p className="text-xl mb-6">
|
<p className="text-xl mb-6">
|
||||||
Исполняйте мечты с Eternos
|
Исполняйте мечты с Ozon Рассрочкой
|
||||||
</p>
|
</p>
|
||||||
<Button variant="secondary" size="lg">
|
<Button variant="secondary" size="lg">
|
||||||
Подробнее
|
Подробнее
|
||||||
|
|||||||
@@ -5,25 +5,12 @@ import { Checkbox } from "./ui/checkbox"
|
|||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
import { Minus, Plus, Heart, Trash } from 'lucide-react'
|
import { Minus, Plus, Heart, Trash } from 'lucide-react'
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
import { useCart } from "@/contexts/cart-context"
|
||||||
const SAMPLE_ITEMS = [
|
import { useFavorites } from "@/contexts/favorites-context"
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "Бумага офисная SvetoCopy, 500 листов, А4",
|
|
||||||
price: 352,
|
|
||||||
oldPrice: 399,
|
|
||||||
image: "/placeholder.svg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Глюкометр Диаконт Концепт + тест-полоски",
|
|
||||||
price: 1450,
|
|
||||||
oldPrice: 1599,
|
|
||||||
image: "/placeholder.svg",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export function CartItems() {
|
export function CartItems() {
|
||||||
|
const { items, removeFromCart, addToCart, removeAllFromCart, updateQuantity } = useCart()
|
||||||
|
const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites()
|
||||||
const [selectedItems, setSelectedItems] = useState<number[]>([])
|
const [selectedItems, setSelectedItems] = useState<number[]>([])
|
||||||
|
|
||||||
const toggleItem = (id: 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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedItems.length === SAMPLE_ITEMS.length}
|
checked={selectedItems.length === items.length}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
setSelectedItems(checked ? SAMPLE_ITEMS.map(item => item.id) : [])
|
setSelectedItems(checked ? items.map(item => item.id) : [])
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span>Выбрать все</span>
|
<span>Выбрать все</span>
|
||||||
@@ -50,43 +49,43 @@ export function CartItems() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{SAMPLE_ITEMS.map((item) => (
|
{items.map((item) => (
|
||||||
<div key={item.id} className="flex gap-4 p-4 bg-white rounded-lg">
|
<div key={item.id} className="flex gap-4 p-4 bg-white rounded-lg">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedItems.includes(item.id)}
|
checked={selectedItems.includes(item.id)}
|
||||||
onCheckedChange={() => toggleItem(item.id)}
|
onCheckedChange={() => toggleItem(item.id)}
|
||||||
/>
|
/>
|
||||||
<Image
|
<Image
|
||||||
src={item.image}
|
src={item.image || "/placeholder.svg"}
|
||||||
alt={item.name}
|
alt={item.title}
|
||||||
width={100}
|
width={100}
|
||||||
height={100}
|
height={100}
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<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 gap-4 mt-4">
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<Minus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<span className="w-8 text-center">1</span>
|
<span className="w-8 text-center">{item.quantity}</span>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon" onClick={() => handleUpdateQuantity(item.id, item.quantity + 1)}>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon" onClick={() => handleToggleFavorite(item)}>
|
||||||
<Heart className="h-4 w-4" />
|
<Heart className={`h-4 w-4 ${isFavorite(item.id) ? 'fill-red-500 text-red-500' : ''}`} />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon" onClick={() => removeAllFromCart(item.id)}>
|
||||||
<Trash className="h-4 w-4" />
|
<Trash className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-lg font-bold">{item.price} ₽</div>
|
<div className="text-lg font-bold">{item.price * item.quantity} ₽</div>
|
||||||
<div className="text-sm text-muted-foreground line-through">
|
<div className="text-sm text-muted-foreground">
|
||||||
{item.oldPrice} ₽
|
{item.price} ₽ за шт.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 { Button } from "./ui/button"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
|
||||||
export function CartSummary() {
|
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 (
|
return (
|
||||||
<div className="bg-white p-4 rounded-lg">
|
<div className="bg-white p-4 rounded-lg">
|
||||||
<h2 className="text-lg font-semibold mb-4">Ваша корзина</h2>
|
<h2 className="text-lg font-semibold mb-4">Ваша корзина</h2>
|
||||||
<div className="space-y-2 mb-4">
|
<div className="space-y-2 mb-4">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span>Товары (2)</span>
|
<span>Товары ({getTotalItems()})</span>
|
||||||
<span>10 236 ₽</span>
|
<span>{totalPrice} ₽</span>
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-green-600">
|
|
||||||
<span>Скидка</span>
|
|
||||||
<span>- 4 387 ₽</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-lg font-bold mb-4">
|
<div className="flex justify-between text-lg font-bold mb-4">
|
||||||
<span>С Ozon Картой</span>
|
<span>Итого</span>
|
||||||
<span>5 467 ₽</span>
|
<span>{totalPrice} ₽</span>
|
||||||
</div>
|
</div>
|
||||||
<Button className="w-full" size="lg">
|
<Button className="w-full" size="lg" onClick={handleCheckout}>
|
||||||
Перейти к оформлению
|
{isLoggedIn ? "Перейти к оформлению" : "Войти для оформления"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<footer className="bg-white border-t mt-16">
|
<footer className="bg-white border-t mt-16">
|
||||||
<div className="container mx-auto px-4 py-12">
|
<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>
|
<div>
|
||||||
<h3 className="font-semibold mb-4">О компании</h3>
|
<h3 className="font-semibold mb-4">О компании</h3>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
|
|||||||
@@ -1,33 +1,89 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Search } from "./search"
|
import { Search } from "./search"
|
||||||
import { Button } from "./ui/button"
|
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 { 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() {
|
export function Header() {
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||||
|
const { getTotalItems } = useCart()
|
||||||
|
const { getTotalFavorites } = useFavorites()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="border-b sticky top-0 bg-white z-50">
|
<header className="border-b sticky top-0 bg-white z-50">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
<div className="flex h-16 items-center justify-between">
|
<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">
|
<Link href="/" className="text-2xl font-bold text-blue-600">
|
||||||
Eternos
|
STORE
|
||||||
</Link>
|
</Link>
|
||||||
<CatalogMenu />
|
<div className="hidden md:block">
|
||||||
|
<CatalogMenu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block flex-1 max-w-xl mx-4">
|
||||||
<Search />
|
<Search />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="ghost" size="icon">
|
<div className="hidden md:flex items-center gap-4">
|
||||||
<Package2 className="h-5 w-5" />
|
<Button variant="ghost" size="icon" asChild>
|
||||||
</Button>
|
<Link href="/profile">
|
||||||
<Button variant="ghost" size="icon">
|
<User className="h-5 w-5" />
|
||||||
<Heart className="h-5 w-5" />
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<ShoppingCart className="h-5 w-5" />
|
<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>
|
</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>
|
</div>
|
||||||
|
<div className="md:hidden mt-2 mb-4">
|
||||||
|
<Search />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,24 +1,54 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import Image from "next/image"
|
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 { Button } from "./ui/button"
|
||||||
import { Badge } from "./ui/badge"
|
import { useCart } from "@/contexts/cart-context"
|
||||||
|
import { useFavorites } from "@/contexts/favorites-context"
|
||||||
|
|
||||||
interface ProductCardProps {
|
interface ProductCardProps {
|
||||||
product: {
|
product: {
|
||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
price: number
|
price: number
|
||||||
oldPrice: number
|
|
||||||
discount: number
|
|
||||||
image: string
|
image: string
|
||||||
isHotDeal?: boolean
|
|
||||||
isSale?: boolean
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProductCard({ product }: ProductCardProps) {
|
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 (
|
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">
|
<div className="relative aspect-square mb-2">
|
||||||
<Image
|
<Image
|
||||||
src={product.image}
|
src={product.image}
|
||||||
@@ -30,31 +60,24 @@ export function ProductCard({ product }: ProductCardProps) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
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>
|
</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>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-baseline gap-2">
|
<span className="text-xl font-bold">{product.price} ₽</span>
|
||||||
<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>
|
<h3 className="text-sm line-clamp-2">{product.title}</h3>
|
||||||
{product.isSale && (
|
<div className="flex gap-2">
|
||||||
<Badge variant="destructive" className="mt-2">
|
<Button onClick={handleAddToCart} className="flex-1">
|
||||||
Распродажа
|
<ShoppingCart className="mr-2 h-4 w-4" /> В корзину
|
||||||
</Badge>
|
</Button>
|
||||||
)}
|
<Button onClick={handleRemoveFromCart} variant="outline" size="icon">
|
||||||
|
-
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</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 { ProductCard } from "./product-card"
|
||||||
|
import { Product } from "@/types/product"
|
||||||
|
|
||||||
const SAMPLE_PRODUCTS = [
|
interface ProductGridProps {
|
||||||
{
|
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,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function ProductGrid() {
|
export function ProductGrid({ products }: ProductGridProps) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||||
{SAMPLE_PRODUCTS.map((product) => (
|
{products.map((product) => (
|
||||||
<ProductCard key={product.id} product={product} />
|
<ProductCard key={product.id} product={product} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</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"
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
import { SearchIcon } from 'lucide-react'
|
import { SearchIcon } from 'lucide-react'
|
||||||
import { Input } from "./ui/input"
|
import { Input } from "./ui/input"
|
||||||
|
import { Button } from "./ui/button"
|
||||||
|
|
||||||
export function Search() {
|
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 (
|
return (
|
||||||
<div className="relative w-full max-w-lg">
|
<form onSubmit={handleSearch} className="relative w-full max-w-lg">
|
||||||
<SearchIcon className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
<Input
|
||||||
|
type="search"
|
||||||
placeholder="Искать на Store"
|
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"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
Generated
+323
@@ -13,14 +13,20 @@
|
|||||||
"@nextui-org/theme": "^2.4.5",
|
"@nextui-org/theme": "^2.4.5",
|
||||||
"@radix-ui/react-accordion": "^1.2.2",
|
"@radix-ui/react-accordion": "^1.2.2",
|
||||||
"@radix-ui/react-checkbox": "^1.1.3",
|
"@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-navigation-menu": "^1.2.3",
|
||||||
"@radix-ui/react-slider": "^1.2.2",
|
"@radix-ui/react-slider": "^1.2.2",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^11.16.0",
|
"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",
|
"lucide-react": "^0.469.0",
|
||||||
"next": "14.2.16",
|
"next": "14.2.16",
|
||||||
|
"path": "^0.12.7",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"tailwind-merge": "^2.6.0",
|
"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": {
|
"node_modules/@radix-ui/react-direction": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-id": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-navigation-menu": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.3.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-presence": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
|
||||||
@@ -1871,6 +2000,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"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": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||||
@@ -2535,6 +2676,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"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": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@@ -3504,6 +3663,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/get-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
@@ -4320,6 +4488,15 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -4931,6 +5108,16 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@@ -5181,6 +5368,15 @@
|
|||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -5255,6 +5451,75 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -6320,12 +6585,70 @@
|
|||||||
"punycode": "^2.1.0"
|
"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": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -14,14 +14,20 @@
|
|||||||
"@nextui-org/theme": "^2.4.5",
|
"@nextui-org/theme": "^2.4.5",
|
||||||
"@radix-ui/react-accordion": "^1.2.2",
|
"@radix-ui/react-accordion": "^1.2.2",
|
||||||
"@radix-ui/react-checkbox": "^1.1.3",
|
"@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-navigation-menu": "^1.2.3",
|
||||||
"@radix-ui/react-slider": "^1.2.2",
|
"@radix-ui/react-slider": "^1.2.2",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^11.16.0",
|
"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",
|
"lucide-react": "^0.469.0",
|
||||||
"next": "14.2.16",
|
"next": "14.2.16",
|
||||||
|
"path": "^0.12.7",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"tailwind-merge": "^2.6.0",
|
"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