Обновить компоненты главной страницы с улучшенным дизайном и интерактивностью

This commit is contained in:
belikovme 2025-02-27 17:27:49 +07:00
parent d770f217b6
commit 1430cbd25b
15 changed files with 1499 additions and 444 deletions

View File

@ -1,63 +1,46 @@
import { motion } from 'framer-motion';
import Image from 'next/image';
import Link from 'next/link';
import Image from "next/image"
import Link from "next/link"
import { motion } from "framer-motion"
import { Collection } from "../data/collections"
// Типы для свойств компонента
interface CollectionsProps {
collections: Collection[];
}
// Тип для коллекции
interface Collection {
id: number;
name: string;
image: string;
description: string;
url: string;
collections: Collection[]
}
export default function Collections({ collections }: CollectionsProps) {
return (
<section className="py-8 bg-white">
<div className="container mx-auto px-6">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-3xl text-center font-medium mb-12"
>
Коллекции
</motion.h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{collections.map((collection, index) => (
<section className="py-12 px-4 md:px-8 max-w-7xl mx-auto">
<h2 className="text-2xl md:text-3xl font-bold mb-8 font-['Playfair_Display']">Коллекции</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{collections.map((collection, index) => (
<Link href={collection.url} key={collection.id} className="group">
<motion.div
key={collection.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="group relative overflow-hidden rounded-lg"
className="overflow-hidden rounded-xl"
>
<Link href={collection.url}>
<div className="relative aspect-[4/3] w-full overflow-hidden">
<Image
src={collection.image}
alt={collection.name}
layout="fill"
objectFit="cover"
className="transition-all duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-black bg-opacity-20 transition-opacity group-hover:bg-opacity-30" />
</div>
<div className="relative aspect-[4/5] overflow-hidden rounded-xl">
<Image
src={collection.image}
alt={collection.name}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent"></div>
<div className="absolute bottom-0 left-0 right-0 p-6 text-white">
<h3 className="text-xl font-medium mb-2">{collection.name}</h3>
<p className="text-sm opacity-90">{collection.description}</p>
<h3 className="text-xl font-semibold mb-2">{collection.name}</h3>
<p className="text-sm text-white/80 line-clamp-2">{collection.description}</p>
<span className="inline-block mt-4 text-sm font-medium border-b border-white pb-1 transition-all group-hover:border-transparent">
Смотреть коллекцию
</span>
</div>
</Link>
</div>
</motion.div>
))}
</div>
</Link>
))}
</div>
</section>
);
)
}

View File

@ -1,10 +1,12 @@
import Link from "next/link";
import { Search, Heart, User, ShoppingCart } from "lucide-react";
import { Search, Heart, User, ShoppingCart, ChevronLeft } from "lucide-react";
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import Image from "next/image";
import { useRouter } from "next/router";
export default function Header() {
const router = useRouter();
// Состояние для отслеживания прокрутки страницы
const [scrolled, setScrolled] = useState(false);
@ -23,33 +25,57 @@ export default function Header() {
};
}, [scrolled]);
// Функция для возврата на предыдущую страницу
const goBack = () => {
router.back();
};
// Проверяем, находимся ли мы на главной странице
const isHomePage = router.pathname === "/";
// Проверяем, находимся ли мы на странице категорий или коллекций
const isDetailPage = router.pathname.includes("[slug]");
return (
<header className={`fixed w-full z-50 transition-all duration-300 ${scrolled ? 'bg-white shadow-sm' : 'bg-transparent'}`}>
<nav className={`py-4 transition-all duration-300 ${scrolled ? 'text-black' : 'text-white'}`}>
<header className="fixed w-full z-50 transition-all duration-300 bg-white shadow-sm">
<nav className="py-4 transition-all duration-300 text-black">
<div className="container mx-auto px-4 flex items-center justify-between">
<div className="flex items-center space-x-6">
<Link href="/catalog" className="text-sm font-medium hover:opacity-70 transition-opacity">
{isDetailPage && (
<button
onClick={goBack}
className="flex items-center text-sm font-medium hover:opacity-70 transition-opacity mr-4"
>
<ChevronLeft className="w-4 h-4 mr-1" />
Назад
</button>
)}
<Link href="/category" className="text-sm font-medium hover:opacity-70 transition-opacity">
Каталог
</Link>
<Link href="/new" className="text-sm font-medium hover:opacity-70 transition-opacity">
<Link href="/collections" className="text-sm font-medium hover:opacity-70 transition-opacity">
Коллекции
</Link>
<Link href="/new-arrivals" className="text-sm font-medium hover:opacity-70 transition-opacity">
Новинки
</Link>
<Link href="/stores" className="text-sm font-medium hover:opacity-70 transition-opacity">
Магазины
</Link>
</div>
<Link href="/" className="text-sm font-medium italic">
<Link href="/" className="absolute left-1/2 transform -translate-x-1/2">
<div className="relative h-10 w-32">
Dressed for Success
<Image
src="/logo.png"
alt="Brand Logo"
fill
className="object-contain"
priority
/>
</div>
</Link>
<div className="flex items-center space-x-5">
<button className="hover:opacity-70 transition-opacity">
<Search className="w-5 h-5" />
</button>
<Link href="/favorites" className="relative hover:opacity-70 transition-opacity">
<Heart className="w-5 h-5" />
<span className={`absolute -top-2 -right-2 ${scrolled ? 'bg-black text-white' : 'bg-white text-black'} text-xs rounded-full w-4 h-4 flex items-center justify-center`}>
<span className="absolute -top-2 -right-2 bg-black text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">
0
</span>
</Link>
@ -58,7 +84,7 @@ export default function Header() {
</Link>
<Link href="/cart" className="relative hover:opacity-70 transition-opacity">
<ShoppingCart className="w-5 h-5" />
<span className={`absolute -top-2 -right-2 ${scrolled ? 'bg-black text-white' : 'bg-white text-black'} text-xs rounded-full w-4 h-4 flex items-center justify-center`}>
<span className="absolute -top-2 -right-2 bg-black text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">
0
</span>
</Link>

View File

@ -1,7 +1,8 @@
"use client"
import { useState, useEffect } from "react";
import Image from "next/image";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { motion } from "framer-motion";
// Типы для свойств компонента
interface HeroProps {
@ -9,126 +10,53 @@ interface HeroProps {
}
export default function Hero({ images = [] }: HeroProps) {
// Состояние для текущего индекса слайда
const [currentIndex, setCurrentIndex] = useState(0);
// Состояние для автоматического воспроизведения
const [autoplay, setAutoplay] = useState(true);
// Состояние для направления анимации: 1 - следующий слайд, -1 - предыдущий слайд
const [direction, setDirection] = useState(0);
// Если изображения не переданы или массив пуст, используем стандартное изображение
const heroImages = images.length > 0
? images
: ['/photos/head_photo.png'];
// Функция для перехода к следующему слайду
const nextSlide = () => {
setDirection(1);
setCurrentIndex((prevIndex) =>
prevIndex === heroImages.length - 1 ? 0 : prevIndex + 1
);
};
// Функция для перехода к предыдущему слайду
const prevSlide = () => {
setDirection(-1);
setCurrentIndex((prevIndex) =>
prevIndex === 0 ? heroImages.length - 1 : prevIndex - 1
);
};
// Эффект для автоматического воспроизведения слайдера
useEffect(() => {
let interval: NodeJS.Timeout;
if (autoplay) {
interval = setInterval(() => {
nextSlide();
}, 5000); // Смена слайда каждые 5 секунд
}
return () => {
if (interval) clearInterval(interval);
};
}, [autoplay, currentIndex]);
// Определяем варианты анимации для эффекта "скольжения"
const variants = {
enter: (direction: number) => ({
x: direction > 0 ? "100%" : "-100%",
opacity: 0
}),
center: {
x: 0,
opacity: 1
},
exit: (direction: number) => ({
x: direction > 0 ? "-100%" : "100%",
opacity: 0
}),
};
return (
<div className="relative h-screen overflow-hidden">
<AnimatePresence custom={direction} mode="wait">
<motion.div
key={currentIndex}
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.7 }}
className="absolute inset-0"
>
<div className="relative h-[80vh] md:h-screen overflow-hidden bg-white">
{/* Логотип по центру */}
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-20">
<div className="relative w-[200px] h-[200px] md:w-[300px] md:h-[300px]">
<Image
src={heroImages[currentIndex]}
alt="New Arrivals"
layout="fill"
objectFit="cover"
quality={100}
src="/logotip.png"
alt="Brand Logo"
fill
className="object-contain"
priority
/>
{/* Убрали затемнение изображения */}
</motion.div>
</AnimatePresence>
<button
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-white bg-opacity-50 p-2 rounded-full hover:bg-opacity-70 transition-all"
onClick={() => {
prevSlide();
setAutoplay(false);
}}
>
<ChevronLeft className="w-6 h-6 text-black" />
</button>
<button
className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-white bg-opacity-50 p-2 rounded-full hover:bg-opacity-70 transition-all"
onClick={() => {
nextSlide();
setAutoplay(false);
}}
>
<ChevronRight className="w-6 h-6 text-black" />
</button>
{/* Индикаторы слайдов */}
<div className="absolute bottom-8 left-0 right-0 flex justify-center space-x-2">
{heroImages.map((_, index) => (
<button
key={index}
onClick={() => {
// Определяем направление в зависимости от выбранного индекса
setDirection(index > currentIndex ? 1 : -1);
setCurrentIndex(index);
setAutoplay(false);
}}
className={`w-3 h-3 rounded-full ${
index === currentIndex ? 'bg-white' : 'bg-white/50'
}`}
/>
))}
</div>
</div>
{/* Контент */}
<div className="absolute inset-0 flex items-center justify-center z-10">
<div className="text-center text-black px-4 mt-[300px] md:mt-[400px]">
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2, duration: 0.8 }}
className="text-4xl md:text-6xl font-bold mb-4 font-['Playfair_Display']"
>
Элегантность в каждой детали
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.8 }}
className="text-lg md:text-xl mb-8 max-w-2xl mx-auto"
>
Откройте для себя новую коллекцию, созданную с любовью к качеству и стилю
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6, duration: 0.8 }}
>
<a
href="#new-arrivals"
className="bg-black text-white px-8 py-3 rounded-md font-medium hover:bg-gray-800 transition-colors inline-block"
>
Смотреть коллекцию
</a>
</motion.div>
</div>
</div>
</div>
);

