add footer component

This commit is contained in:
Nikita 2026-09-19 10:04:29 +05:00
parent 0440f95abf
commit f19c6a27d5
18 changed files with 926 additions and 9 deletions

View file

@ -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()),
]

View file

@ -38,10 +38,11 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
# =============================
"corsheaders",
"rest_framework",
"api",
"dressstyle",
'corsheaders',
'rest_framework',
'api',
'dressstyle',
'reviews',
]
MIDDLEWARE = [

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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 (
<footer className={styles.footer}>
{/* Newsletter секция */}
<div className={styles.newsletterSection}>
<div className={styles.newsletterContainer}>
<div className={styles.newsletterContent}>
<h2 className={styles.newsletterTitle}>
STAY UPTO DATE ABOUT<br />OUR LATEST OFFERS
</h2>
</div>
<form onSubmit={handleSubmit} className={styles.newsletterForm}>
<div className={styles.inputWrapper}>
<svg className={styles.emailIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
<polyline points="22,6 12,13 2,6" />
</svg>
<input
type="email"
placeholder="Enter your email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
className={styles.emailInput}
disabled={status === 'loading' || status === 'success'}
/>
</div>
<button
type="submit"
className={styles.subscribeButton}
disabled={status === 'loading' || status === 'success'}
>
{status === 'loading' ? 'Subscribing...' : 'Subscribe to Newsletter'}
</button>
{message && (
<div className={`${styles.message} ${status === 'success' ? styles.success : styles.error}`}>
{message}
</div>
)}
</form>
</div>
</div>
{/* Основной footer */}
<div className={styles.mainFooter}>
<div className={styles.container}>
<div className={styles.mainContent}>
{/* Бренд */}
<div className={styles.brand}>
<h3 className={styles.brandName}>SHOP.CO</h3>
<p className={styles.brandDescription}>
We have clothes that suits your style and which you're proud to wear. From women to men.
</p>
{/* Социальные иконки */}
<div className={styles.socialLinks}>
<a href="#" className={styles.socialLink} aria-label="Twitter">
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
</svg>
</a>
<a href="#" className={styles.socialLink} aria-label="Facebook">
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
</a>
<a href="#" className={styles.socialLink} aria-label="Instagram">
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
</svg>
</a>
<a href="#" className={styles.socialLink} aria-label="GitHub">
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
</div>
</div>
{/* Колонки ссылок */}
<div className={styles.linksGrid}>
<div className={styles.linksColumn}>
<h4 className={styles.columnTitle}>COMPANY</h4>
<ul className={styles.linksList}>
{footerLinks.company.map((link) => (
<li key={link.name}>
<a href={link.href} className={styles.link}>{link.name}</a>
</li>
))}
</ul>
</div>
<div className={styles.linksColumn}>
<h4 className={styles.columnTitle}>HELP</h4>
<ul className={styles.linksList}>
{footerLinks.help.map((link) => (
<li key={link.name}>
<a href={link.href} className={styles.link}>{link.name}</a>
</li>
))}
</ul>
</div>
<div className={styles.linksColumn}>
<h4 className={styles.columnTitle}>FAQ</h4>
<ul className={styles.linksList}>
{footerLinks.faq.map((link) => (
<li key={link.name}>
<a href={link.href} className={styles.link}>{link.name}</a>
</li>
))}
</ul>
</div>
<div className={styles.linksColumn}>
<h4 className={styles.columnTitle}>RESOURCES</h4>
<ul className={styles.linksList}>
{footerLinks.resources.map((link) => (
<li key={link.name}>
<a href={link.href} className={styles.link}>{link.name}</a>
</li>
))}
</ul>
</div>
</div>
</div>
{/* Нижняя часть */}
<div className={styles.bottomBar}>
<p className={styles.copyright}>
Shop.co © 2000-{currentYear}, All Rights Reserved
</p>
<div className={styles.paymentMethods}>
<div className={styles.paymentIcon}>VISA</div>
<div className={styles.paymentIcon}>Mastercard</div>
<div className={styles.paymentIcon}>PayPal</div>
<div className={styles.paymentIcon}>Apple Pay</div>
<div className={styles.paymentIcon}>Google Pay</div>
</div>
</div>
</div>
</div>
</footer>
)
}
export default Footer

View file

@ -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;
}
}

View file

