add new pages
This commit is contained in:
parent
dce8b7eab5
commit
2ede0d8641
21 changed files with 3014 additions and 44 deletions
282
api/fixtures/initial_data.json
Normal file
282
api/fixtures/initial_data.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
from rest_framework import serializers
|
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):
|
class ProductSerializer(serializers.ModelSerializer):
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
from django.urls import path
|
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
|
from reviews.views import ReviewListView, LatestReviewsView, NewsletterCreateView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("hello/", hello),
|
path("hello/", hello),
|
||||||
path("products/", ProductListView.as_view()),
|
path("products/", ProductListView.as_view()),
|
||||||
|
path("categories/", CategoryListView.as_view()),
|
||||||
|
|
||||||
# Reviews
|
# Reviews
|
||||||
path("reviews/", ReviewListView.as_view()),
|
path("reviews/", ReviewListView.as_view()),
|
||||||
|
|
|
||||||
40
api/views.py
40
api/views.py
|
|
@ -1,10 +1,10 @@
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework.generics import ListAPIView
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from .models import Product
|
from .models import Product, Category
|
||||||
from .serializers import ProductSerializer
|
from .serializers import ProductSerializer, CategorySerializer
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
|
|
@ -14,13 +14,43 @@ def hello(request):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class ProductListView(ListAPIView):
|
class ProductListView(generics.ListAPIView):
|
||||||
queryset = Product.objects.all()
|
|
||||||
serializer_class = ProductSerializer
|
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):
|
class ProductDetailView(generics.RetrieveAPIView):
|
||||||
"""Получить один товар по slug"""
|
"""Получить один товар по slug"""
|
||||||
queryset = Product.objects.all()
|
queryset = Product.objects.all()
|
||||||
serializer_class = ProductSerializer
|
serializer_class = ProductSerializer
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
|
|
||||||
|
|
||||||
|
class CategoryListView(generics.ListAPIView):
|
||||||
|
queryset = Category.objects.all()
|
||||||
|
serializer_class = CategorySerializer
|
||||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
|
@ -1,17 +1,23 @@
|
||||||
import { Routes, Route } from 'react-router-dom'
|
import { Routes, Route } from 'react-router-dom'
|
||||||
import { CartProvider } from './context/CartContext' // <-- Импортируем провайдер
|
import { CartProvider } from './context/CartContext'
|
||||||
|
|
||||||
import Home from './pages/Home/Home'
|
import Home from './pages/Home/Home'
|
||||||
import ProductDetail from './pages/ProductDetail/ProductDetail'
|
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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<CartProvider> {/* <-- Оборачиваем всё приложение */}
|
<CartProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/product/:slug" element={<ProductDetail />} />
|
<Route path="/product/:slug" element={<ProductDetail />} />
|
||||||
<Route path="/cart" element={<Cart />} />
|
<Route path="/cart" element={<Cart />} />
|
||||||
|
<Route path="/sale" element={<OnSale />} />
|
||||||
|
<Route path="/new-arrivals" element={<NewArrivalsPage />} />
|
||||||
|
<Route path="/brands" element={<BrandsPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</CartProvider>
|
</CartProvider>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,18 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: #f1efef;
|
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 {
|
.image {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import styles from './DressStyle.module.css'
|
import styles from './DressStyle.module.css'
|
||||||
|
|
||||||
type DressStyleItem = {
|
type DressStyleItem = {
|
||||||
|
|
@ -8,6 +7,18 @@ type DressStyleItem = {
|
||||||
image: string
|
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() {
|
function DressStyle() {
|
||||||
const [dressStyles, setDressStyles] = useState<DressStyleItem[]>([])
|
const [dressStyles, setDressStyles] = useState<DressStyleItem[]>([])
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
@ -16,7 +27,14 @@ function DressStyle() {
|
||||||
fetch('http://127.0.0.1:8000/api/dress-styles/')
|
fetch('http://127.0.0.1:8000/api/dress-styles/')
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
setDressStyles(data)
|
setDressStyles(data)
|
||||||
|
} else {
|
||||||
|
setDressStyles(FALLBACK_STYLES)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setDressStyles(FALLBACK_STYLES)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
|
@ -33,13 +51,23 @@ function DressStyle() {
|
||||||
<h2 className={styles.title}>BROWSE BY DRESS STYLE</h2>
|
<h2 className={styles.title}>BROWSE BY DRESS STYLE</h2>
|
||||||
|
|
||||||
<div className={styles.styles}>
|
<div className={styles.styles}>
|
||||||
{dressStyles.map((dressStyle) => (
|
{dressStyles.map((dressStyle, idx) => (
|
||||||
<article key={dressStyle.id} className={styles.card}>
|
<article
|
||||||
|
key={dressStyle.id}
|
||||||
|
className={styles.card}
|
||||||
|
style={!dressStyle.image ? { backgroundColor: CARD_COLORS[idx % CARD_COLORS.length] } : undefined}
|
||||||
|
>
|
||||||
|
{dressStyle.image ? (
|
||||||
<img
|
<img
|
||||||
src={dressStyle.image}
|
src={dressStyle.image}
|
||||||
alt={dressStyle.name}
|
alt={dressStyle.name}
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.placeholderIcon}>
|
||||||
|
{CARD_ICONS[idx % CARD_ICONS.length]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<h3 className={styles.name}>{dressStyle.name}</h3>
|
<h3 className={styles.name}>{dressStyle.name}</h3>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,92 @@
|
||||||
.arrow {
|
.arrow {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
line-height: 1;
|
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 {
|
.search {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,31 @@
|
||||||
import { useState } from 'react'
|
import { useState, useRef, useEffect } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
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 { navigationItems } from '../../shared/navigation/navigation.data'
|
||||||
import styles from './Header.module.css'
|
import styles from './Header.module.css'
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
const [isShopOpen, setIsShopOpen] = useState(false)
|
const [isShopOpen, setIsShopOpen] = useState(false)
|
||||||
|
const shopRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// Получаем актуальные данные корзины из глобального контекста
|
// Получаем актуальные данные корзины из глобального контекста
|
||||||
const { cart } = useCart()
|
const { cart } = useCart()
|
||||||
|
|
||||||
// Считаем общее количество товаров в реальном времени
|
// Считаем общее количество товаров в реальном времени
|
||||||
// Если cart или items еще не загрузились, вернет 0
|
|
||||||
const totalItems = cart?.items?.reduce((sum: number, item: any) => sum + item.quantity, 0) ?? 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 (
|
return (
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
|
|
@ -25,23 +36,65 @@ function Header() {
|
||||||
<nav className={styles.navigation}>
|
<nav className={styles.navigation}>
|
||||||
{navigationItems.map((item) =>
|
{navigationItems.map((item) =>
|
||||||
item.children ? (
|
item.children ? (
|
||||||
<button
|
// Shop с выпадающим меню
|
||||||
|
<div
|
||||||
key={item.label}
|
key={item.label}
|
||||||
|
className={styles.shopWrapper}
|
||||||
|
ref={shopRef}
|
||||||
|
>
|
||||||
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.navigationItem}
|
className={`${styles.navigationItem} ${isShopOpen ? styles.navigationItemActive : ''}`}
|
||||||
onClick={() => setIsShopOpen(!isShopOpen)}
|
onClick={() => setIsShopOpen(!isShopOpen)}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={isShopOpen}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
<span className={styles.arrow}>▼</span>
|
<span className={`${styles.arrow} ${isShopOpen ? styles.arrowOpen : ''}`}>▼</span>
|
||||||
</button>
|
</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}
|
key={item.href}
|
||||||
href={item.href}
|
to={item.href!}
|
||||||
className={styles.navigationItem}
|
className={styles.navigationItem}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</a>
|
</Link>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -85,7 +138,6 @@ function Header() {
|
||||||
<circle cx="17" cy="20" r="1.5" fill="currentColor" />
|
<circle cx="17" cy="20" r="1.5" fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{/* Показываем бейдж только если товаров > 0 */}
|
|
||||||
{totalItems > 0 && (
|
{totalItems > 0 && (
|
||||||
<span className={styles.cartCount}>
|
<span className={styles.cartCount}>
|
||||||
{totalItems > 99 ? '99+' : totalItems}
|
{totalItems > 99 ? '99+' : totalItems}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import ProductsCard from '../ProductCard/ProductCard'
|
||||||
type Product = {
|
type Product = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
slug: string
|
||||||
image: string
|
image: string
|
||||||
rating: number
|
rating: number
|
||||||
price: string
|
price: string
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import ProductsCard from '../ProductCard/ProductCard'
|
||||||
type Product = {
|
type Product = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
slug: string
|
||||||
image: string
|
image: string
|
||||||
rating: number
|
rating: number
|
||||||
price: string
|
price: string
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
|
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
|
||||||
|
|
||||||
interface CartContextType {
|
interface CartContextType {
|
||||||
cart: any
|
cart: any
|
||||||
|
|
|
||||||
451
frontend/src/pages/Brands/BrandsPage.module.css
Normal file
451
frontend/src/pages/Brands/BrandsPage.module.css
Normal 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; }
|
||||||
|
}
|
||||||
306
frontend/src/pages/Brands/BrandsPage.tsx
Normal file
306
frontend/src/pages/Brands/BrandsPage.tsx
Normal 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
|
||||||
|
|
@ -398,3 +398,290 @@
|
||||||
height: 48px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,229 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useCart } from '../../context/CartContext' // <-- ВАЖНО: только этот импорт!
|
import { useCart } from '../../context/CartContext'
|
||||||
import styles from './Cart.module.css'
|
import styles from './Cart.module.css'
|
||||||
import AnnouncementBar from '../../components/AnnouncementBar/AnnouncementBar'
|
import AnnouncementBar from '../../components/AnnouncementBar/AnnouncementBar'
|
||||||
import Header from '../../components/Header/Header'
|
import Header from '../../components/Header/Header'
|
||||||
import Footer from '../../components/Footer/Footer'
|
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() {
|
function Cart() {
|
||||||
// Берем всё из глобального контекста. Никаких своих fetch!
|
|
||||||
const { cart, updateQuantity, removeFromCart } = useCart()
|
const { cart, updateQuantity, removeFromCart } = useCart()
|
||||||
const [promoCode, setPromoCode] = useState('')
|
const [promoCode, setPromoCode] = useState('')
|
||||||
const [appliedDiscount, setAppliedDiscount] = useState<number | null>(null)
|
const [appliedDiscount, setAppliedDiscount] = useState<number | null>(null)
|
||||||
|
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false)
|
||||||
|
|
||||||
// Если корзина еще загружается
|
|
||||||
if (!cart) {
|
if (!cart) {
|
||||||
return <div className={styles.loading}>Loading cart...</div>
|
return <div className={styles.loading}>Loading cart...</div>
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +239,7 @@ function Cart() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtotal = cartItems.reduce(
|
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
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -58,7 +270,7 @@ function Cart() {
|
||||||
<p>Add some products to your cart to continue.</p>
|
<p>Add some products to your cart to continue.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
cartItems.map((item) => (
|
cartItems.map((item: any) => (
|
||||||
<article key={item.id} className={styles.item}>
|
<article key={item.id} className={styles.item}>
|
||||||
<div className={styles.imageWrapper}>
|
<div className={styles.imageWrapper}>
|
||||||
<img
|
<img
|
||||||
|
|
@ -173,6 +385,7 @@ function Cart() {
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.checkoutButton}
|
className={styles.checkoutButton}
|
||||||
disabled={cartItems.length === 0}
|
disabled={cartItems.length === 0}
|
||||||
|
onClick={() => setIsCheckoutOpen(true)}
|
||||||
>
|
>
|
||||||
Go to Checkout
|
Go to Checkout
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<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>
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
||||||
|
{isCheckoutOpen && (
|
||||||
|
<CheckoutModal total={total} onClose={() => setIsCheckoutOpen(false)} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
451
frontend/src/pages/NewArrivals/NewArrivalsPage.module.css
Normal file
451
frontend/src/pages/NewArrivals/NewArrivalsPage.module.css
Normal 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; }
|
||||||
|
}
|
||||||
203
frontend/src/pages/NewArrivals/NewArrivalsPage.tsx
Normal file
203
frontend/src/pages/NewArrivals/NewArrivalsPage.tsx
Normal 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
|
||||||
384
frontend/src/pages/OnSale/OnSale.module.css
Normal file
384
frontend/src/pages/OnSale/OnSale.module.css
Normal 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; }
|
||||||
|
}
|
||||||
167
frontend/src/pages/OnSale/OnSale.tsx
Normal file
167
frontend/src/pages/OnSale/OnSale.tsx
Normal 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
|
||||||
Loading…
Add table
Reference in a new issue