View File

@ -1,103 +1,256 @@
import { motion } from 'framer-motion';
import Image from 'next/image';
import { useState } from 'react';
import { Heart } from 'lucide-react';
"use client"
import { useState, useRef, useEffect } from "react"
import Image from "next/image"
import { Heart, ChevronLeft, ChevronRight } from "lucide-react"
import Link from "next/link"
import { Product, formatPrice } from "../data/products"
import { motion } from "framer-motion"
// Типы для свойств компонента
interface NewArrivalsProps {
products: Product[];
}
// Тип для товара
interface Product {
id: number;
name: string;
price: number;
images: string[];
description: string;
isNew?: boolean;
products: Product[]
}
export default function NewArrivals({ products }: NewArrivalsProps) {
// Состояние для отслеживания наведения на карточки товаров
const [hoveredProduct, setHoveredProduct] = useState<number | null>(null);
// Состояние для отслеживания избранных товаров
const [favorites, setFavorites] = useState<number[]>([]);
const [hoveredProduct, setHoveredProduct] = useState<number | null>(null)
const [favorites, setFavorites] = useState<number[]>([])
const sliderRef = useRef<HTMLDivElement>(null)
const [showLeftArrow, setShowLeftArrow] = useState(false)
const [showRightArrow, setShowRightArrow] = useState(true)
const [isDragging, setIsDragging] = useState(false)
const [startX, setStartX] = useState(0)
const [scrollLeftValue, setScrollLeftValue] = useState(0)
const [currentSlide, setCurrentSlide] = useState(0)
const [slidesPerView, setSlidesPerView] = useState(4)
// Функция для добавления/удаления товара из избранного
const toggleFavorite = (id: number, e: React.MouseEvent) => {
e.stopPropagation();
setFavorites(prev =>
prev.includes(id)
? prev.filter(itemId => itemId !== id)
: [...prev, id]
);
e.stopPropagation()
e.preventDefault()
setFavorites((prev) => (prev.includes(id) ? prev.filter((itemId) => itemId !== id) : [...prev, id]))
}
// Определение количества слайдов на экране в зависимости от размера экрана
useEffect(() => {
const handleResize = () => {
const width = window.innerWidth;
if (width < 640) {
setSlidesPerView(1);
} else if (width < 768) {
setSlidesPerView(2);
} else if (width < 1024) {
setSlidesPerView(3);
} else {
setSlidesPerView(4);
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
// Обновление состояния стрелок при скролле
const updateArrowVisibility = () => {
if (sliderRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = sliderRef.current
setShowLeftArrow(scrollLeft > 0)
setShowRightArrow(scrollLeft < scrollWidth - clientWidth - 5) // 5px buffer
// Обновление текущего слайда
if (clientWidth > 0) {
const slideWidth = scrollWidth / products.length;
const newCurrentSlide = Math.round(scrollLeft / slideWidth);
setCurrentSlide(newCurrentSlide);
}
}
}
// Инициализация и обработка изменений размера
useEffect(() => {
updateArrowVisibility()
window.addEventListener("resize", updateArrowVisibility)
return () => window.removeEventListener("resize", updateArrowVisibility)
}, [])
// Обработчики скролла
const handleScroll = () => {
updateArrowVisibility()
}
const scrollLeft = () => {
if (sliderRef.current) {
const itemWidth = sliderRef.current.scrollWidth / products.length;
const newScrollLeft = Math.max(0, sliderRef.current.scrollLeft - (itemWidth * slidesPerView));
sliderRef.current.scrollTo({ left: newScrollLeft, behavior: "smooth" });
}
}
const scrollRight = () => {
if (sliderRef.current) {
const itemWidth = sliderRef.current.scrollWidth / products.length;
const newScrollLeft = Math.min(
sliderRef.current.scrollWidth - sliderRef.current.clientWidth,
sliderRef.current.scrollLeft + (itemWidth * slidesPerView)
);
sliderRef.current.scrollTo({ left: newScrollLeft, behavior: "smooth" });
}
}
// Обработчики перетаскивания (для мобильных)
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true)
setStartX(e.pageX - (sliderRef.current?.offsetLeft || 0))
setScrollLeftValue(sliderRef.current?.scrollLeft || 0)
}
const handleMouseUp = () => {
setIsDragging(false)
}
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDragging) return
e.preventDefault()
if (sliderRef.current) {
const x = e.pageX - (sliderRef.current.offsetLeft || 0)
const walk = (x - startX) * 2 // Скорость скролла
sliderRef.current.scrollLeft = scrollLeftValue - walk
}
}
const handleMouseLeave = () => {
setIsDragging(false)
}
// Обработчики тач-событий
const handleTouchStart = (e: React.TouchEvent) => {
if (sliderRef.current) {
setStartX(e.touches[0].pageX - (sliderRef.current.offsetLeft || 0))
setScrollLeftValue(sliderRef.current.scrollLeft)
}
}
const handleTouchMove = (e: React.TouchEvent) => {
if (sliderRef.current) {
const x = e.touches[0].pageX - (sliderRef.current.offsetLeft || 0)
const walk = (x - startX) * 2
sliderRef.current.scrollLeft = scrollLeftValue - walk
}
}
// Переход к определенному слайду
const goToSlide = (index: number) => {
if (sliderRef.current) {
const itemWidth = sliderRef.current.scrollWidth / products.length;
sliderRef.current.scrollTo({ left: itemWidth * index, behavior: "smooth" });
}
};
return (
<section className="py-8 bg-white">
<div className="container mx-auto px-6">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-3xl text-center font-medium mb-12"
<section id="new-arrivals" className="my-12 px-4 md:px-8 max-w-7xl mx-auto relative">
<h2 className="text-2xl md:text-3xl font-bold mb-6 font-['Playfair_Display']">Новинки</h2>
{/* Контейнер слайдера с кнопками навигации */}
<div className="relative">
{/* Кнопка влево */}
{showLeftArrow && (
<button
onClick={scrollLeft}
className="absolute -left-4 top-1/2 -translate-y-1/2 z-10 bg-white/80 hover:bg-white rounded-full p-2 shadow-md transition-all"
aria-label="Предыдущие товары"
>
<ChevronLeft className="w-6 h-6" />
</button>
)}
{/* Слайдер продуктов */}
<div
ref={sliderRef}
className="flex overflow-x-auto gap-4 md:gap-6 pb-6 scrollbar-hide snap-x snap-mandatory"
onScroll={handleScroll}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleMouseUp}
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
>
Новинки
</motion.h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-6 gap-y-12">
{products.map((product, index) => (
{products.map((product) => (
<motion.div
key={product.id}
className="flex-none w-[280px] sm:w-[320px] md:w-[300px] lg:w-[280px] xl:w-[300px] snap-start"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="group relative"
onMouseEnter={() => setHoveredProduct(product.id)}
onMouseLeave={() => setHoveredProduct(null)}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: product.id * 0.05 }}
whileHover={{ y: -5, transition: { duration: 0.2 } }}
>
{/* Метка "New" */}
{product.isNew && (
<div className="absolute top-4 left-4 z-10 bg-gray-100 text-gray-700 text-xs px-3 py-1 rounded-full">
New
</div>
)}
{/* Кнопка избранного */}
<button
onClick={(e) => toggleFavorite(product.id, e)}
className="absolute top-4 right-4 z-10 p-1"
<Link
href={`/product/${product.slug}`}
className="block h-full"
onMouseEnter={() => setHoveredProduct(product.id)}
onMouseLeave={() => setHoveredProduct(null)}
>
<Heart
className={`w-6 h-6 transition-colors ${
favorites.includes(product.id)
? 'fill-black text-black'
: 'text-gray-400 hover:text-black'
}`}
/>
</button>
{/* Изображение товара */}
<div className="relative aspect-[3/4] w-full overflow-hidden mb-4">
<Image
src={hoveredProduct === product.id && product.images.length > 1 ? product.images[1] : product.images[0]}
alt={product.name}
layout="fill"
objectFit="cover"
className="transition-all duration-500 hover:scale-105"
/>
</div>
{/* Информация о товаре */}
<div className="px-1">
<h3 className="text-sm text-gray-700 mb-2 line-clamp-2">{product.name}</h3>
<p className="text-base font-medium">{product.price.toLocaleString()} </p>
</div>
<div className="relative overflow-hidden rounded-xl">
<div className="aspect-[3/4] relative overflow-hidden rounded-xl">
<Image
src={
hoveredProduct === product.id && product.images.length > 1 ? product.images[1] : product.images[0]
}
alt={product.name}
fill
sizes="(max-width: 640px) 280px, (max-width: 768px) 320px, (max-width: 1024px) 300px, 280px"
className="object-cover transition-all duration-500 hover:scale-105"
/>
{product.isNew && (
<span className="absolute top-4 left-4 bg-black text-white text-sm py-1 px-3 rounded">Новинка</span>
)}
<button
onClick={(e) => toggleFavorite(product.id, e)}
className="absolute top-4 right-4 bg-white/80 hover:bg-white rounded-full p-2 transition-all"
aria-label={favorites.includes(product.id) ? "Удалить из избранного" : "Добавить в избранное"}
>
<Heart
className={`w-5 h-5 ${favorites.includes(product.id) ? "fill-red-500 text-red-500" : "text-gray-700"}`}
/>
</button>
</div>
</div>
<div className="mt-4">
<h3 className="text-lg font-medium">{product.name}</h3>
<p className="mt-1 text-lg font-bold">{formatPrice(product.price)} </p>
</div>
</Link>
</motion.div>
))}
</div>
{/* Кнопка вправо */}
{showRightArrow && (
<button
onClick={scrollRight}
className="absolute -right-4 top-1/2 -translate-y-1/2 z-10 bg-white/80 hover:bg-white rounded-full p-2 shadow-md transition-all"
aria-label="Следующие товары"
>
<ChevronRight className="w-6 h-6" />
</button>
)}
</div>
{/* Индикаторы слайдов (точки) */}
<div className="flex justify-center mt-6 space-x-2">
{Array.from({ length: Math.ceil(products.length / slidesPerView) }).map((_, index) => (
<button
key={index}
onClick={() => goToSlide(index * slidesPerView)}
className={`w-2 h-2 rounded-full transition-all ${
Math.floor(currentSlide / slidesPerView) === index ? "bg-black scale-150" : "bg-gray-300"
}`}
aria-label={`Перейти к слайду ${index + 1}`}
/>
))}
</div>
</section>
);
)
}

