From 2ede0d8641eafd2d9c00b23deb2f586beb854869 Mon Sep 17 00:00:00 2001 From: Nikita Egorov Date: Sun, 27 Sep 2026 11:27:54 +0500 Subject: [PATCH] add new pages --- api/fixtures/initial_data.json | 282 +++++++++++ api/serializers.py | 7 +- api/urls.py | 9 +- api/views.py | 42 +- db.sqlite3 | Bin 200704 -> 200704 bytes frontend/src/App.tsx | 12 +- .../DressStyle/DressStyle.module.css | 12 + .../src/components/DressStyle/DressStyle.tsx | 46 +- .../src/components/Header/Header.module.css | 86 ++++ frontend/src/components/Header/Header.tsx | 82 +++- .../components/NewArrivals/NewArrivals.tsx | 1 + .../src/components/TopSelling/TopSelling.tsx | 1 + frontend/src/context/CartContext.tsx | 2 +- .../src/pages/Brands/BrandsPage.module.css | 451 ++++++++++++++++++ frontend/src/pages/Brands/BrandsPage.tsx | 306 ++++++++++++ frontend/src/pages/Cart/Cart.module.css | 287 +++++++++++ frontend/src/pages/Cart/Cart.tsx | 227 ++++++++- .../NewArrivals/NewArrivalsPage.module.css | 451 ++++++++++++++++++ .../src/pages/NewArrivals/NewArrivalsPage.tsx | 203 ++++++++ frontend/src/pages/OnSale/OnSale.module.css | 384 +++++++++++++++ frontend/src/pages/OnSale/OnSale.tsx | 167 +++++++ 21 files changed, 3014 insertions(+), 44 deletions(-) create mode 100644 api/fixtures/initial_data.json create mode 100644 frontend/src/pages/Brands/BrandsPage.module.css create mode 100644 frontend/src/pages/Brands/BrandsPage.tsx create mode 100644 frontend/src/pages/NewArrivals/NewArrivalsPage.module.css create mode 100644 frontend/src/pages/NewArrivals/NewArrivalsPage.tsx create mode 100644 frontend/src/pages/OnSale/OnSale.module.css create mode 100644 frontend/src/pages/OnSale/OnSale.tsx diff --git a/api/fixtures/initial_data.json b/api/fixtures/initial_data.json new file mode 100644 index 0000000..ebdd690 --- /dev/null +++ b/api/fixtures/initial_data.json @@ -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" + } + } +] diff --git a/api/serializers.py b/api/serializers.py index 7339f27..5295fa3 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -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): diff --git a/api/urls.py b/api/urls.py index a7436a5..52572fe 100644 --- a/api/urls.py +++ b/api/urls.py @@ -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//", ProductDetailView.as_view()), + path("products//", ProductDetailView.as_view()), ] \ No newline at end of file diff --git a/api/views.py b/api/views.py index 6250865..def21cf 100644 --- a/api/views.py +++ b/api/views.py @@ -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' \ No newline at end of file + lookup_field = 'slug' + + +class CategoryListView(generics.ListAPIView): + queryset = Category.objects.all() + serializer_class = CategorySerializer \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index 154aea145da3949a01d46fc81fa3508a785ea02e..e57a4f09044f24266d3a5097cac7b9df79e8625f 100644 GIT binary patch delta 5501 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 delta 1070 zcmZ`%O>7ip82-L@_GhLu^SxVi7fRhQAt0p9{&l-;XOV4IVr)I~JFN#UhL(+f2Q}$|UI?`%3)w_N;sF{DMh=vOim4Ui52S`-0vcDV-xQ*U#>33v zd7tNfo_FRcPHDv{ZK9r@n|x}Y^Bf7j|NHsDLeS&VH%Z&*Kq{L*Xuq`5-C&2yS*J^_ zlovk+Z>cl1n?k7&%2K&}Csf}jG1o%LJ>f4WJ##Kso;f;?Crf|N809}ctWmZwy-M&j zJEd2d!7gA0kK(uNIks3frY7}Lx96qt>P)3NLzQPG#=5wBmHNvOqmfm**62kl1I`n? zh4baw(a(_2BP0a0OmG#K1+;*Cc^m9_QpW29|HR+$SNsKk!te1WUKis_$XD-ygD-|) z_h#)2BFn=>9>zvJsdUwZnOD`+J0f+D%&dN@lJ=OJ5@pQa&i}eb>-ei};O8BC;3N;u zLY>2U!=ds6CYuU2O%UrIoYCHJ)n|6)D2(@*AkC~?6J$COx|{? zthIv_u4TK3Q|%|XgEi_AEur>{uiveD#bq+_)3J<_Jdnz!hK$ajb35PnE}RYBDt^)w zZfXgH%|OI7!p-qWOFY^ZjK;!dbF($-kjZ8`?pKnX$Bv~(glPB#T$Y50PklqZTieWN zG-Bl>d%MlrpE{b(886z~ZDMT0_OKa`m_aifZHk%VLWflVnbgzqok}u!B>k50f-QaJ z9r&_oqqHp^iv}ZYu}G}N>ZSj4(0aDzo787STf?n2!b8I=|JCVdyp2n^hzoeNVie&c zoa|~XE5xVhb z%fd|&4Sj7i;Z=LYNBf>5KheQ?71w&Kk2%f {/* <-- Оборачиваем всё приложение */} + } /> } /> } /> + } /> + } /> + } /> ) diff --git a/frontend/src/components/DressStyle/DressStyle.module.css b/frontend/src/components/DressStyle/DressStyle.module.css index dd819d1..f80bd37 100644 --- a/frontend/src/components/DressStyle/DressStyle.module.css +++ b/frontend/src/components/DressStyle/DressStyle.module.css @@ -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 { diff --git a/frontend/src/components/DressStyle/DressStyle.tsx b/frontend/src/components/DressStyle/DressStyle.tsx index 18d20c1..8c439d6 100644 --- a/frontend/src/components/DressStyle/DressStyle.tsx +++ b/frontend/src/components/DressStyle/DressStyle.tsx @@ -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([]) 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() {

