add new pages

This commit is contained in:
Nikita 2026-09-27 11:27:54 +05:00
parent dce8b7eab5
commit 2ede0d8641
21 changed files with 3014 additions and 44 deletions

View file

@ -0,0 +1,282 @@
[
{
"model": "api.category",
"pk": 1,
"fields": {
"name": "New Arrivals",
"slug": "new-arrivals"
}
},
{
"model": "api.category",
"pk": 2,
"fields": {
"name": "Top Selling",
"slug": "top-selling"
}
},
{
"model": "api.category",
"pk": 3,
"fields": {
"name": "On Sale",
"slug": "on-sale"
}
},
{
"model": "api.product",
"pk": 1,
"fields": {
"category": 1,
"name": "T-shirt with Tape Details",
"slug": "t-shirt-with-tape-details",
"description": "A relaxed-fit t-shirt featuring sporty tape details along the shoulders and sleeves. Crafted from 100% breathable cotton for all-day comfort. Perfect for casual outings or layering under a jacket.",
"image": "products/t-shirt.png",
"rating": "4.5",
"price": "120.00",
"old_price": null
}
},
{
"model": "api.product",
"pk": 2,
"fields": {
"category": 1,
"name": "Skinny Fit Jeans",
"slug": "skinny-fit-jeans",
"description": "Slim silhouette jeans with a mid-rise waist, crafted from stretch denim that moves with you. The classic five-pocket design and clean finish make them effortlessly versatile from day to night.",
"image": "products/skinny.png",
"rating": "3.5",
"price": "240.00",
"old_price": "260.00"
}
},
{
"model": "api.product",
"pk": 3,
"fields": {
"category": 1,
"name": "Checkered Shirt",
"slug": "checkered-shirt",
"description": "A timeless checkered pattern shirt in a relaxed regular fit. Woven from lightweight cotton flannel, it's ideal for layering or wearing open over a tee. Available in classic colour combinations.",
"image": "products/checkered.png",
"rating": "4.5",
"price": "180.00",
"old_price": null
}
},
{
"model": "api.product",
"pk": 4,
"fields": {
"category": 1,
"name": "Sleeve Striped T-Shirt",
"slug": "sleeve-striped-t-shirt",
"description": "A modern spin on a classic design — bold stripes run along the sleeves while the body stays clean. Made from soft jersey fabric with a relaxed fit and a crew neckline. Effortlessly cool.",
"image": "products/sleeve.png",
"rating": "4.5",
"price": "130.00",
"old_price": "160.00"
}
},
{
"model": "api.product",
"pk": 5,
"fields": {
"category": 2,
"name": "Vertical Striped Shirt",
"slug": "vertical-striped-shirt",
"description": "Classic vertical stripes give this button-up shirt a polished, elongating silhouette. Made from a crisp cotton-blend fabric that keeps its shape all day. Great for smart-casual occasions.",
"image": "products/Striped.png",
"rating": "5.0",
"price": "212.00",
"old_price": "232.00"
}
},
{
"model": "api.product",
"pk": 6,
"fields": {
"category": 2,
"name": "Courage Graphic T-shirt",
"slug": "courage-graphic-t-shirt",
"description": "Express yourself with this bold graphic tee. Features a large front print on heavyweight cotton. The relaxed fit and ribbed crew neck make it a wardrobe staple for any creative individual.",
"image": "products/Courage.png",
"rating": "4.0",
"price": "145.00",
"old_price": null
}
},
{
"model": "api.product",
"pk": 7,
"fields": {
"category": 2,
"name": "Loose Fit Bermuda Shorts",
"slug": "loose-fit-bermuda-shorts",
"description": "Comfortable, relaxed-fit Bermuda shorts perfect for warm days. Features side pockets, a drawstring waist, and a clean hem. Made from lightweight twill fabric that keeps you cool and stylish.",
"image": "products/Loose.png",
"rating": "3.0",
"price": "80.00",
"old_price": null
}
},
{
"model": "api.product",
"pk": 8,
"fields": {
"category": 2,
"name": "Faded Skinny Jeans",
"slug": "faded-skinny-jeans",
"description": "Faded wash skinny jeans with a worn-in look that gets better over time. Slim through the hip and thigh, with a tapered leg opening. Made from a stretch-denim blend for maximum comfort.",
"image": "products/Faded.png",
"rating": "4.5",
"price": "210.00",
"old_price": null
}
},
{
"model": "api.product",
"pk": 9,
"fields": {
"category": 3,
"name": "Classic White Polo",
"slug": "classic-white-polo",
"description": "A wardrobe essential — the classic pique polo in crisp white. Features a two-button placket, ribbed collar, and cuffed short sleeves. Made from breathable cotton for smart-casual looks.",
"image": "products/t-shirt.png",
"rating": "4.5",
"price": "95.00",
"old_price": "130.00"
}
},
{
"model": "api.product",
"pk": 10,
"fields": {
"category": 3,
"name": "Slim Chino Pants",
"slug": "slim-chino-pants",
"description": "Sharp, versatile slim chinos in a refined cotton blend. Sits slightly below the waist with a neat taper from the knee to the leg opening. A smart alternative to jeans for any occasion.",
"image": "products/Faded.png",
"rating": "4.0",
"price": "110.00",
"old_price": "149.00"
}
},
{
"model": "api.product",
"pk": 11,
"fields": {
"category": 3,
"name": "Graphic Print Hoodie",
"slug": "graphic-print-hoodie",
"description": "A heavyweight pullover hoodie with a bold chest graphic. Features a lined hood with adjustable drawcords, a kangaroo pocket, and ribbed cuffs. Perfect for streetwear layering.",
"image": "products/Courage.png",
"rating": "4.5",
"price": "175.00",
"old_price": "220.00"
}
},
{
"model": "api.product",
"pk": 12,
"fields": {
"category": 3,
"name": "Relaxed Linen Shorts",
"slug": "relaxed-linen-shorts",
"description": "Breathable linen shorts with a relaxed fit and elasticated waistband. The natural texture adds a laid-back summer aesthetic, while the side pockets provide practicality.",
"image": "products/Loose.png",
"rating": "3.5",
"price": "65.00",
"old_price": "89.00"
}
},
{
"model": "reviews.review",
"pk": 1,
"fields": {
"customer_name": "Sarah M.",
"rating": 5,
"comment": "I'm blown away by the quality and style of the clothes I received from Shop.co. From casual wear to elegant dresses, every piece I've bought has exceeded my expectations.",
"verified": true,
"created_at": "2024-08-14T10:30:00Z"
}
},
{
"model": "reviews.review",
"pk": 2,
"fields": {
"customer_name": "Alex K.",
"rating": 5,
"comment": "Finding clothes that align with my personal style used to be a challenge until I discovered Shop.co. The range of options they offer is truly remarkable, catering to a variety of tastes and occasions.",
"verified": true,
"created_at": "2024-08-12T14:00:00Z"
}
},
{
"model": "reviews.review",
"pk": 3,
"fields": {
"customer_name": "James L.",
"rating": 5,
"comment": "As someone who's always on the lookout for unique fashion pieces, I'm thrilled to have stumbled upon Shop.co. The selection of clothes is not only diverse but also on-point with the latest trends.",
"verified": true,
"created_at": "2024-08-10T09:00:00Z"
}
},
{
"model": "reviews.review",
"pk": 4,
"fields": {
"customer_name": "Moana H.",
"rating": 4,
"comment": "I absolutely love this t-shirt! The design is unique and the fabric feels so comfortable. As a fellow designer, I appreciate the attention to detail. It's become my favorite go-to shirt.",
"verified": true,
"created_at": "2024-08-08T11:00:00Z"
}
},
{
"model": "reviews.review",
"pk": 5,
"fields": {
"customer_name": "Ethan P.",
"rating": 5,
"comment": "I'm not just wearing a piece of clothing; I'm wearing a piece of art. The intricate details and vibrant colours have made it my go-to source for unique and stylish fashion.",
"verified": true,
"created_at": "2024-08-06T16:00:00Z"
}
},
{
"model": "reviews.review",
"pk": 6,
"fields": {
"customer_name": "Emily R.",
"rating": 5,
"comment": "The fabric quality is outstanding and the delivery was super fast. I ordered three pieces and all of them fit perfectly. Will definitely be ordering again!",
"verified": true,
"created_at": "2024-08-04T08:00:00Z"
}
},
{
"model": "reviews.review",
"pk": 7,
"fields": {
"customer_name": "Marcus T.",
"rating": 4,
"comment": "Great selection and competitive prices. The packaging was eco-friendly which I really appreciate. The shirt I got is exactly as described in the photos.",
"verified": true,
"created_at": "2024-08-02T12:00:00Z"
}
},
{
"model": "reviews.review",
"pk": 8,
"fields": {
"customer_name": "Olivia N.",
"rating": 5,
"comment": "Shop.co has completely changed how I shop for clothes online. The size guide is accurate, the return process is seamless, and the quality is consistently excellent across all their collections.",
"verified": true,
"created_at": "2024-07-30T15:00:00Z"
}
}
]

View file