View File

@ -1,58 +1,43 @@
import { motion } from 'framer-motion';
import Image from 'next/image';
import Link from 'next/link';
import Image from "next/image"
import Link from "next/link"
import { motion } from "framer-motion"
import { Category } from "../data/categories"
// Типы для свойств компонента
interface PopularCategoriesProps {
categories: Category[];
}
// Тип для категории
interface Category {
id: number;
name: string;
image: string;
url: string;
categories: Category[]
}
export default function PopularCategories({ categories }: PopularCategoriesProps) {
return (
<section className="py-8 bg-white">
<div className="container mx-auto px-6">
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-3xl text-center font-medium mb-12"
>
Популярные категории
</motion.h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{categories.map((category, index) => (
<section className="py-12 px-4 md:px-8 max-w-7xl mx-auto">
<h2 className="text-2xl md:text-3xl font-bold mb-8 font-['Playfair_Display']">Популярные категории</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-6">
{categories.map((category, index) => (
<Link href={category.url} key={category.id} className="group">
<motion.div
key={category.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="group"
transition={{ delay: index * 0.05 }}
className="overflow-hidden rounded-xl"
>
<Link href={category.url} className="block">
<div className="relative aspect-square w-full overflow-hidden mb-4 bg-gray-100">
<Image
src={category.image}
alt={category.name}
layout="fill"
objectFit="cover"
className="transition-all duration-500 group-hover:scale-105"
/>
<div className="relative aspect-square overflow-hidden rounded-xl">
<Image
src={category.image}
alt={category.name}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
<div className="absolute bottom-0 left-0 right-0 p-4 text-white">
<h3 className="text-base md:text-lg font-medium">{category.name}</h3>
</div>
<h3 className="text-center text-sm font-medium">{category.name}</h3>
</Link>
</div>
</motion.div>
))}
</div>
</Link>
))}
</div>
</section>
);
)
}

