тут нихуя не работает, ебаный рот этого казино
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
50
frontend/style/components/favorite-items.tsx
Normal file
50
frontend/style/components/favorite-items.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
59
frontend/style/components/favorites-context.tsx
Normal file
59
frontend/style/components/favorites-context.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
97
frontend/style/components/product-detail.tsx
Normal file
97
frontend/style/components/product-detail.tsx
Normal file
@@ -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>
|
||||
|
||||
71
frontend/style/components/register-form.tsx
Normal file
71
frontend/style/components/register-form.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
26
frontend/style/components/ui/label.tsx
Normal file
26
frontend/style/components/ui/label.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
7
frontend/style/components/ui/sheet.tsx
Normal file
7
frontend/style/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
32
frontend/style/components/user-profile.tsx
Normal file
32
frontend/style/components/user-profile.tsx
Normal file
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user