BROWSE BY DRESS STYLE

- {dressStyles.map((dressStyle) => ( -
- {dressStyle.name} + {dressStyles.map((dressStyle, idx) => ( +
+ {dressStyle.image ? ( + {dressStyle.name} + ) : ( +
+ {CARD_ICONS[idx % CARD_ICONS.length]} +
+ )}

{dressStyle.name}

diff --git a/frontend/src/components/Header/Header.module.css b/frontend/src/components/Header/Header.module.css index 7307f17..b46f6af 100644 --- a/frontend/src/components/Header/Header.module.css +++ b/frontend/src/components/Header/Header.module.css @@ -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 { diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index 9908f0e..bc6b288 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -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(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 (
@@ -25,23 +36,65 @@ function Header() {
) : ( - {item.label} - + ), )} @@ -85,7 +138,6 @@ function Header() { - {/* Показываем бейдж только если товаров > 0 */} {totalItems > 0 && ( {totalItems > 99 ? '99+' : totalItems} diff --git a/frontend/src/components/NewArrivals/NewArrivals.tsx b/frontend/src/components/NewArrivals/NewArrivals.tsx index 07500b0..44d80f2 100644 --- a/frontend/src/components/NewArrivals/NewArrivals.tsx +++ b/frontend/src/components/NewArrivals/NewArrivals.tsx @@ -6,6 +6,7 @@ import ProductsCard from '../ProductCard/ProductCard' type Product = { id: number name: string + slug: string image: string rating: number price: string diff --git a/frontend/src/components/TopSelling/TopSelling.tsx b/frontend/src/components/TopSelling/TopSelling.tsx index 1a583ab..43e7f01 100644 --- a/frontend/src/components/TopSelling/TopSelling.tsx +++ b/frontend/src/components/TopSelling/TopSelling.tsx @@ -6,6 +6,7 @@ import ProductsCard from '../ProductCard/ProductCard' type Product = { id: number name: string + slug: string image: string rating: number price: string diff --git a/frontend/src/context/CartContext.tsx b/frontend/src/context/CartContext.tsx index e4a104a..0124958 100644 --- a/frontend/src/context/CartContext.tsx +++ b/frontend/src/context/CartContext.tsx @@ -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 diff --git a/frontend/src/pages/Brands/BrandsPage.module.css b/frontend/src/pages/Brands/BrandsPage.module.css new file mode 100644 index 0000000..20b166f --- /dev/null +++ b/frontend/src/pages/Brands/BrandsPage.module.css @@ -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; } +} diff --git a/frontend/src/pages/Brands/BrandsPage.tsx b/frontend/src/pages/Brands/BrandsPage.tsx new file mode 100644 index 0000000..c56aeb2 --- /dev/null +++ b/frontend/src/pages/Brands/BrandsPage.tsx @@ -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 ( + <> + +
+ +
+ {/* ─── Hero ─── */} +
+
+

OUR BRANDS

+

+ From luxury maisons to streetwear icons — discover the world's most celebrated fashion brands, all in one place. +

+ {/* Stats */} +
+ {STATS.map((s) => ( +
+ {s.num} + {s.label} +
+ ))} +
+
+
+ +
+ {/* ─── Breadcrumbs ─── */} +
+ Home + › + Brands +
+ + {/* ─── Search ─── */} +
+
+ + + + + setSearch(e.target.value)} + className={styles.searchInput} + /> +
+

{filtered.length} brands

+
+ + {filtered.length === 0 ? ( +
+
🔍
+

No brands found for "{search}"

+

Try a different search term.

+ +
+ ) : ( + <> + {/* ─── Featured Brands ─── */} + {featured.length > 0 && ( +
+

Featured Brands

+
+ {featured.map((brand) => ( + +
+ {brand.emoji} + Featured +
+

{brand.name}

+

{brand.tagline}

+
+ {brand.items} + Shop Now → +
+ + ))} +
+
+ )} + + {/* ─── All Brands ─── */} + {regular.length > 0 && ( +
+

All Brands

+
+ {regular.map((brand) => ( + + {brand.emoji} +
+

{brand.name}

+

{brand.tagline}

+ {brand.items} +
+ → + + ))} +
+
+ )} + + )} + + {/* ─── Partner CTA ─── */} +
+
+

Are You a Brand?

+

+ Join thousands of fashion brands on Shop.co. Reach millions of style-conscious shoppers worldwide and grow your business with us. +

+ +
+
+
+ {['Global Reach', 'Easy Setup', 'Analytics', 'Support 24/7'].map((b) => ( +
+ ✓ + {b} +
+ ))} +
+
+
+
+
+ +
+ + ) +} + +export default BrandsPage diff --git a/frontend/src/pages/Cart/Cart.module.css b/frontend/src/pages/Cart/Cart.module.css index d25f1ca..29a46e9 100644 --- a/frontend/src/pages/Cart/Cart.module.css +++ b/frontend/src/pages/Cart/Cart.module.css @@ -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; + } } \ No newline at end of file diff --git a/frontend/src/pages/Cart/Cart.tsx b/frontend/src/pages/Cart/Cart.tsx index f9ff1f5..ff8da2f 100644 --- a/frontend/src/pages/Cart/Cart.tsx +++ b/frontend/src/pages/Cart/Cart.tsx @@ -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>({}) + + const validate = () => { + const e: Record = {} + 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) => { + setForm((prev) => ({ ...prev, [e.target.name]: e.target.value })) + setErrors((prev) => ({ ...prev, [e.target.name]: '' })) + } + + return ( +
+
e.stopPropagation()}> + + + {step === 'form' ? ( + <> +

Checkout

+

+ Order total: ${total.toFixed(0)} +

+ +
+ {/* Personal */} +
+ Personal Info +
+
+ + + {errors.firstName && {errors.firstName}} +
+
+ + + {errors.lastName && {errors.lastName}} +
+
+
+
+ + + {errors.email && {errors.email}} +
+
+ + + {errors.phone && {errors.phone}} +
+
+
+ + {/* Shipping */} +
+ Shipping Address +
+ + + {errors.address && {errors.address}} +
+
+
+ + + {errors.city && {errors.city}} +
+
+ + + {errors.zip && {errors.zip}} +
+
+
+ + {/* Payment */} +
+ Payment Method +
+ {(['card', 'paypal', 'cash'] as const).map((method) => ( + + ))} +
+
+ + +
+ + ) : ( +
+
✓
+

Order Placed!

+

+ Thank you, {form.firstName}! Your order has been placed successfully. +
+ We'll send a confirmation to {form.email}. +

+ +
+ )} +
+
+ ) +} + +// ────────────────────────────────────────────── +// Cart Page +// ────────────────────────────────────────────── function Cart() { - // Берем всё из глобального контекста. Никаких своих fetch! const { cart, updateQuantity, removeFromCart } = useCart() const [promoCode, setPromoCode] = useState('') const [appliedDiscount, setAppliedDiscount] = useState(null) + const [isCheckoutOpen, setIsCheckoutOpen] = useState(false) - // Если корзина еще загружается if (!cart) { return
Loading cart...
} @@ -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() {

Add some products to your cart to continue.

) : ( - cartItems.map((item) => ( + cartItems.map((item: any) => (
setIsCheckoutOpen(true)} > Go to Checkout @@ -186,6 +399,10 @@ function Cart() {
+ + {isCheckoutOpen && ( + setIsCheckoutOpen(false)} /> + )} ) } diff --git a/frontend/src/pages/NewArrivals/NewArrivalsPage.module.css b/frontend/src/pages/NewArrivals/NewArrivalsPage.module.css new file mode 100644 index 0000000..af7c8f3 --- /dev/null +++ b/frontend/src/pages/NewArrivals/NewArrivalsPage.module.css @@ -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; } +} diff --git a/frontend/src/pages/NewArrivals/NewArrivalsPage.tsx b/frontend/src/pages/NewArrivals/NewArrivalsPage.tsx new file mode 100644 index 0000000..3cd7cbd --- /dev/null +++ b/frontend/src/pages/NewArrivals/NewArrivalsPage.tsx @@ -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([]) + 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 ( + <> + +
+ +
+ {/* ─── Hero ─── */} +
+
+ ✨ Just Arrived +

NEW ARRIVALS

+

+ Discover our freshest styles — meticulously crafted garments that define the season's must-have looks. Be the first to wear what's trending. +

+
+ View Sale → +
+
+
+
+
This Season
+
Fresh Drops
+
Updated Weekly
+
+
+
+ + {/* ─── Features Row ─── */} +
+ {FEATURES.map((f) => ( +
+ {f.icon} +
+ {f.title} + {f.text} +
+
+ ))} +
+ +
+ {/* ─── Breadcrumbs ─── */} +
+ Home + › + New Arrivals +
+ + {/* ─── Filter & Sort Toolbar ─── */} +
+
+ {FILTERS.map((f) => ( + + ))} +
+
+ + {isLoading ? '...' : `${products.length} items`} + + +
+
+ + {/* ─── Product Grid ─── */} + {isLoading ? ( +
+
+

Loading products...

+
+ ) : sorted.length === 0 ? ( +
+
🛍️
+

Nothing here yet

+

We're adding new items soon. Check back later!

+ +
+ ) : ( +
+ {sorted.map((product) => ( + + ))} +
+ )} + + {/* ─── Trending Tags ─── */} +
+

Trending Now

+
+ {['#StreetStyle', '#CasualFit', '#SummerVibes', '#MinimalistFashion', '#OverSized', '#DenimLove', '#GraphicTees', '#MonoChrome'].map((tag) => ( + {tag} + ))} +
+
+ + {/* ─── Lookbook CTA ─── */} +
+
+ Style Inspiration +

Browse By Style

+

+ Not sure what to pick? Explore curated outfit collections for every occasion — from casual to formal, streetwear to resort. +

+ + Explore Styles → + +
+
+
+ {['Casual', 'Formal', 'Party', 'Gym'].map((s, i) => ( +
+ {s} +
+ ))} +
+
+
+
+ + +