87
data/categories.ts Normal file
View File

@ -0,0 +1,87 @@
// Тип для категории
export interface Category {
id: number;
name: string;
image: string;
url: string;
slug: string;
description: string;
}
// Данные о категориях
export const categories: Category[] = [
{
id: 1,
name: 'Женская обувь',
image: '/category/shoes.jpg',
url: '/category/shoes',
slug: 'shoes',
description: 'Элегантная и комфортная обувь для любого случая. Наша коллекция включает в себя модели из высококачественных материалов, созданные с вниманием к деталям.'
},
{
id: 2,
name: 'Шляпы и перчатки',
image: '/category/hat.jpg',
url: '/category/hats-and-gloves',
slug: 'hats-and-gloves',
description: 'Стильные аксессуары, которые дополнят ваш образ и защитят от непогоды. Изготовлены из премиальных материалов с использованием традиционных техник.'
},
{
id: 3,
name: 'Штаны и брюки',
image: '/category/pants.jpg',
url: '/category/pants',
slug: 'pants',
description: 'Разнообразные модели брюк для любого случая. От классических деловых моделей до повседневных и спортивных вариантов, созданных для комфорта и стиля.'
},
{
id: 4,
name: 'Свитеры и кардиганы',
image: '/category/sweaters.jpg',
url: '/category/sweaters',
slug: 'sweaters',
description: 'Теплые и уютные свитеры и кардиганы из натуральных материалов. Идеальный выбор для холодного времени года, сочетающий комфорт и элегантность.'
},
{
id: 5,
name: 'Платья и юбки',
image: '/category/dress.jpg',
url: '/category/dress',
slug: 'dress',
description: 'Элегантные платья и юбки для любого случая. От повседневных моделей до вечерних нарядов, подчеркивающих женственность и изысканность.'
},
{
id: 6,
name: 'Костюмы и пальто',
image: '/category/jacket.jpg',
url: '/category/jackets',
slug: 'jackets',
description: 'Стильные костюмы и пальто высокого качества. Идеальный выбор для создания элегантного образа в любое время года.'
},
{
id: 7,
name: 'Женский шелк',
image: '/category/silk.jpg',
url: '/category/womens-silk',
slug: 'womens-silk',
description: 'Роскошные изделия из натурального шелка. Блузки, платья и аксессуары, которые подчеркнут вашу индивидуальность и создадут ощущение роскоши.'
},
{
id: 8,
name: 'Аксессуары',
image: '/category/scarf.jpg',
url: '/category/accessories',
slug: 'accessories',
description: 'Стильные аксессуары, которые дополнят ваш образ. Сумки, шарфы, ремни и другие детали, которые сделают ваш образ завершенным и уникальным.'
}
];
// Функция для получения категории по slug
export const getCategoryBySlug = (slug: string): Category | undefined => {
return categories.find(category => category.slug === slug);
};
// Функция для получения категории по id
export const getCategoryById = (id: number): Category | undefined => {
return categories.find(category => category.id === id);
};

63
data/collections.ts Normal file
View File

@ -0,0 +1,63 @@
// Тип для коллекции
export interface Collection {
id: number;
name: string;
image: string;
description: string;
url: string;
slug: string;
}
// Данные о коллекциях
export const collections: Collection[] = [
{
id: 1,
name: 'Весна-Лето 2024',
image: '/photos/photo1.jpg',
description: 'Легкие ткани и яркие цвета для теплого сезона. Наша новая коллекция воплощает свежесть и элегантность, идеально подходящую для весенних и летних дней.',
url: '/collections/spring-summer-2024',
slug: 'spring-summer-2024'
},
{
id: 2,
name: 'Осень-Зима 2023',
image: '/photos/photo2.jpg',
description: 'Теплые и уютные модели для холодного времени года. Коллекция сочетает в себе комфорт и стиль, предлагая элегантные решения для зимнего гардероба.',
url: '/collections/autumn-winter-2023',
slug: 'autumn-winter-2023'
},
{
id: 3,
name: 'Базовый гардероб',
image: '/photos/head_photo.png',
description: 'Классические модели, которые никогда не выходят из моды. Эта коллекция представляет собой основу любого гардероба, включая вневременные предметы одежды высокого качества.',
url: '/collections/basic',
slug: 'basic'
},
{
id: 4,
name: 'Вечерняя коллекция',
image: '/photos/evening.jpg',
description: 'Элегантные наряды для особых случаев. Изысканные ткани и утонченный дизайн создают неповторимые образы для вечерних мероприятий и торжественных событий.',
url: '/collections/evening',
slug: 'evening'
},
{
id: 5,
name: 'Деловой стиль',
image: '/photos/business.jpg',
description: 'Стильная и функциональная одежда для работы и деловых встреч. Коллекция сочетает в себе профессионализм и элегантность, подчеркивая ваш статус и вкус.',
url: '/collections/business',
slug: 'business'
}
];
// Функция для получения коллекции по slug
export const getCollectionBySlug = (slug: string): Collection | undefined => {
return collections.find(collection => collection.slug === slug);
};
// Функция для получения коллекции по id
export const getCollectionById = (id: number): Collection | undefined => {
return collections.find(collection => collection.id === id);
};

170
data/products.ts Normal file
View File