@ -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<Review[]>([])
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) => (
<span key={i} className={i < rating ? styles.starFilled : styles.starEmpty}>
★
</span>
))
}
if (loading) return <div className={styles.loading}>Loading reviews...</div>
if (reviews.length === 0) return <div className={styles.empty}>No reviews yet</div>
return (
<section className={styles.section}>
<div className={styles.header}>
<h2 className={styles.title}>OUR HAPPY CUSTOMERS</h2>
<div className={styles.navigation}>
<button onClick={prevReview} className={styles.navButton}>←</button>
<button onClick={nextReview} className={styles.navButton}>→</button>
</div>
</div>
<div className={styles.reviewsGrid}>
{reviews.slice(currentIndex, currentIndex + 3).map((review) => (
<div key={review.id} className={styles.reviewCard}>
<div className={styles.stars}>
{renderStars(review.rating)}
</div>
<div className={styles.customer}>
<h3 className={styles.customerName}>{review.customer_name}</h3>
{review.verified && <span className={styles.verifiedBadge}>✓</span>}
</div>
<p className={styles.comment}>"{review.comment}"</p>
</div>
))}
</div>
</section>
)
}
export default Reviews

View file

@ -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() {
<TopSelling />
<DressStyle />
<Reviews />
<Footer />
</>
)
}

0
reviews/__init__.py Normal file
View file

18
reviews/admin.py Normal file
View file

@ -0,0 +1,18 @@
from django.contrib import admin
from .models import Review, NewsletterSubscription
@admin.register(Review)
class ReviewAdmin(admin.ModelAdmin):
list_display = ("customer_name", "rating", "verified", "created_at")
list_filter = ("verified", "rating", "created_at")
search_fields = ("customer_name", "comment")
list_editable = ("verified", "rating")
@admin.register(NewsletterSubscription)
class NewsletterSubscriptionAdmin(admin.ModelAdmin):
list_display = ("email", "subscribed_at", "is_active")
list_filter = ("is_active", "subscribed_at")
search_fields = ("email",)
readonly_fields = ("subscribed_at",)

5
reviews/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ReviewsConfig(AppConfig):
name = 'reviews'

View file

@ -0,0 +1,37 @@
# Generated by Django 6.1.1 on 2026-09-19 04:05
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='NewsletterSubscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254, unique=True)),
('subscribed_at', models.DateTimeField(auto_now_add=True)),
('is_active', models.BooleanField(default=True)),
],
),
migrations.CreateModel(
name='Review',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('customer_name', models.CharField(max_length=100)),
('rating', models.IntegerField(choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)])),
('comment', models.TextField()),
('verified', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'ordering': ['-created_at'],
},
),
]

View file

23
reviews/models.py Normal file
View file

@ -0,0 +1,23 @@
from django.db import models
class Review(models.Model):
customer_name = models.CharField(max_length=100)
rating = models.IntegerField(choices=[(i, i) for i in range(1, 6)])
comment = models.TextField()
verified = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Review by {self.customer_name}"
class Meta:
ordering = ['-created_at']
class NewsletterSubscription(models.Model):
email = models.EmailField(unique=True)
subscribed_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
def __str__(self):
return self.email

14
reviews/serializers.py Normal file
View file

@ -0,0 +1,14 @@
from rest_framework import serializers
from .models import Review, NewsletterSubscription
class ReviewSerializer(serializers.ModelSerializer):
class Meta:
model = Review
fields = ["id", "customer_name", "rating", "comment", "verified", "created_at"]
class NewsletterSerializer(serializers.ModelSerializer):
class Meta:
model = NewsletterSubscription
fields = ["email"]

3
reviews/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
reviews/urls.py Normal file
View file

@ -0,0 +1,8 @@
from django.urls import path
from .views import ReviewListView, LatestReviewsView, NewsletterCreateView
urlpatterns = [
path("", ReviewListView.as_view()),
path("latest/", LatestReviewsView.as_view()),
path("newsletter/", NewsletterCreateView.as_view()),
]

47
reviews/views.py Normal file
View file

@ -0,0 +1,47 @@
from rest_framework import generics, status
from rest_framework.response import Response
from .models import Review, NewsletterSubscription
from .serializers import ReviewSerializer, NewsletterSerializer
class ReviewListView(generics.ListAPIView):
"""Все верифицированные отзывы"""
queryset = Review.objects.filter(verified=True)
serializer_class = ReviewSerializer
class LatestReviewsView(generics.ListAPIView):
"""Последние 6 отзывов для слайдера"""
serializer_class = ReviewSerializer
def get_queryset(self):
return Review.objects.filter(verified=True).order_by("-created_at")[:6]
class NewsletterCreateView(generics.CreateAPIView):
"""Подписка на newsletter"""
serializer_class = NewsletterSerializer
def create(self, request, *args, **kwargs):
email = request.data.get("email")
if not email:
return Response(
{"error": "Email is required"},
status=status.HTTP_400_BAD_REQUEST,
)
if NewsletterSubscription.objects.filter(email=email).exists():
return Response(
{"message": "You are already subscribed!"},
status=status.HTTP_200_OK,
)
serializer = self.get_serializer(data={"email": email})
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
return Response(
{"message": "Successfully subscribed to newsletter!"},
status=status.HTTP_201_CREATED,
)