diff --git a/api/urls.py b/api/urls.py index bdef1f9..26811ed 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,8 +1,15 @@ from django.urls import path - from .views import hello, ProductListView +from reviews.views import ReviewListView, LatestReviewsView, NewsletterCreateView urlpatterns = [ path("hello/", hello), path("products/", ProductListView.as_view()), + + # Reviews + path("reviews/", ReviewListView.as_view()), + path("reviews/latest/", LatestReviewsView.as_view()), + + # Newsletter + path("newsletter/", NewsletterCreateView.as_view()), ] \ No newline at end of file diff --git a/backend/settings.py b/backend/settings.py index 2fd54a6..ea8b254 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -38,10 +38,11 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', # ============================= - "corsheaders", - "rest_framework", - "api", - "dressstyle", + 'corsheaders', + 'rest_framework', + 'api', + 'dressstyle', + 'reviews', ] MIDDLEWARE = [ diff --git a/backend/urls.py b/backend/urls.py index 34b6873..eedb32f 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -4,14 +4,16 @@ from django.contrib import admin from django.urls import include, path urlpatterns = [ - path("admin/", admin.site.urls), - + path("api/", include("api.urls")), - + path("api/dress-styles/", include("dressstyle.urls")), - + + # Добавляем reviews + path("api/reviews/", include("reviews.urls")), ] + if settings.DEBUG: urlpatterns += static( settings.MEDIA_URL, diff --git a/frontend/src/components/Footer/Footer.module.css b/frontend/src/components/Footer/Footer.module.css new file mode 100644 index 0000000..887c7bf --- /dev/null +++ b/frontend/src/components/Footer/Footer.module.css @@ -0,0 +1,311 @@ +.footer { + background-color: #F0F0F0; + margin-top: 80px; +} + +/* Newsletter секция */ +.newsletterSection { + padding: 0 25px; + margin-top: -60px; + position: relative; + z-index: 10; +} + +.newsletterContainer { + max-width: 1200px; + margin: 0 auto; + background-color: #000; + border-radius: 20px; + padding: 50px 60px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 40px; +} + +.newsletterContent { + flex: 1; +} + +.newsletterTitle { + font-size: 40px; + font-weight: 800; + color: #fff; + text-transform: uppercase; + line-height: 1.2; + margin: 0; + letter-spacing: -1px; +} + +.newsletterForm { + flex: 0 0 400px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.inputWrapper { + position: relative; + display: flex; + align-items: center; +} + +.emailIcon { + position: absolute; + left: 20px; + width: 20px; + height: 20px; + color: #999; +} + +.emailInput { + width: 100%; + padding: 16px 20px 16px 52px; + border: none; + border-radius: 50px; + font-size: 14px; + background: #fff; + outline: none; + transition: box-shadow 0.3s ease; +} + +.emailInput:focus { + box-shadow: 0 0 0 3px rgba(255,255,255,0.3); +} + +.emailInput::placeholder { + color: #999; +} + +.subscribeButton { + width: 100%; + padding: 16px 32px; + background: #fff; + color: #000; + border: none; + border-radius: 50px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.subscribeButton:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 10px 20px rgba(255,255,255,0.2); +} + +.subscribeButton:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.message { + text-align: center; + padding: 10px; + border-radius: 8px; + font-size: 13px; +} + +.success { + background: #10B981; + color: #fff; +} + +.error { + background: #EF4444; + color: #fff; +} + +/* Основной footer */ +.mainFooter { + padding: 80px 25px 40px; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} + +.mainContent { + display: flex; + justify-content: space-between; + gap: 60px; + margin-bottom: 60px; +} + +.brand { + flex: 0 0 280px; +} + +.brandName { + font-size: 28px; + font-weight: 800; + color: #000; + margin: 0 0 16px 0; + letter-spacing: -1px; +} + +.brandDescription { + font-size: 13px; + line-height: 1.6; + color: #666; + margin: 0 0 24px 0; +} + +.socialLinks { + display: flex; + gap: 12px; +} + +.socialLink { + width: 36px; + height: 36px; + background: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + color: #000; + transition: all 0.3s ease; + border: 1px solid #E0E0E0; +} + +.socialLink:hover { + background: #000; + color: #fff; + transform: translateY(-3px); +} + +.linksGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 40px; + flex: 1; +} + +.linksColumn { + display: flex; + flex-direction: column; +} + +.columnTitle { + font-size: 13px; + font-weight: 700; + color: #000; + text-transform: uppercase; + letter-spacing: 1px; + margin: 0 0 20px 0; +} + +.linksList { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 14px; +} + +.link { + font-size: 13px; + color: #666; + text-decoration: none; + transition: color 0.3s ease; +} + +.link:hover { + color: #000; +} + +.bottomBar { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 30px; + border-top: 1px solid #D0D0D0; +} + +.copyright { + font-size: 13px; + color: #666; + margin: 0; +} + +.paymentMethods { + display: flex; + gap: 10px; +} + +.paymentIcon { + padding: 6px 12px; + background: #fff; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + color: #666; + border: 1px solid #E0E0E0; +} + +/* Адаптивность */ +@media (max-width: 1024px) { + .newsletterContainer { + flex-direction: column; + text-align: center; + padding: 40px 30px; + } + + .newsletterForm { + width: 100%; + flex: none; + } + + .mainContent { + flex-direction: column; + gap: 40px; + } + + .brand { + flex: none; + } + + .linksGrid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 640px) { + .newsletterSection { + margin-top: -40px; + padding: 0 15px; + } + + .newsletterTitle { + font-size: 28px; + } + + .mainFooter { + padding: 60px 15px 30px; + } + + .linksGrid { + grid-template-columns: 1fr; + gap: 30px; + } + + .brandName { + font-size: 24px; + } + + .bottomBar { + flex-direction: column; + gap: 20px; + text-align: center; + } + + .paymentMethods { + flex-wrap: wrap; + justify-content: center; + } +} \ No newline at end of file diff --git a/frontend/src/components/Footer/Footer.tsx b/frontend/src/components/Footer/Footer.tsx new file mode 100644 index 0000000..63a1ef0 --- /dev/null +++ b/frontend/src/components/Footer/Footer.tsx @@ -0,0 +1,221 @@ +import { useState } from 'react' +import styles from './Footer.module.css' + +const footerLinks = { + company: [ + { name: 'About', href: '#' }, + { name: 'Features', href: '#' }, + { name: 'Works', href: '#' }, + { name: 'Career', href: '#' }, + ], + help: [ + { name: 'Customer Support', href: '#' }, + { name: 'Delivery Details', href: '#' }, + { name: 'Terms & Conditions', href: '#' }, + { name: 'Privacy Policy', href: '#' }, + ], + faq: [ + { name: 'Account', href: '#' }, + { name: 'Manage Deliveries', href: '#' }, + { name: 'Orders', href: '#' }, + { name: 'Payments', href: '#' }, + ], + resources: [ + { name: 'Free eBooks', href: '#' }, + { name: 'Development Tutorial', href: '#' }, + { name: 'How to - Blog', href: '#' }, + { name: 'Youtube Playlist', href: '#' }, + ], +} + +function Footer() { + const [email, setEmail] = useState('') + const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') + const [message, setMessage] = useState('') + + const currentYear = new Date().getFullYear(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!email) { + setStatus('error') + setMessage('Please enter your email') + return + } + + setStatus('loading') + + try { + const response = await fetch('http://localhost:8000/api/reviews/newsletter/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }) + + const data = await response.json() + + if (response.ok) { + setStatus('success') + setMessage(data.message) + setEmail('') + setTimeout(() => setStatus('idle'), 3000) + } else { + setStatus('error') + setMessage(data.error || data.message) + } + } catch (error) { + setStatus('error') + setMessage('Something went wrong. Please try again.') + } + } + + return ( + + ) +} + +export default Footer \ No newline at end of file diff --git a/frontend/src/components/Reviews/Reviews.module.css b/frontend/src/components/Reviews/Reviews.module.css new file mode 100644 index 0000000..73ec0ad --- /dev/null +++ b/frontend/src/components/Reviews/Reviews.module.css @@ -0,0 +1,133 @@ +.section { + padding: 80px 25px; + max-width: 1200px; + margin: 0 auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 40px; +} + +.title { + font-size: 48px; + font-weight: 800; + text-transform: uppercase; + color: #000; + letter-spacing: -1px; + margin: 0; +} + +.navigation { + display: flex; + gap: 12px; +} + +.navButton { + width: 48px; + height: 48px; + border: 2px solid #000; + background: transparent; + border-radius: 50%; + cursor: pointer; + font-size: 20px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.navButton:hover { + background: #000; + color: #fff; +} + +.reviewsGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 24px; +} + +.reviewCard { + background: #fff; + border: 1px solid #E0E0E0; + border-radius: 16px; + padding: 32px; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +.reviewCard:hover { + transform: translateY(-5px); + box-shadow: 0 10px 30px rgba(0,0,0,0.1); +} + +.stars { + display: flex; + gap: 4px; + margin-bottom: 16px; + font-size: 20px; +} + +.starFilled { + color: #FFC107; +} + +.starEmpty { + color: #E0E0E0; +} + +.customer { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; +} + +.customerName { + font-size: 18px; + font-weight: 700; + color: #000; + margin: 0; +} + +.verifiedBadge { + width: 20px; + height: 20px; + background: #10B981; + color: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; +} + +.comment { + font-size: 14px; + line-height: 1.6; + color: #666; + margin: 0; +} + +.loading, .empty { + text-align: center; + padding: 80px; + font-size: 18px; + color: #666; +} + +@media (max-width: 968px) { + .reviewsGrid { + grid-template-columns: 1fr; + } + .title { + font-size: 32px; + } + .header { + flex-direction: column; + gap: 20px; + text-align: center; + } +} \ No newline at end of file diff --git a/frontend/src/components/Reviews/Reviews.tsx b/frontend/src/components/Reviews/Reviews.tsx new file mode 100644 index 0000000..51fd2db --- /dev/null +++ b/frontend/src/components/Reviews/Reviews.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState } from 'react' +import styles from './Reviews.module.css' + +interface Review { + id: number + customer_name: string + rating: number + comment: string + verified: boolean +} + +function Reviews() { + const [reviews, setReviews] = useState([]) + const [loading, setLoading] = useState(true) + const [currentIndex, setCurrentIndex] = useState(0) + + useEffect(() => { + fetch('http://localhost:8000/api/reviews/latest/') + .then(res => res.json()) + .then(data => { + setReviews(data) + setLoading(false) + }) + .catch(err => { + console.error('Error fetching reviews:', err) + setLoading(false) + }) + }, []) + + const nextReview = () => { + setCurrentIndex((prev) => + prev + 3 >= reviews.length ? 0 : prev + 1 + ) + } + + const prevReview = () => { + setCurrentIndex((prev) => + prev - 1 < 0 ? Math.max(0, reviews.length - 3) : prev - 1 + ) + } + + const renderStars = (rating: number) => { + return Array.from({ length: 5 }, (_, i) => ( + + ★ + + )) + } + + if (loading) return
Loading reviews...
+ if (reviews.length === 0) return
No reviews yet
+ + return ( +
+
+

OUR HAPPY CUSTOMERS

+
+ + +
+
+ +
+ {reviews.slice(currentIndex, currentIndex + 3).map((review) => ( +
+
+ {renderStars(review.rating)} +
+
+

{review.customer_name}

+ {review.verified && ✓} +
+

"{review.comment}"

+
+ ))} +
+
+ ) +} + +export default Reviews \ No newline at end of file diff --git a/frontend/src/pages/Home/Home.tsx b/frontend/src/pages/Home/Home.tsx index 419f738..1ced32a 100644 --- a/frontend/src/pages/Home/Home.tsx +++ b/frontend/src/pages/Home/Home.tsx @@ -5,6 +5,8 @@ import Brends from '../../components/Brends/Brends' import Arrivals from '../../components/NewArrivals/NewArrivals' import TopSelling from '../../components/TopSelling/TopSelling' import DressStyle from '../../components/DressStyle/DressStyle' +import Reviews from '../../components/Reviews/Reviews' +import Footer from '../../components/Footer/Footer' function Home() { return ( @@ -22,6 +24,10 @@ function Home() { + + + +