@ -0,0 +1,170 @@
// Тип для товара
export interface Product {
id: number;
name: string;
price: number;
images: string[];
description: string;
isNew?: boolean;
categoryId: number;
slug: string;
}
// Данные о товарах
export const products: Product[] = [
{
id: 1,
name: 'Пальто оверсайз',
price: 43800,
images: ['/wear/palto1.jpg', '/wear/palto2.jpg'],
description: 'Элегантное пальто оверсайз высокого качества. Изготовлено из премиальных материалов, обеспечивающих комфорт и тепло. Идеально подходит для холодного сезона и создания стильного образа.',
isNew: true,
categoryId: 6,
slug: 'palto-oversaiz'
},
{
id: 2,
name: 'Костюм хлопок',
price: 12800,
images: ['/wear/pidzak2.jpg', '/wear/pidzak1.jpg'],
description: 'Стильный костюм для особых случаев. Выполнен из высококачественного хлопка, обеспечивающего комфорт и элегантный внешний вид. Идеально подходит для деловых встреч и официальных мероприятий.',
isNew: true,
categoryId: 6,
slug: 'kostyum-hlopok'
},
{
id: 3,
name: 'Блузка',
price: 3500,
images: ['/wear/sorochka1.jpg', '/wear/sorochka2.jpg'],
description: 'Классическая блузка в коричневом цвете. Изготовлена из мягкой и приятной к телу ткани. Универсальная модель, которая подойдет как для офиса, так и для повседневного образа.',
isNew: true,
categoryId: 7,
slug: 'bluzka'
},
{
id: 4,
name: 'Платье со сборкой',
price: 28800,
images: ['/wear/jumpsuit_1.jpg', '/wear/jumpsuit_2.jpg'],
description: 'Элегантное платье высокого качества со сборкой. Подчеркивает фигуру и создает женственный силуэт. Идеально подходит для особых случаев и вечерних мероприятий.',
isNew: true,
categoryId: 5,
slug: 'plate-so-sborkoy'
},
{
id: 5,
name: 'Кожаные туфли',
price: 15600,
images: ['/wear/shoes1.jpg', '/wear/shoes2.jpg'],
description: 'Элегантные кожаные туфли ручной работы. Изготовлены из натуральной кожи высшего качества. Комфортная колодка и стильный дизайн делают эту модель незаменимой в гардеробе.',
isNew: false,
categoryId: 1,
slug: 'kozhanye-tufli'
},
{
id: 6,
name: 'Шелковый шарф',
price: 5900,
images: ['/wear/scarf1.jpg', '/wear/scarf2.jpg'],
description: 'Роскошный шелковый шарф с уникальным принтом. Изготовлен из 100% натурального шелка. Добавит элегантности и шарма любому образу.',
isNew: false,
categoryId: 8,
slug: 'shelkovyj-sharf'
},
{
id: 7,
name: 'Шерстяной свитер',
price: 8700,
images: ['/wear/sweater1.jpg', '/wear/sweater2.jpg'],
description: 'Теплый шерстяной свитер крупной вязки. Изготовлен из мягкой шерсти мериноса. Идеально подходит для холодного времени года.',
isNew: false,
categoryId: 4,
slug: 'sherstyanoj-sviter'
},
{
id: 8,
name: 'Классические брюки',
price: 7500,
images: ['/wear/pants1.jpg', '/wear/pants2.jpg'],
description: 'Классические брюки прямого кроя. Выполнены из высококачественной ткани с добавлением эластана для комфортной посадки. Универсальная модель для офиса и повседневной носки.',
isNew: false,
categoryId: 3,
slug: 'klassicheskie-bryuki'
},
{
id: 9,
name: 'Фетровая шляпа',
price: 6200,
images: ['/wear/hat1.jpg', '/wear/hat2.jpg'],
description: 'Элегантная фетровая шляпа ручной работы. Изготовлена из высококачественного фетра. Дополнит любой образ и защитит от непогоды.',
isNew: false,
categoryId: 2,
slug: 'fetrovaya-shlyapa'
},
{
id: 10,
name: 'Шелковая блузка',
price: 9800,
images: ['/wear/silk1.jpg', '/wear/silk2.jpg'],
description: 'Роскошная шелковая блузка с элегантным дизайном. Изготовлена из 100% натурального шелка. Идеально подходит для создания изысканного образа.',
isNew: true,
categoryId: 7,
slug: 'shelkovaya-bluzka'
},
{
id: 11,
name: 'Кожаная сумка',
price: 18500,
images: ['/wear/bag1.jpg', '/wear/bag2.jpg'],
description: 'Стильная кожаная сумка ручной работы. Изготовлена из натуральной кожи высшего качества. Вместительная и функциональная модель для повседневного использования.',
isNew: false,
categoryId: 8,
slug: 'kozhanaya-sumka'
},
{
id: 12,
name: 'Кашемировое пальто',
price: 52000,
images: ['/wear/coat1.jpg', '/wear/coat2.jpg'],
description: 'Роскошное кашемировое пальто классического кроя. Изготовлено из 100% кашемира высшего качества. Элегантная модель, которая прослужит долгие годы.',
isNew: true,
categoryId: 6,
slug: 'kashemirovoe-palto'
}
];
// Функция для получения товаров по категории
export const getProductsByCategory = (categoryId: number): Product[] => {
return products.filter(product => product.categoryId === categoryId);
};
// Функция для получения новых товаров
export const getNewProducts = (): Product[] => {
return products.filter(product => product.isNew);
};
// Функция для получения товара по slug
export const getProductBySlug = (slug: string): Product | undefined => {
return products.find(product => product.slug === slug);
};
// Функция для получения товара по id
export const getProductById = (id: number): Product | undefined => {
return products.find(product => product.id === id);
};
// Функция для получения похожих товаров
export const getSimilarProducts = (productId: number, limit: number = 4): Product[] => {
const product = getProductById(productId);
if (!product) return [];
return products
.filter(p => p.id !== productId && p.categoryId === product.categoryId)
.slice(0, limit);
};
// Функция для форматирования цены
export const formatPrice = (price: number): string => {
return new Intl.NumberFormat('ru-RU').format(price);
};

170
pages/category/[slug].tsx Normal file
View File