@ -1,6 +1,11 @@
from rest_framework import serializers
from .models import Product, Category
from .models import Product
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('id', 'name', 'slug')
class ProductSerializer(serializers.ModelSerializer):

View file

@ -1,17 +1,18 @@
from django.urls import path
from .views import hello, ProductListView, ProductDetailView
from .views import hello, ProductListView, ProductDetailView, CategoryListView
from reviews.views import ReviewListView, LatestReviewsView, NewsletterCreateView
urlpatterns = [
path("hello/", hello),
path("products/", ProductListView.as_view()),
path("categories/", CategoryListView.as_view()),
# Reviews
path("reviews/", ReviewListView.as_view()),
path("reviews/latest/", LatestReviewsView.as_view()),
# Newsletter
path("newsletter/", NewsletterCreateView.as_view()),
path("products/<slug:slug>/", ProductDetailView.as_view()),
path("products/<slug:slug>/", ProductDetailView.as_view()),
]

View file

@ -1,10 +1,10 @@
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework.generics import ListAPIView
from rest_framework import generics
from django.db.models import Q
from .models import Product
from .serializers import ProductSerializer
from .models import Product, Category
from .serializers import ProductSerializer, CategorySerializer
@api_view(["GET"])
@ -14,13 +14,43 @@ def hello(request):
})
class ProductListView(ListAPIView):
queryset = Product.objects.all()
class ProductListView(generics.ListAPIView):
serializer_class = ProductSerializer
def get_queryset(self):
qs = Product.objects.select_related('category').all()
# ?category=on-sale or ?category=new-arrivals etc.
category_slug = self.request.query_params.get('category')
if category_slug:
# Special virtual filter: "on-sale" = products that have old_price set
if category_slug == 'on-sale':
qs = qs.filter(old_price__isnull=False)
else:
qs = qs.filter(category__slug=category_slug)
# ?sale=true → only products with discount
sale = self.request.query_params.get('sale')
if sale == 'true':
qs = qs.filter(old_price__isnull=False)
# ?search=jeans
search = self.request.query_params.get('search')
if search:
qs = qs.filter(
Q(name__icontains=search) | Q(description__icontains=search)
)
return qs
class ProductDetailView(generics.RetrieveAPIView):
"""Получить один товар по slug"""
queryset = Product.objects.all()
serializer_class = ProductSerializer
lookup_field = 'slug'
lookup_field = 'slug'
class CategoryListView(generics.ListAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer

Binary file not shown.

View file

@ -1,17 +1,23 @@
import { Routes, Route } from 'react-router-dom'
import { CartProvider } from './context/CartContext' // <-- Импортируем провайдер
import { CartProvider } from './context/CartContext'
import Home from './pages/Home/Home'
import ProductDetail from './pages/ProductDetail/ProductDetail'
import Cart from './pages/Cart/Cart' // Убедитесь, что путь правильный
import Cart from './pages/Cart/Cart'
import OnSale from './pages/OnSale/OnSale'
import NewArrivalsPage from './pages/NewArrivals/NewArrivalsPage'
import BrandsPage from './pages/Brands/BrandsPage'
function App() {
return (
<CartProvider> {/* <-- Оборачиваем всё приложение */}
<CartProvider>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product/:slug" element={<ProductDetail />} />
<Route path="/cart" element={<Cart />} />
<Route path="/sale" element={<OnSale />} />
<Route path="/new-arrivals" element={<NewArrivalsPage />} />
<Route path="/brands" element={<BrandsPage />} />
</Routes>
</CartProvider>
)

View file

@ -34,6 +34,18 @@
overflow: hidden;
border-radius: 14px;
background: #f1efef;
display: flex;
align-items: center;
justify-content: center;
}
.placeholderIcon {
font-size: 64px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
}
.image {

View file

@ -1,5 +1,4 @@
import { useEffect, useState } from 'react'
import styles from './DressStyle.module.css'
type DressStyleItem = {
@ -8,6 +7,18 @@ type DressStyleItem = {
image: string
}
// Статические fallback данные если API пустой
const FALLBACK_STYLES: DressStyleItem[] = [
{ id: 1, name: 'Casual', image: '' },
{ id: 2, name: 'Formal', image: '' },
{ id: 3, name: 'Party', image: '' },
{ id: 4, name: 'Gym', image: '' },
]
// Цвета для fallback карточек
const CARD_COLORS = ['#F5E6D3', '#D3E6F5', '#F5D3E6', '#D3F5D3']
const CARD_ICONS = ['☀️', '👔', '🎉', '💪']
function DressStyle() {
const [dressStyles, setDressStyles] = useState<DressStyleItem[]>([])
const [isLoading, setIsLoading] = useState(true)
@ -16,7 +27,14 @@ function DressStyle() {
fetch('http://127.0.0.1:8000/api/dress-styles/')
.then((response) => response.json())
.then((data) => {
setDressStyles(data)
if (Array.isArray(data) && data.length > 0) {
setDressStyles(data)
} else {
setDressStyles(FALLBACK_STYLES)
}
})
.catch(() => {
setDressStyles(FALLBACK_STYLES)
})
.finally(() => {
setIsLoading(false)
@ -33,13 +51,23 @@ function DressStyle() {
<h2 className={styles.title}>BROWSE BY DRESS STYLE</h2>
<div className={styles.styles}>
{dressStyles.map((dressStyle) => (
<article key={dressStyle.id} className={styles.card}>
<img
src={dressStyle.image}
alt={dressStyle.name}
className={styles.image}
/>
{dressStyles.map((dressStyle, idx) => (
<article
key={dressStyle.id}
className={styles.card}
style={!dressStyle.image ? { backgroundColor: CARD_COLORS[idx % CARD_COLORS.length] } : undefined}
>
{dressStyle.image ? (
<img
src={dressStyle.image}
alt={dressStyle.name}
className={styles.image}
/>
) : (
<div className={styles.placeholderIcon}>
{CARD_ICONS[idx % CARD_ICONS.length]}
</div>
)}
<h3 className={styles.name}>{dressStyle.name}</h3>
</article>

View file

@ -50,6 +50,92 @@
.arrow {
font-size: 9px;
line-height: 1;
transition: transform 0.25s ease;
}
.arrowOpen {
transform: rotate(180deg);
}
.navigationItemActive {
opacity: 0.7;
}
/* ── Shop Dropdown ── */
.shopWrapper {
position: relative;
}
.dropdown {
position: absolute;
top: calc(100% + 16px);
left: 50%;
transform: translateX(-50%);
z-index: 200;
min-width: 340px;
background: #fff;
border: 1px solid #E8E8E8;
border-radius: 20px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
padding: 20px;
animation: dropdownIn 0.2s ease;
}
@keyframes dropdownIn {
from { opacity: 0; transform: translateX(-50%) translateY(-8px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
/* Pointer arrow */
.dropdown::before {
content: '';
position: absolute;
top: -7px;
left: 50%;
transform: translateX(-50%);
width: 14px;
height: 14px;
background: #fff;
border-left: 1px solid #E8E8E8;
border-top: 1px solid #E8E8E8;
rotate: 45deg;
}
.dropdownGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.dropdownSection {
display: flex;
flex-direction: column;
gap: 4px;
}
.dropdownSectionTitle {
margin: 0 0 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
color: #999;
}
.dropdownLink {
display: block;
padding: 8px 12px;
border-radius: 10px;
font-size: 14px;
font-weight: 500;
color: #111;
text-decoration: none;
transition: background 0.15s ease, color 0.15s ease;
}
.dropdownLink:hover {
background: #F5F5F5;
color: #000;
}
.search {

View file

@ -1,20 +1,31 @@
import { useState } from 'react'
import { useState, useRef, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useCart } from '../../context/CartContext' // <-- ВАЖНО: импорт из контекста, а не из hooks
import { useCart } from '../../context/CartContext'
import { navigationItems } from '../../shared/navigation/navigation.data'
import styles from './Header.module.css'
function Header() {
const [isShopOpen, setIsShopOpen] = useState(false)
const shopRef = useRef<HTMLDivElement>(null)
// Получаем актуальные данные корзины из глобального контекста
const { cart } = useCart()
// Считаем общее количество товаров в реальном времени
// Если cart или items еще не загрузились, вернет 0
const totalItems = cart?.items?.reduce((sum: number, item: any) => sum + item.quantity, 0) ?? 0
// Закрываем дропдаун при клике вне его
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (shopRef.current && !shopRef.current.contains(e.target as Node)) {
setIsShopOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
return (
<header className={styles.header}>
<div className="container">
@ -25,23 +36,65 @@ function Header() {
<nav className={styles.navigation}>
{navigationItems.map((item) =>
item.children ? (
<button
// Shop с выпадающим меню
<div
key={item.label}
type="button"
className={styles.navigationItem}
onClick={() => setIsShopOpen(!isShopOpen)}
className={styles.shopWrapper}
ref={shopRef}
>
{item.label}
<span className={styles.arrow}>▼</span>
</button>
<button
type="button"
className={`${styles.navigationItem} ${isShopOpen ? styles.navigationItemActive : ''}`}
onClick={() => setIsShopOpen(!isShopOpen)}
aria-haspopup="true"
aria-expanded={isShopOpen}
>
{item.label}
<span className={`${styles.arrow} ${isShopOpen ? styles.arrowOpen : ''}`}>▼</span>
</button>
{isShopOpen && (
<div className={styles.dropdown}>
<div className={styles.dropdownGrid}>
{/* Категории */}
<div className={styles.dropdownSection}>
<p className={styles.dropdownSectionTitle}>Categories</p>
{item.children.map((child) => (
<Link
key={child.href}
to={child.href!}
className={styles.dropdownLink}
onClick={() => setIsShopOpen(false)}
>
{child.label}
</Link>
))}
</div>
{/* Быстрые ссылки */}
<div className={styles.dropdownSection}>
<p className={styles.dropdownSectionTitle}>Discover</p>
<Link to="/new-arrivals" className={styles.dropdownLink} onClick={() => setIsShopOpen(false)}>
✨ New Arrivals
</Link>
<Link to="/sale" className={styles.dropdownLink} onClick={() => setIsShopOpen(false)}>
🔥 On Sale
</Link>
<Link to="/brands" className={styles.dropdownLink} onClick={() => setIsShopOpen(false)}>
🏷️ Brands
</Link>
</div>
</div>
</div>
)}
</div>
) : (
<a
<Link
key={item.href}
href={item.href}
to={item.href!}
className={styles.navigationItem}
>
{item.label}
</a>
</Link>
),
)}
</nav>
@ -85,7 +138,6 @@ function Header() {
<circle cx="17" cy="20" r="1.5" fill="currentColor" />
</svg>
{/* Показываем бейдж только если товаров > 0 */}
{totalItems > 0 && (
<span className={styles.cartCount}>
{totalItems > 99 ? '99+' : totalItems}

View file

@ -6,6 +6,7 @@ import ProductsCard from '../ProductCard/ProductCard'
type Product = {
id: number
name: string
slug: string
image: string
rating: number
price: string

View file

@ -6,6 +6,7 @@ import ProductsCard from '../ProductCard/ProductCard'
type Product = {
id: number
name: string
slug: string
image: string
rating: number
price: string

View file

@ -1,4 +1,4 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
interface CartContextType {
cart: any

View file

@ -0,0 +1,451 @@
.page {
min-height: 80vh;
}
/* ─── Hero ─────────────────────── */
.hero {
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
padding: 72px 24px 60px;
color: #fff;
text-align: center;
}
.heroInner {
max-width: 720px;
margin: 0 auto;
}
.heroTitle {
margin: 0 0 20px;
font-size: 64px;
font-weight: 900;
letter-spacing: -3px;
line-height: 1;
}
.heroSub {
margin: 0 0 40px;
font-size: 17px;
color: rgba(255,255,255,0.62);
line-height: 1.65;
}
/* Stats */
.stats {
display: flex;
align-items: center;
justify-content: center;
gap: 40px;
flex-wrap: wrap;
}
.stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.stat strong {
font-size: 32px;
font-weight: 800;
letter-spacing: -1px;
color: #fff;
}
.stat span {
font-size: 12px;
color: rgba(255,255,255,0.5);
text-transform: uppercase;
letter-spacing: 1px;
}
/* ─── Container ─────────────────────── */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 24px 80px;
}
.breadcrumbs {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 32px;
font-size: 14px;
color: #777;
}
.breadcrumbLink {
color: #777;
text-decoration: none;
transition: color 0.2s;
}
.breadcrumbLink:hover { color: #000; }
.sep { color: #bbb; }
/* ─── Search ─────────────────────── */
.searchWrapper {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 40px;
}
.searchBox {
flex: 1;
display: flex;
align-items: center;
gap: 10px;
height: 52px;
padding: 0 20px;
border: 1.5px solid #e8e8e8;
border-radius: 14px;
background: #fff;
transition: border-color 0.2s;
}
.searchBox:focus-within { border-color: #000; }
.searchIcon {
width: 20px;
height: 20px;
color: #999;
flex-shrink: 0;
}
.searchInput {
flex: 1;
border: 0;
outline: none;
font-family: inherit;
font-size: 15px;
color: #111;
background: transparent;
}
.searchInput::placeholder { color: #bbb; }
.searchCount {
font-size: 14px;
color: #888;
white-space: nowrap;
flex-shrink: 0;
margin: 0;
}
/* ─── Section ─────────────────────── */
.section {
margin-bottom: 52px;
}
.sectionTitle {
margin: 0 0 24px;
font-size: 22px;
font-weight: 800;
letter-spacing: -0.5px;
color: #000;
}
/* ─── Featured Cards (top 3) ─────────────────────── */
.featuredGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.featuredCard {
padding: 28px 28px 24px;
background: var(--bg);
border-radius: 24px;
border: 1px solid transparent;
text-decoration: none;
color: #000;
display: flex;
flex-direction: column;
gap: 10px;
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
}
.featuredCard:hover {
transform: translateY(-6px);
box-shadow: 0 16px 40px rgba(0,0,0,0.1);
border-color: var(--accent, #ccc);
}
.featuredCardTop {
display: flex;
align-items: center;
justify-content: space-between;
}
.cardEmoji { font-size: 36px; }
.featuredBadge {
display: inline-block;
padding: 4px 12px;
border-radius: 30px;
background: #000;
color: #fff;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.cardName {
margin: 0;
font-size: 22px;
font-weight: 900;
letter-spacing: -0.5px;
}
.cardTagline {
margin: 0;
font-size: 13px;
color: #555;
line-height: 1.5;
}
.cardFooter {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
padding-top: 12px;
border-top: 1px solid rgba(0,0,0,0.08);
}
.cardItems {
font-size: 13px;
font-weight: 600;
color: #777;
}
.cardArrow {
font-size: 13px;
font-weight: 700;
color: var(--accent, #000);
opacity: 0;
transform: translateX(-4px);
transition: opacity 0.2s, transform 0.2s;
}
.featuredCard:hover .cardArrow {
opacity: 1;
transform: translateX(0);
}
/* ─── All Brands Grid ─────────────────────── */
.brandsGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.brandCard {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 20px;
background: var(--bg);
border-radius: 18px;
border: 1px solid transparent;
text-decoration: none;
color: #000;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
}
.brandCard:hover {
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
border-color: var(--accent, #ccc);
}
.brandEmoji { font-size: 28px; flex-shrink: 0; }
.brandInfo { flex: 1; min-width: 0; }
.brandName {
margin: 0 0 3px;
font-size: 16px;
font-weight: 800;
letter-spacing: -0.3px;
}
.brandTagline {
margin: 0 0 5px;
font-size: 12px;
color: #666;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
.brandItems {
font-size: 11px;
font-weight: 600;
color: var(--accent, #888);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.brandArrow {
font-size: 16px;
opacity: 0;
transform: translateX(-4px);
transition: opacity 0.2s, transform 0.2s;
flex-shrink: 0;
color: var(--accent, #000);
}
.brandCard:hover .brandArrow {
opacity: 1;
transform: translateX(0);
}
/* ─── Empty ─────────────────────── */
.empty {
text-align: center;
padding: 80px 40px;
border: 1px solid #e8e8e8;
border-radius: 24px;
margin-bottom: 48px;
}
.emptyIcon { font-size: 52px; margin-bottom: 20px; }
.empty h3 { margin: 0 0 8px; font-size: 20px; font-weight: 700; }
.empty p { margin: 0 0 24px; color: #666; }
.emptyBtn {
padding: 12px 28px;
border: 0;
border-radius: 30px;
background: #000;
color: #fff;
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.emptyBtn:hover { background: #333; }
/* ─── Partner CTA ─────────────────────── */
.partnerCta {
display: grid;
grid-template-columns: 1fr auto;
gap: 48px;
align-items: center;
padding: 48px 52px;
background: #f7f7f7;
border: 1px solid #e8e8e8;
border-radius: 28px;
}
.partnerTitle {
margin: 0 0 12px;
font-size: 28px;
font-weight: 800;
letter-spacing: -1px;
color: #000;
}
.partnerText {
margin: 0 0 28px;
font-size: 15px;
color: #555;
line-height: 1.6;
max-width: 420px;
}
.partnerActions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.partnerBtn {
display: inline-flex;
align-items: center;
height: 48px;
padding: 0 28px;
border-radius: 30px;
background: #000;
color: #fff;
text-decoration: none;
font-size: 14px;
font-weight: 700;
transition: background 0.2s, transform 0.2s;
}
.partnerBtn:hover { background: #222; transform: translateY(-2px); }
.partnerBtnOutline {
display: inline-flex;
align-items: center;
height: 48px;
padding: 0 24px;
border-radius: 30px;
border: 1.5px solid #000;
color: #000;
text-decoration: none;
font-size: 14px;
font-weight: 600;
transition: all 0.2s;
}
.partnerBtnOutline:hover { background: #000; color: #fff; }
.partnerBenefits {
display: flex;
flex-direction: column;
gap: 14px;
}
.partnerBenefit {
display: flex;
align-items: center;
gap: 10px;
font-size: 15px;
font-weight: 600;
color: #333;
white-space: nowrap;
}
.partnerBenefit span:first-child {
width: 22px;
height: 22px;
border-radius: 50%;
background: #000;
color: #fff;
font-size: 11px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* ─── Responsive ─────────────────────── */
@media (max-width: 1024px) {
.featuredGrid { grid-template-columns: repeat(2, 1fr); }
.brandsGrid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
.heroTitle { font-size: 42px; }
.stats { gap: 24px; }
.partnerCta { grid-template-columns: 1fr; padding: 32px 28px; }
}
@media (max-width: 600px) {
.featuredGrid { grid-template-columns: 1fr; }
.brandsGrid { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
.heroTitle { font-size: 32px; }
}

View file

@ -0,0 +1,306 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import AnnouncementBar from '../../components/AnnouncementBar/AnnouncementBar'
import Header from '../../components/Header/Header'
import Footer from '../../components/Footer/Footer'
import styles from './BrandsPage.module.css'
const BRANDS = [
{
name: 'VERSACE',
tagline: 'Italian luxury fashion house founded in 1978',
emoji: '💎',
bg: '#fdf6e3',
accent: '#c9a84c',
items: '120+ items',
href: '/shop?brand=versace',
featured: true,
},
{
name: 'ZARA',
tagline: 'Contemporary fashion for every occasion',
emoji: '🛍️',
bg: '#e8f4fd',
accent: '#2980b9',
items: '340+ items',
href: '/shop?brand=zara',
featured: true,
},
{
name: 'GUCCI',
tagline: 'The House of Gucci — iconic luxury style',
emoji: '👑',
bg: '#fdecea',
accent: '#c0392b',
items: '85+ items',
href: '/shop?brand=gucci',
featured: true,
},
{
name: 'PRADA',
tagline: 'Modern elegance and refined Italian craftsmanship',
emoji: '✨',
bg: '#eafdf5',
accent: '#27ae60',
items: '60+ items',
href: '/shop?brand=prada',
featured: false,
},
{
name: 'Calvin Klein',
tagline: 'Minimal. Modern. Quintessentially American.',
emoji: '🖤',
bg: '#f5f0ff',
accent: '#8e44ad',
items: '200+ items',
href: '/shop?brand=calvin-klein',
featured: false,
},
{
name: 'H&M',
tagline: 'Fashion & quality at the best price',
emoji: '🌿',
bg: '#f0fff4',
accent: '#16a085',
items: '500+ items',
href: '/shop?brand=hm',
featured: false,
},
{
name: 'Nike',
tagline: 'Just Do It — performance meets style',
emoji: '👟',
bg: '#e8f0fe',
accent: '#2c3e50',
items: '280+ items',
href: '/shop?brand=nike',
featured: false,
},
{
name: 'Adidas',
tagline: 'Impossible is Nothing — sportswear icon',
emoji: '⚡',
bg: '#fff3e0',
accent: '#e67e22',
items: '190+ items',
href: '/shop?brand=adidas',
featured: false,
},
{
name: 'Levi\'s',
tagline: 'The original American denim since 1853',
emoji: '👖',
bg: '#e3f2fd',
accent: '#1565c0',
items: '150+ items',
href: '/shop?brand=levis',
featured: false,
},
{
name: 'Tommy Hilfiger',
tagline: 'Classic American cool with a preppy twist',
emoji: '⛵',
bg: '#fce4ec',
accent: '#c62828',
items: '110+ items',
href: '/shop?brand=tommy',
featured: false,
},
{
name: 'Ralph Lauren',
tagline: 'The American dream, tailored to perfection',
emoji: '🐎',
bg: '#f3e5f5',
accent: '#6a1b9a',
items: '95+ items',
href: '/shop?brand=ralph-lauren',
featured: false,
},
{
name: 'Supreme',
tagline: 'Streetwear royalty — limited, exclusive, iconic',
emoji: '🔴',
bg: '#ffebee',
accent: '#b71c1c',
items: '40+ items',
href: '/shop?brand=supreme',
featured: false,
},
]
const STATS = [
{ num: '200+', label: 'Global Brands' },
{ num: '50K+', label: 'Products' },
{ num: '2M+', label: 'Happy Customers' },
{ num: '150+', label: 'Countries Shipped' },
]
function BrandsPage() {
const [search, setSearch] = useState('')
const filtered = BRANDS.filter((b) =>
b.name.toLowerCase().includes(search.toLowerCase())
)
const featured = filtered.filter((b) => b.featured)
const regular = filtered.filter((b) => !b.featured)
return (
<>
<AnnouncementBar />
<Header />
<main className={styles.page}>
{/* ─── Hero ─── */}
<div className={styles.hero}>
<div className={styles.heroInner}>
<h1 className={styles.heroTitle}>OUR BRANDS</h1>
<p className={styles.heroSub}>
From luxury maisons to streetwear icons — discover the world's most celebrated fashion brands, all in one place.
</p>
{/* Stats */}
<div className={styles.stats}>
{STATS.map((s) => (
<div key={s.label} className={styles.stat}>
<strong>{s.num}</strong>
<span>{s.label}</span>
</div>
))}
</div>
</div>
</div>
<div className={styles.container}>
{/* ─── Breadcrumbs ─── */}
<div className={styles.breadcrumbs}>
<Link to="/" className={styles.breadcrumbLink}>Home</Link>
<span className={styles.sep}>›</span>
<span>Brands</span>
</div>
{/* ─── Search ─── */}
<div className={styles.searchWrapper}>
<div className={styles.searchBox}>
<svg className={styles.searchIcon} viewBox="0 0 24 24" fill="none">
<circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2" />
<path d="M16.5 16.5L21 21" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
<input
type="search"
placeholder="Search brands..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className={styles.searchInput}
/>
</div>
<p className={styles.searchCount}>{filtered.length} brands</p>
</div>
{filtered.length === 0 ? (
<div className={styles.empty}>
<div className={styles.emptyIcon}>🔍</div>
<h3>No brands found for "{search}"</h3>
<p>Try a different search term.</p>
<button type="button" onClick={() => setSearch('')} className={styles.emptyBtn}>
Clear Search
</button>
</div>
) : (
<>
{/* ─── Featured Brands ─── */}
{featured.length > 0 && (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>Featured Brands</h2>
<div className={styles.featuredGrid}>
{featured.map((brand) => (
<Link
key={brand.name}
to={brand.href}
className={styles.featuredCard}
style={{
'--bg': brand.bg,
'--accent': brand.accent,
} as React.CSSProperties}
>
<div className={styles.featuredCardTop}>
<span className={styles.cardEmoji}>{brand.emoji}</span>
<span className={styles.featuredBadge}>Featured</span>
</div>
<h3 className={styles.cardName}>{brand.name}</h3>
<p className={styles.cardTagline}>{brand.tagline}</p>
<div className={styles.cardFooter}>
<span className={styles.cardItems}>{brand.items}</span>
<span className={styles.cardArrow}>Shop Now →</span>
</div>
</Link>
))}
</div>
</section>
)}
{/* ─── All Brands ─── */}
{regular.length > 0 && (
<section className={styles.section}>
<h2 className={styles.sectionTitle}>All Brands</h2>
<div className={styles.brandsGrid}>
{regular.map((brand) => (
<Link
key={brand.name}
to={brand.href}
className={styles.brandCard}
style={{
'--bg': brand.bg,
'--accent': brand.accent,
} as React.CSSProperties}
>
<span className={styles.brandEmoji}>{brand.emoji}</span>
<div className={styles.brandInfo}>
<h3 className={styles.brandName}>{brand.name}</h3>
<p className={styles.brandTagline}>{brand.tagline}</p>
<span className={styles.brandItems}>{brand.items}</span>
</div>
<span className={styles.brandArrow}>→</span>
</Link>
))}
</div>
</section>
)}
</>
)}
{/* ─── Partner CTA ─── */}
<div className={styles.partnerCta}>
<div className={styles.partnerCtaLeft}>
<h3 className={styles.partnerTitle}>Are You a Brand?</h3>
<p className={styles.partnerText}>
Join thousands of fashion brands on Shop.co. Reach millions of style-conscious shoppers worldwide and grow your business with us.
</p>
<div className={styles.partnerActions}>
<a href="mailto:partners@shop.co" className={styles.partnerBtn}>
Become a Partner
</a>
<a href="#" className={styles.partnerBtnOutline}>
Learn More →
</a>
</div>
</div>
<div className={styles.partnerCtaRight}>
<div className={styles.partnerBenefits}>
{['Global Reach', 'Easy Setup', 'Analytics', 'Support 24/7'].map((b) => (
<div key={b} className={styles.partnerBenefit}>
<span>✓</span>
<span>{b}</span>
</div>
))}
</div>
</div>
</div>
</div>
</main>
<Footer />
</>
)
}
export default BrandsPage

View file

@ -397,4 +397,291 @@
width: 100%;
height: 48px;
}
}
/* ────────────────────────────────────────────
Checkout Modal
──────────────────────────────────────────── */
.modalOverlay {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal {
position: relative;
width: 100%;
max-width: 640px;
max-height: 90vh;
overflow-y: auto;
background: #fff;
border-radius: 24px;
padding: 40px;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.18);
animation: slideUp 0.25s ease;
}
@keyframes slideUp {
from { transform: translateY(24px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.modal::-webkit-scrollbar {
width: 6px;
}
.modal::-webkit-scrollbar-track {
background: transparent;
}
.modal::-webkit-scrollbar-thumb {
background: #e0e0e0;
border-radius: 3px;
}
.modalClose {
position: absolute;
top: 20px;
right: 20px;
width: 36px;
height: 36px;
border: 0;
border-radius: 50%;
background: #f5f5f5;
color: #555;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease, color 0.2s ease;
}
.modalClose:hover {
background: #000;
color: #fff;
}
.modalTitle {
margin: 0 0 6px;
font-size: 28px;
font-weight: 800;
letter-spacing: -1px;
color: #000;
}
.modalSubtitle {
margin: 0 0 28px;
font-size: 15px;
color: #666;
}
/* Form */
.checkoutForm {
display: flex;
flex-direction: column;
gap: 24px;
}
.fieldset {
border: 1px solid #E8E8E8;
border-radius: 16px;
padding: 20px 24px 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.legend {
padding: 0 8px;
font-size: 12px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
color: #888;
}
.formRow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.label {
font-size: 13px;
font-weight: 600;
color: #333;
}
.input {
height: 46px;
padding: 0 16px;
border: 1.5px solid #E8E8E8;
border-radius: 12px;
font-family: inherit;
font-size: 14px;
color: #111;
outline: none;
transition: border-color 0.2s ease;
}
.input::placeholder {
color: #bbb;
}
.input:focus {
border-color: #000;
}
.inputError {
border-color: #FF3333 !important;
}
.error {
font-size: 12px;
color: #FF3333;
}
/* Payment */
.paymentOptions {
display: flex;
flex-direction: column;
gap: 10px;
}
.paymentOption {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 18px;
border: 1.5px solid #E8E8E8;
border-radius: 12px;
font-size: 14px;
font-weight: 500;
color: #333;
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease;
}
.paymentOption:hover {
border-color: #bbb;
}
.paymentOptionActive {
border-color: #000 !important;
background: #fafafa;
}
.paymentRadio {
accent-color: #000;
width: 16px;
height: 16px;
cursor: pointer;
}
/* Submit */
.submitButton {
width: 100%;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
border: 0;
border-radius: 30px;
background: #000;
color: #fff;
font-family: inherit;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.3px;
cursor: pointer;
transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease;
}
.submitButton:hover {
background: #222;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
/* Success */
.successState {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 20px 0;
}
.successIcon {
width: 72px;
height: 72px;
border-radius: 50%;
background: #000;
color: #fff;
font-size: 32px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
}
.successTitle {
margin: 0 0 12px;
font-size: 32px;
font-weight: 800;
letter-spacing: -1px;
color: #000;
}
.successText {
margin: 0 0 32px;
font-size: 15px;
color: #555;
line-height: 1.6;
}
.successButton {
padding: 0 40px;
height: 52px;
border: 2px solid #000;
border-radius: 30px;
background: transparent;
color: #000;
font-family: inherit;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.successButton:hover {
background: #000;
color: #fff;
}
@media (max-width: 480px) {
.modal {
padding: 28px 20px;
}
.formRow {
grid-template-columns: 1fr;
}
}

View file

@ -1,17 +1,229 @@
import { useState } from 'react'
import { useCart } from '../../context/CartContext' // <-- ВАЖНО: только этот импорт!
import { useCart } from '../../context/CartContext'
import styles from './Cart.module.css'
import AnnouncementBar from '../../components/AnnouncementBar/AnnouncementBar'
import Header from '../../components/Header/Header'
import Footer from '../../components/Footer/Footer'
// ──────────────────────────────────────────────
// Checkout Modal
// ──────────────────────────────────────────────
interface CheckoutModalProps {
total: number
onClose: () => void
}
function CheckoutModal({ total, onClose }: CheckoutModalProps) {
const [step, setStep] = useState<'form' | 'success'>('form')
const [form, setForm] = useState({
firstName: '',
lastName: '',
email: '',
phone: '',
address: '',
city: '',
zip: '',
payment: 'card',
})
const [errors, setErrors] = useState<Record<string, string>>({})
const validate = () => {
const e: Record<string, string> = {}
if (!form.firstName.trim()) e.firstName = 'Required'
if (!form.lastName.trim()) e.lastName = 'Required'
if (!form.email.trim() || !/\S+@\S+\.\S+/.test(form.email)) e.email = 'Valid email required'
if (!form.phone.trim()) e.phone = 'Required'
if (!form.address.trim()) e.address = 'Required'
if (!form.city.trim()) e.city = 'Required'
if (!form.zip.trim()) e.zip = 'Required'
return e
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const errs = validate()
if (Object.keys(errs).length > 0) {
setErrors(errs)
return
}
setStep('success')
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }))
setErrors((prev) => ({ ...prev, [e.target.name]: '' }))
}
return (
<div className={styles.modalOverlay} onClick={onClose}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<button type="button" className={styles.modalClose} onClick={onClose} aria-label="Close">
✕
</button>
{step === 'form' ? (
<>
<h2 className={styles.modalTitle}>Checkout</h2>
<p className={styles.modalSubtitle}>
Order total: <strong>${total.toFixed(0)}</strong>
</p>
<form className={styles.checkoutForm} onSubmit={handleSubmit} noValidate>
{/* Personal */}
<fieldset className={styles.fieldset}>
<legend className={styles.legend}>Personal Info</legend>
<div className={styles.formRow}>
<div className={styles.field}>
<label className={styles.label}>First Name</label>
<input
name="firstName"
value={form.firstName}
onChange={handleChange}
className={`${styles.input} ${errors.firstName ? styles.inputError : ''}`}
placeholder="John"
/>
{errors.firstName && <span className={styles.error}>{errors.firstName}</span>}
</div>
<div className={styles.field}>
<label className={styles.label}>Last Name</label>
<input
name="lastName"
value={form.lastName}
onChange={handleChange}
className={`${styles.input} ${errors.lastName ? styles.inputError : ''}`}
placeholder="Doe"
/>
{errors.lastName && <span className={styles.error}>{errors.lastName}</span>}
</div>
</div>
<div className={styles.formRow}>
<div className={styles.field}>
<label className={styles.label}>Email</label>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
className={`${styles.input} ${errors.email ? styles.inputError : ''}`}
placeholder="john@example.com"
/>
{errors.email && <span className={styles.error}>{errors.email}</span>}
</div>
<div className={styles.field}>
<label className={styles.label}>Phone</label>
<input
name="phone"
value={form.phone}
onChange={handleChange}
className={`${styles.input} ${errors.phone ? styles.inputError : ''}`}
placeholder="+1 (555) 000-0000"
/>
{errors.phone && <span className={styles.error}>{errors.phone}</span>}
</div>
</div>
</fieldset>
{/* Shipping */}
<fieldset className={styles.fieldset}>
<legend className={styles.legend}>Shipping Address</legend>
<div className={styles.field}>
<label className={styles.label}>Street Address</label>
<input
name="address"
value={form.address}
onChange={handleChange}
className={`${styles.input} ${errors.address ? styles.inputError : ''}`}
placeholder="123 Main St"
/>
{errors.address && <span className={styles.error}>{errors.address}</span>}
</div>
<div className={styles.formRow}>
<div className={styles.field}>
<label className={styles.label}>City</label>
<input
name="city"
value={form.city}
onChange={handleChange}
className={`${styles.input} ${errors.city ? styles.inputError : ''}`}
placeholder="New York"
/>
{errors.city && <span className={styles.error}>{errors.city}</span>}
</div>
<div className={styles.field}>
<label className={styles.label}>ZIP Code</label>
<input
name="zip"
value={form.zip}
onChange={handleChange}
className={`${styles.input} ${errors.zip ? styles.inputError : ''}`}
placeholder="10001"
/>
{errors.zip && <span className={styles.error}>{errors.zip}</span>}
</div>
</div>
</fieldset>
{/* Payment */}
<fieldset className={styles.fieldset}>
<legend className={styles.legend}>Payment Method</legend>
<div className={styles.paymentOptions}>
{(['card', 'paypal', 'cash'] as const).map((method) => (
<label
key={method}
className={`${styles.paymentOption} ${form.payment === method ? styles.paymentOptionActive : ''}`}
>
<input
type="radio"
name="payment"
value={method}
checked={form.payment === method}
onChange={handleChange}
className={styles.paymentRadio}
/>
{method === 'card' && '💳 Credit / Debit Card'}
{method === 'paypal' && '🅿️ PayPal'}
{method === 'cash' && '💵 Cash on Delivery'}
</label>
))}
</div>
</fieldset>
<button type="submit" className={styles.submitButton}>
Place Order — ${total.toFixed(0)}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 12h14" /><path d="M12 5l7 7-7 7" />
</svg>
</button>
</form>
</>
) : (
<div className={styles.successState}>
<div className={styles.successIcon}>✓</div>
<h2 className={styles.successTitle}>Order Placed!</h2>
<p className={styles.successText}>
Thank you, {form.firstName}! Your order has been placed successfully.
<br />
We'll send a confirmation to <strong>{form.email}</strong>.
</p>
<button type="button" className={styles.successButton} onClick={onClose}>
Continue Shopping
</button>
</div>
)}
</div>
</div>
)
}
// ──────────────────────────────────────────────
// Cart Page
// ──────────────────────────────────────────────
function Cart() {
// Берем всё из глобального контекста. Никаких своих fetch!
const { cart, updateQuantity, removeFromCart } = useCart()
const [promoCode, setPromoCode] = useState('')
const [appliedDiscount, setAppliedDiscount] = useState<number | null>(null)
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false)
// Если корзина еще загружается
if (!cart) {
return <div className={styles.loading}>Loading cart...</div>
}
@ -27,7 +239,7 @@ function Cart() {
}
const subtotal = cartItems.reduce(
(total, item) => total + Number(item.product.price) * item.quantity,
(total: number, item: any) => total + Number(item.product.price) * item.quantity,
0
)
@ -58,7 +270,7 @@ function Cart() {
<p>Add some products to your cart to continue.</p>
</div>
) : (
cartItems.map((item) => (
cartItems.map((item: any) => (
<article key={item.id} className={styles.item}>
<div className={styles.imageWrapper}>
<img
@ -173,6 +385,7 @@ function Cart() {
type="button"
className={styles.checkoutButton}
disabled={cartItems.length === 0}
onClick={() => setIsCheckoutOpen(true)}
>
Go to Checkout
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
@ -186,6 +399,10 @@ function Cart() {
</main>
<Footer />
{isCheckoutOpen && (
<CheckoutModal total={total} onClose={() => setIsCheckoutOpen(false)} />
)}
</>
)
}

View file

@ -0,0 +1,451 @@
.page {
min-height: 80vh;
}
/* ─── Hero ─────────────────────── */
.hero {
display: flex;
align-items: center;
justify-content: space-between;
gap: 48px;
padding: 64px 24px 56px;
max-width: 1248px;
margin: 0 auto;
}
.heroContent {
max-width: 560px;
}
.heroBadge {
display: inline-block;
padding: 7px 16px;
margin-bottom: 20px;
border-radius: 30px;
background: #f0f0f0;
font-size: 13px;
font-weight: 600;
color: #333;
}
.heroTitle {
margin: 0 0 16px;
font-size: 64px;
font-weight: 900;
letter-spacing: -3px;
line-height: 1;
color: #000;
}
.heroSub {
margin: 0 0 28px;
font-size: 16px;
color: #555;
line-height: 1.65;
max-width: 460px;
}
.heroActions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.heroActionPrimary {
display: inline-flex;
align-items: center;
height: 50px;
padding: 0 28px;
border-radius: 30px;
background: #000;
color: #fff;
text-decoration: none;
font-size: 15px;
font-weight: 600;
transition: background 0.25s, transform 0.2s;
}
.heroActionPrimary:hover {
background: #222;
transform: translateY(-2px);
}
.heroActionOutline {
display: inline-flex;
align-items: center;
height: 50px;
padding: 0 28px;
border-radius: 30px;
border: 1.5px solid #000;
color: #000;
text-decoration: none;
font-size: 15px;
font-weight: 600;
transition: all 0.25s;
}
.heroActionOutline:hover {
background: #000;
color: #fff;
}
.heroVisual {
flex-shrink: 0;
}
.heroCard {
width: 220px;
height: 220px;
border-radius: 28px;
background: linear-gradient(135deg, #000 0%, #333 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
text-align: center;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
}
.heroCardLabel {
font-size: 11px;
font-weight: 600;
letter-spacing: 2px;
text-transform: uppercase;
color: rgba(255,255,255,0.5);
margin-bottom: 8px;
}
.heroCardTitle {
font-size: 28px;
font-weight: 900;
letter-spacing: -1px;
}
.heroCardSub {
font-size: 12px;
color: rgba(255,255,255,0.6);
margin-top: 6px;
}
/* ─── Features Row ─────────────────────── */
.featuresRow {
display: grid;
grid-template-columns: repeat(4, 1fr);
background: #f8f8f8;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.featureItem {
display: flex;
align-items: center;
gap: 14px;
padding: 20px 28px;
border-right: 1px solid #eee;
}
.featureItem:last-child { border-right: none; }
.featureIcon { font-size: 26px; flex-shrink: 0; }
.featureItem strong {
display: block;
font-size: 13px;
font-weight: 700;
color: #111;
margin-bottom: 2px;
}
.featureItem span {
font-size: 12px;
color: #888;
}
/* ─── Container ─────────────────────── */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 24px 80px;
}
.breadcrumbs {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 32px;
font-size: 14px;
color: #777;
}
.breadcrumbLink {
color: #777;
text-decoration: none;
transition: color 0.2s;
}
.breadcrumbLink:hover { color: #000; }
.sep { color: #bbb; }
/* ─── Toolbar ─────────────────────── */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 28px;
flex-wrap: wrap;
}
.filterTabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.filterTab {
padding: 10px 22px;
border: 1.5px solid #e8e8e8;
border-radius: 30px;
background: transparent;
font-family: inherit;
font-size: 14px;
font-weight: 500;
color: #555;
cursor: pointer;
transition: all 0.2s;
}
.filterTab:hover {
border-color: #999;
color: #000;
}
.filterTabActive {
border-color: #000 !important;
background: #000;
color: #fff !important;
}
.sortRow {
display: flex;
align-items: center;
gap: 12px;
}
.resultsCount {
font-size: 14px;
color: #888;
white-space: nowrap;
}
.sortSelect {
height: 40px;
padding: 0 14px;
border: 1.5px solid #e8e8e8;
border-radius: 10px;
font-family: inherit;
font-size: 14px;
color: #111;
background: #fff;
outline: none;
cursor: pointer;
transition: border-color 0.2s;
}
.sortSelect:focus { border-color: #000; }
/* ─── Grid ─────────────────────── */
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
margin-bottom: 48px;
}
/* ─── Loading / Empty ─────────────────────── */
.loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 80px 0;
color: #666;
}
.spinner {
width: 44px;
height: 44px;
border: 3px solid #eee;
border-top-color: #000;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty {
text-align: center;
padding: 80px 40px;
border: 1px solid #e8e8e8;
border-radius: 24px;
margin-bottom: 48px;
}
.emptyIcon { font-size: 52px; margin-bottom: 20px; }
.empty h3 { margin: 0 0 8px; font-size: 22px; font-weight: 700; }
.empty p { margin: 0 0 24px; color: #666; }
.emptyBtn {
padding: 12px 28px;
border: 0;
border-radius: 30px;
background: #000;
color: #fff;
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.emptyBtn:hover { background: #333; }
/* ─── Trending Tags ─────────────────────── */
.trending {
margin-bottom: 48px;
}
.trendingTitle {
margin: 0 0 16px;
font-size: 18px;
font-weight: 700;
color: #000;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.tag {
padding: 8px 18px;
border-radius: 30px;
background: #f5f5f5;
font-size: 13px;
font-weight: 500;
color: #333;
cursor: pointer;
transition: all 0.2s;
}
.tag:hover {
background: #000;
color: #fff;
}
/* ─── Lookbook CTA ─────────────────────── */
.lookbookCta {
display: grid;
grid-template-columns: 1fr auto;
gap: 48px;
align-items: center;
padding: 48px;
background: linear-gradient(135deg, #000 0%, #1a1a1a 100%);
border-radius: 28px;
color: #fff;
}
.lookbookBadge {
display: inline-block;
padding: 5px 14px;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 30px;
font-size: 11px;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
color: rgba(255,255,255,0.7);
margin-bottom: 16px;
}
.lookbookTitle {
margin: 0 0 12px;
font-size: 30px;
font-weight: 800;
letter-spacing: -1px;
}
.lookbookText {
margin: 0 0 24px;
font-size: 15px;
color: rgba(255,255,255,0.6);
line-height: 1.6;
max-width: 420px;
}
.lookbookBtn {
display: inline-flex;
align-items: center;
height: 48px;
padding: 0 28px;
border-radius: 30px;
background: #fff;
color: #000;
text-decoration: none;
font-size: 14px;
font-weight: 700;
transition: background 0.2s, transform 0.2s;
}
.lookbookBtn:hover {
background: #f0f0f0;
transform: translateY(-2px);
}
.styleGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.styleChip {
padding: 18px 24px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 16px;
font-size: 15px;
font-weight: 600;
color: #fff;
text-align: center;
cursor: pointer;
transition: background 0.2s;
}
.styleChip:hover {
background: rgba(255,255,255,0.2);
}
/* ─── Responsive ─────────────────────── */
@media (max-width: 1024px) {
.grid { grid-template-columns: repeat(3, 1fr); }
.featuresRow { grid-template-columns: repeat(2, 1fr); }
.featureItem:nth-child(2) { border-right: none; }
}
@media (max-width: 768px) {
.heroTitle { font-size: 44px; }
.heroVisual { display: none; }
.hero { padding: 40px 24px 32px; }
.grid { grid-template-columns: repeat(2, 1fr); }
.lookbookCta { grid-template-columns: 1fr; padding: 36px 28px; }
.styleGrid { display: flex; flex-wrap: wrap; }
}
@media (max-width: 480px) {
.heroTitle { font-size: 34px; }
.grid { grid-template-columns: 1fr; }
.featuresRow { grid-template-columns: 1fr; }
.featureItem { border-right: none; border-bottom: 1px solid #eee; }
}

View file

@ -0,0 +1,203 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import AnnouncementBar from '../../components/AnnouncementBar/AnnouncementBar'
import Header from '../../components/Header/Header'
import Footer from '../../components/Footer/Footer'
import ProductCard from '../../components/ProductCard/ProductCard'
import styles from './NewArrivalsPage.module.css'
type Product = {
id: number
name: string
slug: string
image: string
rating: number
price: string
old_price: string | null
discount_percent: number | null
category: number
description: string
}
const FILTERS = [
{ key: 'all', label: 'All Items' },
{ key: 'new-arrivals', label: 'New Arrivals' },
{ key: 'top-selling', label: 'Top Selling' },
]
const FEATURES = [
{ icon: '✨', title: 'Fresh Weekly Drops', text: 'New styles added every Monday' },
{ icon: '📦', title: 'Express Shipping', text: 'Get it in 2-3 business days' },
{ icon: '🎯', title: 'Trend Forecast', text: 'Styles curated by our team' },
{ icon: '💯', title: 'Quality Guarantee', text: 'Love it or we replace it' },
]
function NewArrivalsPage() {
const [products, setProducts] = useState<Product[]>([])
const [isLoading, setIsLoading] = useState(true)
const [activeFilter, setActiveFilter] = useState('all')
const [sort, setSort] = useState('default')
useEffect(() => {
const url = activeFilter === 'all'
? 'http://127.0.0.1:8000/api/products/'
: `http://127.0.0.1:8000/api/products/?category=${activeFilter}`
setIsLoading(true)
fetch(url)
.then((res) => res.json())
.then((data: Product[]) => setProducts(data))
.catch(() => setProducts([]))
.finally(() => setIsLoading(false))
}, [activeFilter])
const sorted = [...products].sort((a, b) => {
if (sort === 'price-asc') return Number(a.price) - Number(b.price)
if (sort === 'price-desc') return Number(b.price) - Number(a.price)
if (sort === 'rating') return Number(b.rating) - Number(a.rating)
return 0
})
return (
<>
<AnnouncementBar />
<Header />
<main className={styles.page}>
{/* ─── Hero ─── */}
<div className={styles.hero}>
<div className={styles.heroContent}>
<span className={styles.heroBadge}>✨ Just Arrived</span>
<h1 className={styles.heroTitle}>NEW ARRIVALS</h1>
<p className={styles.heroSub}>
Discover our freshest styles — meticulously crafted garments that define the season's must-have looks. Be the first to wear what's trending.
</p>
<div className={styles.heroActions}>
<Link to="/sale" className={styles.heroActionOutline}>View Sale →</Link>
</div>
</div>
<div className={styles.heroVisual}>
<div className={styles.heroCard}>
<div className={styles.heroCardLabel}>This Season</div>
<div className={styles.heroCardTitle}>Fresh Drops</div>
<div className={styles.heroCardSub}>Updated Weekly</div>
</div>
</div>
</div>
{/* ─── Features Row ─── */}
<div className={styles.featuresRow}>
{FEATURES.map((f) => (
<div key={f.title} className={styles.featureItem}>
<span className={styles.featureIcon}>{f.icon}</span>
<div>
<strong>{f.title}</strong>
<span>{f.text}</span>
</div>
</div>
))}
</div>
<div className={styles.container}>
{/* ─── Breadcrumbs ─── */}
<div className={styles.breadcrumbs}>
<Link to="/" className={styles.breadcrumbLink}>Home</Link>
<span className={styles.sep}>›</span>
<span>New Arrivals</span>
</div>
{/* ─── Filter & Sort Toolbar ─── */}
<div className={styles.toolbar}>
<div className={styles.filterTabs}>
{FILTERS.map((f) => (
<button
key={f.key}
type="button"
className={`${styles.filterTab} ${activeFilter === f.key ? styles.filterTabActive : ''}`}
onClick={() => setActiveFilter(f.key)}
>
{f.label}
</button>
))}
</div>
<div className={styles.sortRow}>
<span className={styles.resultsCount}>
{isLoading ? '...' : `${products.length} items`}
</span>
<select
className={styles.sortSelect}
value={sort}
onChange={(e) => setSort(e.target.value)}
>
<option value="default">Sort: Featured</option>
<option value="price-asc">Price: Low → High</option>
<option value="price-desc">Price: High → Low</option>
<option value="rating">Top Rated</option>
</select>
</div>
</div>
{/* ─── Product Grid ─── */}
{isLoading ? (
<div className={styles.loading}>
<div className={styles.spinner} />
<p>Loading products...</p>
</div>
) : sorted.length === 0 ? (
<div className={styles.empty}>
<div className={styles.emptyIcon}>🛍️</div>
<h3>Nothing here yet</h3>
<p>We're adding new items soon. Check back later!</p>
<button type="button" className={styles.emptyBtn} onClick={() => setActiveFilter('all')}>
Show All Items
</button>
</div>
) : (
<div className={styles.grid}>
{sorted.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
)}
{/* ─── Trending Tags ─── */}
<div className={styles.trending}>
<h3 className={styles.trendingTitle}>Trending Now</h3>
<div className={styles.tags}>
{['#StreetStyle', '#CasualFit', '#SummerVibes', '#MinimalistFashion', '#OverSized', '#DenimLove', '#GraphicTees', '#MonoChrome'].map((tag) => (
<span key={tag} className={styles.tag}>{tag}</span>
))}
</div>
</div>
{/* ─── Lookbook CTA ─── */}
<div className={styles.lookbookCta}>
<div className={styles.lookbookCtaLeft}>
<span className={styles.lookbookBadge}>Style Inspiration</span>
<h3 className={styles.lookbookTitle}>Browse By Style</h3>
<p className={styles.lookbookText}>
Not sure what to pick? Explore curated outfit collections for every occasion — from casual to formal, streetwear to resort.
</p>
<Link to="/" className={styles.lookbookBtn}>
Explore Styles →
</Link>
</div>
<div className={styles.lookbookCtaRight}>
<div className={styles.styleGrid}>
{['Casual', 'Formal', 'Party', 'Gym'].map((s, i) => (
<div key={s} className={styles.styleChip} style={{ animationDelay: `${i * 0.1}s` }}>
{s}
</div>
))}
</div>
</div>
</div>
</div>
</main>
<Footer />
</>
)
}
export default NewArrivalsPage

View file

@ -0,0 +1,384 @@
.page {
min-height: 80vh;
}
/* ─── Hero Banner ─────────────────────── */
.heroBanner {
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%);
padding: 60px 24px;
color: #fff;
overflow: hidden;
}
.heroInner {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 40px;
}
.heroLeft {
max-width: 580px;
}
.heroBadge {
display: inline-block;
padding: 8px 18px;
border: 1px solid rgba(255,255,255,0.25);
border-radius: 30px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.5px;
color: rgba(255,255,255,0.85);
margin-bottom: 20px;
}
.heroTitle {
margin: 0 0 16px;
font-size: 60px;
font-weight: 900;
letter-spacing: -3px;
line-height: 1;
text-transform: uppercase;
}
.heroHighlight {
color: #FFD700;
}
.heroText {
margin: 0 0 32px;
font-size: 17px;
color: rgba(255,255,255,0.65);
line-height: 1.6;
max-width: 440px;
}
.heroStats {
display: flex;
align-items: center;
gap: 24px;
}
.heroStat {
display: flex;
flex-direction: column;
gap: 2px;
}
.heroStat strong {
font-size: 22px;
font-weight: 800;
color: #FFD700;
letter-spacing: -0.5px;
}
.heroStat span {
font-size: 12px;
color: rgba(255,255,255,0.55);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.heroStatDivider {
width: 1px;
height: 36px;
background: rgba(255,255,255,0.2);
}
.heroRight {
flex-shrink: 0;
}
.discountCircle {
width: 180px;
height: 180px;
border-radius: 50%;
background: #FFD700;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 0 60px rgba(255, 215, 0, 0.4);
}
.discountCircleNum {
font-size: 42px;
font-weight: 900;
color: #000;
letter-spacing: -2px;
line-height: 1;
}
.discountCircleText {
font-size: 10px;
font-weight: 700;
color: #000;
letter-spacing: 2px;
margin-top: 4px;
}
/* ─── Promo Strips ─────────────────────── */
.promoStrips {
display: grid;
grid-template-columns: repeat(4, 1fr);
background: #f5f5f5;
border-bottom: 1px solid #e8e8e8;
}
.promoStrip {
display: flex;
align-items: center;
gap: 12px;
padding: 18px 24px;
border-right: 1px solid #e8e8e8;
}
.promoStrip:last-child {
border-right: none;
}
.promoStripIcon {
font-size: 24px;
flex-shrink: 0;
}
.promoStrip strong {
display: block;
font-size: 13px;
font-weight: 700;
color: #000;
margin-bottom: 1px;
}
.promoStrip span {
font-size: 12px;
color: #777;
}
/* ─── Container ─────────────────────── */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 24px 80px;
}
.breadcrumbs {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 32px;
font-size: 14px;
color: #666;
}
.breadcrumbLink {
color: #666;
text-decoration: none;
transition: color 0.2s;
}
.breadcrumbLink:hover { color: #000; }
.sep { color: #bbb; }
/* ─── Toolbar ─────────────────────── */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 28px;
gap: 16px;
flex-wrap: wrap;
}
.sectionTitle {
margin: 0;
font-size: 28px;
font-weight: 800;
letter-spacing: -1px;
color: #000;
}
.count {
font-size: 18px;
font-weight: 400;
color: #888;
letter-spacing: 0;
}
.sortRow {
display: flex;
align-items: center;
gap: 10px;
}
.sortLabel {
font-size: 14px;
color: #666;
}
.sortSelect {
height: 40px;
padding: 0 14px;
border: 1.5px solid #E8E8E8;
border-radius: 10px;
font-family: inherit;
font-size: 14px;
color: #111;
background: #fff;
outline: none;
cursor: pointer;
transition: border-color 0.2s;
}
.sortSelect:focus { border-color: #000; }
/* ─── Grid ─────────────────────── */
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
}
/* ─── Loading / Empty ─────────────────────── */
.loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 80px 0;
color: #666;
}
.spinner {
width: 44px;
height: 44px;
border: 3px solid #eee;
border-top-color: #000;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty {
text-align: center;
padding: 80px 40px;
border: 1px solid #E8E8E8;
border-radius: 24px;
background: #fafafa;
}
.emptyIcon { font-size: 52px; margin-bottom: 20px; }
.empty h3 {
margin: 0 0 8px;
font-size: 22px;
font-weight: 700;
}
.empty p {
margin: 0 0 28px;
color: #666;
font-size: 15px;
}
.emptyBtn {
display: inline-flex;
padding: 12px 32px;
border-radius: 30px;
background: #000;
color: #fff;
text-decoration: none;
font-size: 14px;
font-weight: 600;
transition: background 0.3s;
}
.emptyBtn:hover { background: #333; }
/* ─── Newsletter CTA ─────────────────────── */
.newsletterCta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 32px;
margin-top: 64px;
padding: 40px 48px;
background: #000;
border-radius: 24px;
color: #fff;
flex-wrap: wrap;
}
.newsletterCtaContent h3 {
margin: 0 0 6px;
font-size: 24px;
font-weight: 800;
letter-spacing: -0.5px;
}
.newsletterCtaContent p {
margin: 0;
font-size: 15px;
color: rgba(255,255,255,0.6);
}
.newsletterCtaForm {
display: flex;
gap: 10px;
flex-shrink: 0;
}
.newsletterInput {
height: 48px;
width: 280px;
padding: 0 18px;
border: 0;
border-radius: 12px;
font-family: inherit;
font-size: 14px;
outline: none;
}
.newsletterBtn {
height: 48px;
padding: 0 24px;
border: 0;
border-radius: 12px;
background: #FFD700;
color: #000;
font-family: inherit;
font-size: 14px;
font-weight: 700;
cursor: pointer;
white-space: nowrap;
transition: opacity 0.2s;
}
.newsletterBtn:hover { opacity: 0.85; }
/* ─── Responsive ─────────────────────── */
@media (max-width: 1024px) {
.grid { grid-template-columns: repeat(3, 1fr); }
.promoStrips { grid-template-columns: repeat(2, 1fr); }
.promoStrip:nth-child(2) { border-right: none; }
}
@media (max-width: 768px) {
.heroTitle { font-size: 40px; }
.heroRight { display: none; }
.grid { grid-template-columns: repeat(2, 1fr); }
.newsletterCta { flex-direction: column; }
.newsletterCtaForm { width: 100%; flex-direction: column; }
.newsletterInput { width: 100%; }
}
@media (max-width: 480px) {
.grid { grid-template-columns: 1fr; }
.promoStrips { grid-template-columns: 1fr; }
.promoStrip { border-right: none; border-bottom: 1px solid #e8e8e8; }
}

View file

@ -0,0 +1,167 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import AnnouncementBar from '../../components/AnnouncementBar/AnnouncementBar'
import Header from '../../components/Header/Header'
import Footer from '../../components/Footer/Footer'
import ProductCard from '../../components/ProductCard/ProductCard'
import styles from './OnSale.module.css'
type Product = {
id: number
name: string
slug: string
image: string
rating: number
price: string
old_price: string | null
discount_percent: number | null
category: number
description: string
}
const SORT_OPTIONS = [
{ value: 'default', label: 'Featured' },
{ value: 'price-asc', label: 'Price: Low to High' },
{ value: 'price-desc', label: 'Price: High to Low' },
{ value: 'discount', label: 'Biggest Discount' },
]
function OnSale() {
const [products, setProducts] = useState<Product[]>([])
const [isLoading, setIsLoading] = useState(true)
const [sort, setSort] = useState('default')
useEffect(() => {
fetch('http://127.0.0.1:8000/api/products/?sale=true')
.then((res) => res.json())
.then((data: Product[]) => setProducts(data))
.catch(() => setProducts([]))
.finally(() => setIsLoading(false))
}, [])
const sorted = [...products].sort((a, b) => {
if (sort === 'price-asc') return Number(a.price) - Number(b.price)
if (sort === 'price-desc') return Number(b.price) - Number(a.price)
if (sort === 'discount') return (b.discount_percent ?? 0) - (a.discount_percent ?? 0)
return 0
})
return (
<>
<AnnouncementBar />
<Header />
<main className={styles.page}>
{/* ── Hero Banner ── */}
<div className={styles.heroBanner}>
<div className={styles.heroInner}>
<div className={styles.heroLeft}>
<span className={styles.heroBadge}>🔥 Limited Time Offers</span>
<h1 className={styles.heroTitle}>
BIG SALE<br />
<span className={styles.heroHighlight}>Up to 40% Off</span>
</h1>
<p className={styles.heroText}>
Don't miss our hottest deals on premium fashion. New discounts added weekly — shop before they're gone.
</p>
<div className={styles.heroStats}>
<div className={styles.heroStat}>
<strong>{isLoading ? '—' : products.length}</strong>
<span>Items on Sale</span>
</div>
<div className={styles.heroStatDivider} />
<div className={styles.heroStat}>
<strong>Up to 40%</strong>
<span>Max Discount</span>
</div>
<div className={styles.heroStatDivider} />
<div className={styles.heroStat}>
<strong>Free</strong>
<span>Returns</span>
</div>
</div>
</div>
<div className={styles.heroRight}>
<div className={styles.discountCircle}>
<span className={styles.discountCircleNum}>-40%</span>
<span className={styles.discountCircleText}>TODAY ONLY</span>
</div>
</div>
</div>
</div>
{/* ── Promo Strips ── */}
<div className={styles.promoStrips}>
{[
{ icon: '🚚', title: 'Free Delivery', sub: 'On orders over $100' },
{ icon: '↩️', title: 'Easy Returns', sub: '30-day return policy' },
{ icon: '🔒', title: 'Secure Payment', sub: '100% protected checkout' },
{ icon: '💎', title: 'Premium Quality', sub: 'Handpicked items' },
].map((p) => (
<div key={p.title} className={styles.promoStrip}>
<span className={styles.promoStripIcon}>{p.icon}</span>
<div>
<strong>{p.title}</strong>
<span>{p.sub}</span>
</div>
</div>
))}
</div>
<div className={styles.container}>
{/* ── Breadcrumbs ── */}
<div className={styles.breadcrumbs}>
<Link to="/" className={styles.breadcrumbLink}>Home</Link>
<span className={styles.sep}>›</span>
<span>On Sale</span>
</div>
{/* ── Toolbar ── */}
<div className={styles.toolbar}>
<h2 className={styles.sectionTitle}>
Sale Items
{!isLoading && <span className={styles.count}> ({products.length})</span>}
</h2>
<div className={styles.sortRow}>
<label className={styles.sortLabel}>Sort by:</label>
<select
className={styles.sortSelect}
value={sort}
onChange={(e) => setSort(e.target.value)}
>
{SORT_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</div>
</div>
{/* ── Products ── */}
{isLoading ? (
<div className={styles.loading}>
<div className={styles.spinner} />
<p>Loading sale items...</p>
</div>
) : sorted.length === 0 ? (
<div className={styles.empty}>
<div className={styles.emptyIcon}>😔</div>
<h3>No sale items right now</h3>
<p>Check back soon — new deals drop every week!</p>
<Link to="/" className={styles.emptyBtn}>Browse All Products</Link>
</div>
) : (
<div className={styles.grid}>
{sorted.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
)}
</div>
</main>
<Footer />
</>
)
}
export default OnSale