@ -0,0 +1,170 @@
import { GetStaticPaths, GetStaticProps } from "next"
import Head from "next/head"
import Image from "next/image"
import Link from "next/link"
import { useState } from "react"
import { Heart } from "lucide-react"
import Header from "../../components/Header"
import Footer from "../../components/Footer"
import { categories, getCategoryBySlug } from "../../data/categories"
import { Product, getProductsByCategory, formatPrice } from "../../data/products"
import { useRouter } from "next/router"
import { motion } from "framer-motion"
interface CategoryPageProps {
category: {
id: number
name: string
image: string
url: string
slug: string
description: string
}
products: Product[]
}
export default function CategoryPage({ category, products }: CategoryPageProps) {
const router = useRouter()
const [hoveredProduct, setHoveredProduct] = useState<number | null>(null)
const [favorites, setFavorites] = useState<number[]>([])
// Если страница еще загружается, показываем заглушку
if (router.isFallback) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-gray-900"></div>
</div>
)
}
// Функция для добавления/удаления товара из избранного
const toggleFavorite = (id: number, e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
setFavorites((prev) => (prev.includes(id) ? prev.filter((itemId) => itemId !== id) : [...prev, id]))
}
return (
<div className="min-h-screen bg-white font-['Arimo']">
<Head>
<title>{category.name} | Brand Store</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Header />
<main className="max-w-7xl mx-auto px-4 py-12 md:px-8">
<div className="mb-12">
<Link href="/category" className="text-gray-600 hover:text-black transition-colors">
Все категории
</Link>
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-3xl md:text-4xl font-bold mt-4 font-['Playfair_Display']"
>
{category.name}
</motion.h1>
<p className="mt-2 text-gray-600 max-w-3xl">{category.description}</p>
</div>
{products.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
{products.map((product) => (
<motion.div
key={product.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: product.id * 0.05 }}
whileHover={{ y: -5, transition: { duration: 0.2 } }}
>
<Link
href={`/product/${product.slug}`}
className="block h-full"
onMouseEnter={() => setHoveredProduct(product.id)}
onMouseLeave={() => setHoveredProduct(null)}
>
<div className="relative overflow-hidden rounded-xl">
<div className="aspect-[3/4] relative overflow-hidden rounded-xl">
<Image
src={
hoveredProduct === product.id && product.images.length > 1
? product.images[1]
: product.images[0]
}
alt={product.name}
fill
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw"
className="object-cover transition-all duration-500 group-hover:scale-105"
/>
{product.isNew && (
<span className="absolute top-4 left-4 bg-black text-white text-sm py-1 px-3 rounded">
Новинка
</span>
)}
<button
onClick={(e) => toggleFavorite(product.id, e)}
className="absolute top-4 right-4 bg-white/80 hover:bg-white rounded-full p-2 transition-all"
aria-label={favorites.includes(product.id) ? "Удалить из избранного" : "Добавить в избранное"}
>
<Heart
className={`w-5 h-5 ${favorites.includes(product.id) ? "fill-red-500 text-red-500" : "text-gray-700"}`}
/>
</button>
</div>
</div>
<div className="mt-4">
<h3 className="text-lg font-medium">{product.name}</h3>
<p className="mt-1 text-lg font-bold">{formatPrice(product.price)} </p>
</div>
</Link>
</motion.div>
))}
</div>
) : (
<div className="text-center py-12">
<p className="text-xl text-gray-600">В этой категории пока нет товаров</p>
<Link href="/" className="mt-4 inline-block bg-black text-white px-6 py-2 rounded-md hover:bg-gray-800 transition-colors">
Вернуться на главную
</Link>
</div>
)}
</main>
<Footer />
</div>
)
}
export const getStaticPaths: GetStaticPaths = async () => {
const paths = categories.map((category) => ({
params: { slug: category.slug },
}))
return {
paths,
fallback: 'blocking',
}
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
const slug = params?.slug as string
const category = getCategoryBySlug(slug)
if (!category) {
return {
notFound: true,
}
}
const products = getProductsByCategory(category.id)
return {
props: {
category,
products,
},
revalidate: 600, // Перегенерация страницы каждые 10 минут
}
}

1
pages/category/index.tsx Normal file
View File

@ -0,0 +1 @@

View File

@ -0,0 +1,205 @@
import { GetStaticPaths, GetStaticProps } from "next"
import Head from "next/head"
import Image from "next/image"
import Link from "next/link"
import { useState } from "react"
import { Heart } from "lucide-react"
import Header from "../../components/Header"
import Footer from "../../components/Footer"
import { collections, getCollectionBySlug } from "../../data/collections"
import { Product, products, formatPrice } from "../../data/products"
import { useRouter } from "next/router"
import { motion } from "framer-motion"
interface CollectionPageProps {
collection: {
id: number
name: string
image: string
description: string
url: string
slug: string
}
// Для демонстрации используем все товары, в реальном проекте нужно связать коллекции с товарами
products: Product[]
}
export default function CollectionPage({ collection, products }: CollectionPageProps) {
const router = useRouter()
const [hoveredProduct, setHoveredProduct] = useState<number | null>(null)
const [favorites, setFavorites] = useState<number[]>([])
// Если страница еще загружается, показываем заглушку
if (router.isFallback) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-gray-900"></div>
</div>
)
}
// Функция для добавления/удаления товара из избранного
const toggleFavorite = (id: number, e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
setFavorites((prev) => (prev.includes(id) ? prev.filter((itemId) => itemId !== id) : [...prev, id]))
}
// Для демонстрации выбираем случайные товары для коллекции
// В реальном приложении здесь должна быть логика выбора товаров по коллекции
const collectionProducts = products.slice(0, 8)
return (
<div className="min-h-screen bg-white font-['Arimo']">
<Head>
<title>{collection.name} | Brand Store</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Header />
<main>
{/* Баннер коллекции */}
<div className="relative h-[50vh] md:h-[60vh]">
<Image
src={collection.image}
alt={collection.name}
fill
className="object-cover"
priority
quality={95}
/>
<div className="absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center">
<div className="text-center text-white px-4">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-3xl md:text-5xl font-bold mb-4 font-['Playfair_Display']"
>
{collection.name}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="max-w-2xl mx-auto text-lg"
>
{collection.description}
</motion.p>
</div>
</div>
</div>
{/* Список товаров */}
<section className="py-12 px-4 md:px-8 max-w-7xl mx-auto">
<div className="mb-8">
<Link href="/collections" className="text-gray-600 hover:text-black transition-colors">
Все коллекции
</Link>
<h2 className="text-2xl md:text-3xl font-bold mt-4 font-['Playfair_Display']">
Товары из коллекции
</h2>
</div>
{collectionProducts.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
{collectionProducts.map((product) => (
<motion.div
key={product.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: product.id * 0.05 }}
whileHover={{ y: -5, transition: { duration: 0.2 } }}
>
<Link
href={`/product/${product.slug}`}
className="block h-full"
onMouseEnter={() => setHoveredProduct(product.id)}
onMouseLeave={() => setHoveredProduct(null)}
>
<div className="relative overflow-hidden rounded-xl">
<div className="aspect-[3/4] relative overflow-hidden rounded-xl">
<Image
src={
hoveredProduct === product.id && product.images.length > 1
? product.images[1]
: product.images[0]
}
alt={product.name}
fill
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw"
className="object-cover transition-all duration-500 group-hover:scale-105"
/>
{product.isNew && (
<span className="absolute top-4 left-4 bg-black text-white text-sm py-1 px-3 rounded">
Новинка
</span>
)}
<button
onClick={(e) => toggleFavorite(product.id, e)}
className="absolute top-4 right-4 bg-white/80 hover:bg-white rounded-full p-2 transition-all"
aria-label={favorites.includes(product.id) ? "Удалить из избранного" : "Добавить в избранное"}
>
<Heart
className={`w-5 h-5 ${favorites.includes(product.id) ? "fill-red-500 text-red-500" : "text-gray-700"}`}
/>
</button>
</div>
</div>
<div className="mt-4">
<h3 className="text-lg font-medium">{product.name}</h3>
<p className="mt-1 text-lg font-bold">{formatPrice(product.price)} </p>
</div>
</Link>
</motion.div>
))}
</div>
) : (
<div className="text-center py-12">
<p className="text-xl text-gray-600">В этой коллекции пока нет товаров</p>
<Link href="/" className="mt-4 inline-block bg-black text-white px-6 py-2 rounded-md hover:bg-gray-800 transition-colors">
Вернуться на главную
</Link>
</div>
)}
</section>
</main>
<Footer />
</div>
)
}
export const getStaticPaths: GetStaticPaths = async () => {
const paths = collections.map((collection) => ({
params: { slug: collection.slug },
}))
return {
paths,
fallback: 'blocking',
}
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
const slug = params?.slug as string
const collection = getCollectionBySlug(slug)
if (!collection) {
return {
notFound: true,
}
}
// Для демонстрации используем все товары, в реальном проекте нужно связать коллекции с товарами
// Можно добавить фильтрацию по коллекции, если добавить поле collectionId в товары
return {
props: {
collection,
products,
},
revalidate: 600, // Перегенерация страницы каждые 10 минут
}
}

View File

@ -0,0 +1,73 @@
import { useState } from 'react';
import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
import Header from '../../components/Header';
import Footer from '../../components/Footer';
import { Collection, collections } from '../../data/collections';
import { motion } from 'framer-motion';
export default function Collections() {
const [hoveredCollection, setHoveredCollection] = useState<number | null>(null);
return (
<div className="min-h-screen bg-white font-['Arimo']">
<Head>
<title>Коллекции | Brand Store</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Header />
<main className="max-w-7xl mx-auto px-4 py-12 md:px-8">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-3xl md:text-4xl font-bold mb-8 font-['Playfair_Display']"
>
Коллекции
</motion.h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{collections.map((collection) => (
<motion.div
key={collection.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: collection.id * 0.1 }}
whileHover={{ y: -5, transition: { duration: 0.2 } }}
>
<Link
href={`/collections/${collection.slug}`}
className="block h-full"
onMouseEnter={() => setHoveredCollection(collection.id)}
onMouseLeave={() => setHoveredCollection(null)}
>
<div className="relative overflow-hidden rounded-xl aspect-[16/9]">
<Image
src={collection.image}
alt={collection.name}
fill
className={`object-cover transition-all duration-500 ${
hoveredCollection === collection.id ? 'scale-110' : 'scale-100'
}`}
/>
<div className="absolute inset-0 bg-black bg-opacity-30 transition-opacity duration-300"></div>
<div className="absolute inset-0 flex items-center justify-center">
<h2 className="text-white text-xl md:text-2xl font-bold text-center px-4 font-['Playfair_Display']">
{collection.name}
</h2>
</div>
</div>
<p className="mt-4 text-gray-600 line-clamp-3">{collection.description}</p>
</Link>
</motion.div>
))}
</div>
</main>
<Footer />
</div>
);
}

View File

@ -13,6 +13,9 @@ import fs from 'fs';
import path from 'path';
import { Heart } from 'lucide-react';
import Footer from '../components/Footer';
import { Product, products as allProducts } from '../data/products';
import { Collection, collections as allCollections } from '../data/collections';
import { Category, categories as allCategories } from '../data/categories';
// Типы для свойств компонента
interface HomeProps {
@ -22,33 +25,6 @@ interface HomeProps {
categories: Category[];
}
// Тип для товара
interface Product {
id: number;
name: string;
price: number;
images: string[];
description: string;
isNew?: boolean;
}
// Тип для коллекции
interface Collection {
id: number;
name: string;
image: string;
description: string;
url: string;
}
// Тип для категории
interface Category {
id: number;
name: string;
image: string;
url: string;
}
export default function Home({ heroImages, products, collections, categories }: HomeProps) {
// Состояние для отслеживания наведения на карточки товаров
const [hoveredProduct, setHoveredProduct] = useState<number | null>(null);
@ -135,119 +111,6 @@ export default function Home({ heroImages, products, collections, categories }:
// Функция для получения данных на стороне сервера
export async function getStaticProps() {
// Данные о товарах
const products = [
{
id: 1,
name: 'Пальто оверсайз',
price: 43800,
images: ['/wear/palto1.jpg', '/wear/palto2.jpg'],
description: 'Элегантное пальто оверсайз высокого качества',
isNew: true
},
{
id: 2,
name: 'Костюм хлопок',
price: 12800,
images: ['/wear/pidzak2.jpg', '/wear/pidzak1.jpg'],
description: 'Стильный костюм для особых случаев',
isNew: true
},
{
id: 3,
name: 'Блузка',
price: 3500,
images: ['/wear/sorochka1.jpg', '/wear/sorochka2.jpg'],
description: 'Классическая блузка в коричневом цвете',
isNew: true
},
{
id: 4,
name: 'Платье со сборкой',
price: 28800,
images: ['/wear/jumpsuit_1.jpg', '/wear/jumpsuit_2.jpg'],
description: 'Элегантное платье высокого качества',
isNew: true
}
];
// Данные о коллекциях
const collections = [
{
id: 1,
name: 'Весна-Лето 2024',
image: '/photos/photo1.jpg',
description: 'Легкие ткани и яркие цвета для теплого сезона',
url: '/collections/spring-summer-2024'
},
{
id: 2,
name: 'Осень-Зима 2023',
image: '/photos/photo2.jpg',
description: 'Теплые и уютные модели для холодного времени года',
url: '/collections/autumn-winter-2023'
},
{
id: 3,
name: 'Базовый гардероб',
image: '/photos/head_photo.png',
description: 'Классические модели, которые никогда не выходят из моды',
url: '/collections/basic'
}
];
// Данные о категориях
const categories = [
{
id: 1,
name: 'Женская обувь',
image: '/category/shoes.jpg',
url: '/category/shoes'
},
{
id: 2,
name: 'Шляпы и перчатки',
image: '/category/hat.jpg',
url: '/category/hats-and-gloves'
},
{
id: 3,
name: 'Штаны и брюки',
image: '/category/pants.jpg',
url: '/category/pants'
},
{
id: 4,
name: 'Свитеры и кардиганы',
image: '/category/sweaters.jpg',
url: '/category/sweaters'
},
{
id: 5,
name: 'Платья и юбки',
image: '/category/dress.jpg',
url: '/category/dress'
},
{
id: 6,
name: 'Костюмы',
image: '/category/jacket.jpg',
url: '/category/jackets'
},
{
id: 7,
name: 'Женский шелк',
image: '/category/silk.jpg',
url: '/category/womens-silk'
},
{
id: 8,
name: 'Аксессуары',
image: '/category/scarf.jpg',
url: '/category/accessories'
}
];
// Получение изображений для слайдера из папки hero_photos
const heroImagesDirectory = path.join(process.cwd(), 'public/hero_photos');
let heroImages = [];
@ -275,9 +138,9 @@ export async function getStaticProps() {
return {
props: {
heroImages,
products,
collections,
categories
products: allProducts,
collections: allCollections,
categories: allCategories
},
// Перегенерация страницы каждые 10 минут
revalidate: 600,

338
pages/product/[slug].tsx Normal file
View File

@ -0,0 +1,338 @@
import { useState } from 'react';
import Head from 'next/head';
import Image from 'next/image';
import Link from 'next/link';
import { GetStaticProps, GetStaticPaths } from 'next';
import { useRouter } from 'next/router';
import Header from '../../components/Header';
import Footer from '../../components/Footer';
import { Product, products, getProductBySlug, getSimilarProducts, formatPrice } from '../../data/products';
import { Category, getCategoryById } from '../../data/categories';
import { Heart, ShoppingBag, ChevronLeft, ChevronRight } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
interface ProductPageProps {
product: Product;
category: Category;
similarProducts: Product[];
}
export default function ProductPage({ product, category, similarProducts }: ProductPageProps) {
const router = useRouter();
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [isFavorite, setIsFavorite] = useState(false);
const [quantity, setQuantity] = useState(1);
const [hoveredProduct, setHoveredProduct] = useState<number | null>(null);
// Если страница еще загружается, показываем заглушку
if (router.isFallback) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-gray-900"></div>
</div>
);
}
// Функция для переключения изображений
const nextImage = () => {
setCurrentImageIndex((prev) => (prev === product.images.length - 1 ? 0 : prev + 1));
};
const prevImage = () => {
setCurrentImageIndex((prev) => (prev === 0 ? product.images.length - 1 : prev - 1));
};
// Функция для добавления/удаления товара из избранного
const toggleFavorite = () => {
setIsFavorite(!isFavorite);
};
// Функция для изменения количества товара
const incrementQuantity = () => {
setQuantity((prev) => prev + 1);
};
const decrementQuantity = () => {
if (quantity > 1) {
setQuantity((prev) => prev - 1);
}
};
// Функция для добавления товара в корзину
const addToCart = () => {
// Здесь будет логика добавления товара в корзину
alert(`Товар "${product.name}" добавлен в корзину в количестве ${quantity} шт.`);
};
return (
<div className="min-h-screen bg-white font-['Arimo']">
<Head>
<title>{product.name} | Brand Store</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Header />
<main className="max-w-7xl mx-auto px-4 py-12 md:px-8">
<div className="mb-8">
<Link href={`/category/${category.slug}`} className="text-gray-600 hover:text-black transition-colors">
{category.name}
</Link>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Галерея изображений */}
<div className="relative">
<div className="relative aspect-[3/4] overflow-hidden rounded-xl">
<AnimatePresence mode="wait">
<motion.div
key={currentImageIndex}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="absolute inset-0"
>
<Image
src={product.images[currentImageIndex]}
alt={product.name}
fill
className="object-cover"
priority
quality={95}
/>
</motion.div>
</AnimatePresence>
{/* Кнопки навигации по галерее */}
{product.images.length > 1 && (
<>
<button
onClick={prevImage}
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-white/50 hover:bg-white/70 p-3 rounded-full transition-all z-10"
aria-label="Предыдущее изображение"
>
<ChevronLeft className="w-5 h-5 text-black" />
</button>
<button
onClick={nextImage}
className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-white/50 hover:bg-white/70 p-3 rounded-full transition-all z-10"
aria-label="Следующее изображение"
>
<ChevronRight className="w-5 h-5 text-black" />
</button>
</>
)}
</div>
{/* Миниатюры изображений */}
{product.images.length > 1 && (
<div className="flex mt-4 space-x-2 overflow-x-auto">
{product.images.map((image, index) => (
<button
key={index}
onClick={() => setCurrentImageIndex(index)}
className={`relative w-20 h-20 rounded-md overflow-hidden ${
index === currentImageIndex ? 'ring-2 ring-black' : 'opacity-70 hover:opacity-100'
}`}
aria-label={`Изображение ${index + 1}`}
>
<Image src={image} alt={`${product.name} - изображение ${index + 1}`} fill className="object-cover" />
</button>
))}
</div>
)}
</div>
{/* Информация о товаре */}
<div>
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-3xl font-bold font-['Playfair_Display']"
>
{product.name}
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.1 }}
className="text-2xl font-bold mt-4"
>
{formatPrice(product.price)}
</motion.p>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="mt-6"
>
<h2 className="text-lg font-medium mb-2">Описание</h2>
<p className="text-gray-600">{product.description}</p>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="mt-8"
>
<h2 className="text-lg font-medium mb-2">Количество</h2>
<div className="flex items-center border border-gray-300 rounded-md w-fit">
<button
onClick={decrementQuantity}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 transition-colors"
aria-label="Уменьшить количество"
disabled={quantity <= 1}
>
-
</button>
<span className="px-4 py-2 border-l border-r border-gray-300">{quantity}</span>
<button
onClick={incrementQuantity}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 transition-colors"
aria-label="Увеличить количество"
>
+
</button>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="mt-8 flex flex-col sm:flex-row gap-4"
>
<button
onClick={addToCart}
className="flex items-center justify-center gap-2 bg-black text-white px-8 py-3 rounded-md hover:bg-gray-800 transition-colors"
>
<ShoppingBag className="w-5 h-5" />
Добавить в корзину
</button>
<button
onClick={toggleFavorite}
className={`flex items-center justify-center gap-2 px-8 py-3 rounded-md border transition-colors ${
isFavorite
? 'border-red-500 text-red-500 hover:bg-red-50'
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
>
<Heart className={isFavorite ? 'w-5 h-5 fill-red-500' : 'w-5 h-5'} />
{isFavorite ? 'В избранном' : 'В избранное'}
</button>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.5 }}
className="mt-8 border-t border-gray-200 pt-8"
>
<h2 className="text-lg font-medium mb-2">Категория</h2>
<Link href={`/category/${category.slug}`} className="text-gray-600 hover:text-black transition-colors">
{category.name}
</Link>
</motion.div>
</div>
</div>
{/* Похожие товары */}
{similarProducts.length > 0 && (
<section className="mt-16">
<h2 className="text-2xl font-bold mb-8 font-['Playfair_Display']">Похожие товары</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
{similarProducts.map((similarProduct) => (
<motion.div
key={similarProduct.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: similarProduct.id * 0.05 }}
whileHover={{ y: -5, transition: { duration: 0.2 } }}
>
<Link
href={`/product/${similarProduct.slug}`}
className="block h-full"
onMouseEnter={() => setHoveredProduct(similarProduct.id)}
onMouseLeave={() => setHoveredProduct(null)}
>
<div className="relative overflow-hidden rounded-xl">
<div className="aspect-[3/4] relative overflow-hidden rounded-xl">
<Image
src={
hoveredProduct === similarProduct.id && similarProduct.images.length > 1
? similarProduct.images[1]
: similarProduct.images[0]
}
alt={similarProduct.name}
fill
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw"
className="object-cover transition-all duration-500 group-hover:scale-105"
/>
{similarProduct.isNew && (
<span className="absolute top-4 left-4 bg-black text-white text-sm py-1 px-3 rounded">
Новинка
</span>
)}
</div>
</div>
<div className="mt-4">
<h3 className="text-lg font-medium">{similarProduct.name}</h3>
<p className="mt-1 text-lg font-bold">{formatPrice(similarProduct.price)} </p>
</div>
</Link>
</motion.div>
))}
</div>
</section>
)}
</main>
<Footer />
</div>
);
}
export const getStaticPaths: GetStaticPaths = async () => {
const paths = products.map((product) => ({
params: { slug: product.slug },
}));
return {
paths,
fallback: 'blocking',
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const slug = params?.slug as string;
const product = getProductBySlug(slug);
if (!product) {
return {
notFound: true,
};
}
const category = getCategoryById(product.categoryId);
const similarProducts = getSimilarProducts(product.id);
if (!category) {
return {
notFound: true,
};
}
return {
props: {
product,
category,
similarProducts,
},
revalidate: 600, // Перегенерация страницы каждые 10 минут
};
};

View File

@ -21,6 +21,16 @@
.text-balance {
text-wrap: balance;
}
/* Скрываем скроллбар для слайдера продуктов */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
}
html,