This commit is contained in:
dvirlabs 2026-05-04 01:01:36 +03:00
parent 01cdb9a8c6
commit 6630045f67
27 changed files with 961 additions and 123 deletions

View File

@ -163,7 +163,7 @@ FRONTEND_URL=http://localhost:5173
```
VITE_API_URL=http://localhost:8000/api
VITE_APP_NAME=StyleHub
VITE_APP_NAME=Brand Master
```
## Common Commands

View File

@ -129,7 +129,7 @@ cp .env.example .env
Edit `.env`:
```
VITE_API_URL=http://localhost:8000/api
VITE_APP_NAME=StyleHub
VITE_APP_NAME=Brand Master
```
### 3. Run Development Server

View File

@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from datetime import timedelta
from datetime import timedelta, datetime
from pydantic import BaseModel, EmailStr
from app.database.database import get_db
from app.models import User
from app.schemas.user import UserCreate, UserResponse
@ -15,6 +16,15 @@ from app.config import settings
router = APIRouter(prefix="/api/auth", tags=["auth"])
class ForgotPasswordRequest(BaseModel):
email: EmailStr
class ResetPasswordRequest(BaseModel):
token: str
new_password: str
@router.post("/register", response_model=UserResponse)
def register(user: UserCreate, db: Session = Depends(get_db)):
db_user = db.query(User).filter(User.email == user.email).first()
@ -66,3 +76,57 @@ def verify_token_endpoint(token: str):
detail="Invalid token",
)
return {"user_id": user_id, "valid": True}
@router.post("/forgot-password")
def forgot_password(request: ForgotPasswordRequest, db: Session = Depends(get_db)):
"""
Initiate password reset process.
In a production app, this would send an email with a reset link.
For now, we'll generate a reset token that can be used.
"""
user = db.query(User).filter(User.email == request.email).first()
if not user:
# Don't reveal if email exists or not for security
return {"message": "If the email exists, a reset link has been sent"}
# Create a password reset token (valid for 1 hour)
reset_token = create_access_token(
data={"sub": str(user.id), "type": "password_reset"},
expires_delta=timedelta(hours=1)
)
# In production, send this token via email
# For development, we'll just return it
print(f"Password reset token for {request.email}: {reset_token}")
return {
"message": "If the email exists, a reset link has been sent",
"reset_token": reset_token # Remove this in production
}
@router.post("/reset-password")
def reset_password(request: ResetPasswordRequest, db: Session = Depends(get_db)):
"""
Reset password using the token from forgot-password endpoint.
"""
user_id = verify_token(request.token)
if user_id is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid or expired reset token",
)
user = db.query(User).filter(User.id == int(user_id)).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
# Update password
user.hashed_password = get_password_hash(request.new_password)
db.commit()
return {"message": "Password reset successful"}

View File

@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
from typing import List, Optional
from pathlib import Path
from app.database.database import get_db
@ -38,7 +38,7 @@ def list_products(
limit: int = 20,
db: Session = Depends(get_db),
):
query = db.query(Product)
query = db.query(Product).options(joinedload(Product.model))
if category_id:
query = query.filter(Product.category_id == category_id)
@ -57,12 +57,26 @@ def list_products(
if featured is not None:
query = query.filter(Product.is_featured == featured)
return query.offset(skip).limit(limit).all()
products = query.offset(skip).limit(limit).all()
# Inherit sizes from model if product doesn't have sizes
for product in products:
if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model:
product.sizes = product.model.sizes or []
return products
@router.get("/search", response_model=List[ProductResponse])
def search(q: str, skip: int = 0, limit: int = 20, db: Session = Depends(get_db)):
return search_products(db, q, skip=skip, limit=limit)
products = search_products(db, q, skip=skip, limit=limit)
# Inherit sizes from model if product doesn't have sizes
for product in products:
if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model:
product.sizes = product.model.sizes or []
return products
@router.get("/{product_id}", response_model=ProductResponse)
@ -70,6 +84,11 @@ def get_product(product_id: int, db: Session = Depends(get_db)):
product = get_product_by_id(db, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
# If product doesn't have sizes but has a model, inherit sizes from model
if (not product.sizes or len(product.sizes) == 0) and product.model_id and product.model:
product.sizes = product.model.sizes or []
return product

View File

@ -1,4 +1,4 @@
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
from app.models import Product, Category
from app.schemas.product import ProductCreate, ProductUpdate
from typing import List, Optional
@ -13,7 +13,7 @@ def get_products(
skip: int = 0,
limit: int = 10,
) -> List[Product]:
query = db.query(Product)
query = db.query(Product).options(joinedload(Product.model))
if category_id:
query = query.filter(Product.category_id == category_id)
@ -28,7 +28,7 @@ def get_products(
def get_product_by_id(db: Session, product_id: int) -> Optional[Product]:
return db.query(Product).filter(Product.id == product_id).first()
return db.query(Product).options(joinedload(Product.model)).filter(Product.id == product_id).first()
def create_product(db: Session, product: ProductCreate) -> Product:
@ -67,6 +67,7 @@ def delete_product(db: Session, product_id: int) -> bool:
def search_products(db: Session, query: str, skip: int = 0, limit: int = 10) -> List[Product]:
return (
db.query(Product)
.options(joinedload(Product.model))
.filter(
Product.name.ilike(f"%{query}%") | Product.brand.ilike(f"%{query}%")
)

View File

@ -1,2 +1,2 @@
VITE_API_URL=http://localhost:8000/api
VITE_APP_NAME=StyleHub
VITE_APP_NAME=Brand Master

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>StyleHub - Fashion & Shoe Store</title>
<title>Brand Master - Fashion & Shoe Store</title>
</head>
<body>
<div id="root"></div>

View File

@ -0,0 +1,36 @@
import React from 'react'
import { useNavigate } from 'react-router-dom'
import '../styles/global.css'
export default function AddToCartModal({ isOpen, onClose, productName }) {
const navigate = useNavigate()
if (!isOpen) return null
const handleGoToCart = () => {
navigate('/cart')
onClose()
}
const handleContinueShopping = () => {
onClose()
}
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content cart-modal" onClick={(e) => e.stopPropagation()}>
<div className="cart-modal-icon"></div>
<h2>Added to Cart!</h2>
<p className="cart-modal-message">{productName} has been added to your cart</p>
<div className="cart-modal-actions">
<button className="btn btn-secondary" onClick={handleContinueShopping}>
Continue Shopping
</button>
<button className="btn" onClick={handleGoToCart}>
Go to Cart
</button>
</div>
</div>
</div>
)
}

View File

@ -7,13 +7,14 @@ export default function Footer() {
<footer className="footer">
<div className="footer-container">
<div className="footer-section">
<h3>StyleHub</h3>
<div className="footer-brand">
<span className="footer-logo">👟</span>
<h3>Brand Master</h3>
</div>
<p>Your ultimate destination for fashion and footwear.</p>
<div className="social-links">
<a href="#" title="Facebook">f</a>
<a href="#" title="Twitter">𝕏</a>
<a href="#" title="Instagram">📷</a>
<a href="#" title="LinkedIn">in</a>
<a href="https://instagram.com" target="_blank" rel="noopener noreferrer" title="Instagram">📷</a>
<a href="https://wa.me/972532441361" target="_blank" rel="noopener noreferrer" title="WhatsApp">💬</a>
</div>
</div>
@ -40,14 +41,13 @@ export default function Footer() {
<div className="footer-section">
<h4>Contact</h4>
<p>Email: info@stylehub.com</p>
<p>Phone: +1 (555) 123-4567</p>
<p>Address: 123 Fashion St, NY 10001</p>
<p>Email: info@brandmaster.com</p>
<p>Phone: +972 53-244-1361</p>
</div>
</div>
<div className="footer-bottom">
<p>&copy; 2024 StyleHub. All rights reserved.</p>
<p>&copy; 2026 Brand Master. All rights reserved.</p>
</div>
</footer>
)

View File

@ -0,0 +1,22 @@
import React from 'react'
import '../styles/global.css'
export default function Modal({ isOpen, onClose, children, title }) {
if (!isOpen) return null
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{title && (
<div className="modal-header">
<h2>{title}</h2>
<button className="modal-close" onClick={onClose}>
×
</button>
</div>
)}
<div className="modal-body">{children}</div>
</div>
</div>
)
}

View File

@ -1,5 +1,5 @@
import React, { useContext } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Link, useNavigate, useLocation } from 'react-router-dom'
import { AuthContext } from '../context/AuthContext'
import { CartContext } from '../context/CartContext'
import SearchBar from './SearchBar'
@ -9,17 +9,22 @@ export default function Navbar() {
const { user, token, logout } = useContext(AuthContext)
const { cart } = useContext(CartContext)
const navigate = useNavigate()
const location = useLocation()
const handleLogout = () => {
logout()
navigate('/')
}
const isActive = (path) => {
return location.pathname === path ? 'active' : ''
}
return (
<nav className="navbar">
<div className="navbar-container">
<Link to="/" className="navbar-logo">
<span className="logo-icon">👟</span> StyleHub
<span className="logo-icon">👟</span> Brand Master
</Link>
<div className="navbar-center">
@ -27,13 +32,13 @@ export default function Navbar() {
</div>
<ul className="navbar-menu">
<li><Link to="/">Home</Link></li>
<li><Link to="/products">Shop</Link></li>
<li><Link to="/sales">Sales</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/contact">Contact</Link></li>
<li><Link to="/" className={isActive('/')}>Home</Link></li>
<li><Link to="/products" className={isActive('/products')}>Shop</Link></li>
<li><Link to="/sales" className={isActive('/sales')}>Sales</Link></li>
<li><Link to="/about" className={isActive('/about')}>About</Link></li>
<li><Link to="/contact" className={isActive('/contact')}>Contact</Link></li>
{user?.is_admin && (
<li><Link to="/admin" style={{ color: '#ff6b6b', fontWeight: 'bold' }}>Admin</Link></li>
<li><Link to="/admin" className={`admin-link ${isActive('/admin')}`}>Admin</Link></li>
)}
</ul>

View File

@ -0,0 +1,25 @@
import React, { useEffect } from 'react'
import '../styles/global.css'
export default function Toast({ message, type = 'success', onClose, duration = 3000 }) {
useEffect(() => {
if (duration > 0) {
const timer = setTimeout(onClose, duration)
return () => clearTimeout(timer)
}
}, [duration, onClose])
return (
<div className={`toast toast-${type}`}>
<div className="toast-icon">
{type === 'success' && '✓'}
{type === 'error' && '✕'}
{type === 'info' && ''}
</div>
<div className="toast-message">{message}</div>
<button className="toast-close" onClick={onClose}>
×
</button>
</div>
)
}

View File

@ -4,12 +4,12 @@ import '../styles/global.css'
export default function About() {
return (
<div className="about-page">
<h1>About StyleHub</h1>
<h1>About Brand Master</h1>
<section className="about-section">
<h2>Our Story</h2>
<p>
Founded in 2020, StyleHub started as a passion project to bring
Founded in 2020, Brand Master started as a passion project to bring
quality fashion and footwear to customers worldwide. What began as
a small collection has grown into a comprehensive online store
featuring over 1000 products from leading brands.

View File

@ -2,11 +2,13 @@ import React, { useState, useEffect, useContext } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import api from '../api'
import { AuthContext } from '../context/AuthContext'
import Toast from '../components/Toast'
import '../styles/global.css'
export default function Admin() {
const navigate = useNavigate()
const { user, token } = useContext(AuthContext)
const [toast, setToast] = useState(null)
const [products, setProducts] = useState([])
const [categories, setCategories] = useState([])
const [loading, setLoading] = useState(true)
@ -187,10 +189,10 @@ export default function Admin() {
const allImages = [...currentImages, ...newImages.map(url => url)]
setFormData({ ...formData, images: allImages.join(', ') })
alert('Images uploaded successfully!')
setToast({ type: 'success', message: 'Images uploaded successfully!' })
} catch (error) {
console.error('Error uploading image:', error)
alert('Error uploading image: ' + (error.response?.data?.detail || 'Unknown error'))
setToast({ type: 'error', message: 'Error uploading image: ' + (error.response?.data?.detail || 'Unknown error') })
} finally {
setUploadingImage(false)
}
@ -222,10 +224,10 @@ export default function Admin() {
try {
if (editingProduct) {
await api.put(`/products/${editingProduct.id}`, productData)
alert('Product updated successfully!')
setToast({ type: 'success', message: 'Product updated successfully!' })
} else {
await api.post('/products', productData)
alert('Product created successfully!')
setToast({ type: 'success', message: 'Product created successfully!' })
}
setShowForm(false)
@ -234,7 +236,7 @@ export default function Admin() {
fetchProducts()
} catch (error) {
console.error('Error saving product:', error)
alert('Error saving product: ' + (error.response?.data?.detail || 'Unknown error'))
setToast({ type: 'error', message: 'Error saving product: ' + (error.response?.data?.detail || 'Unknown error') })
}
}
@ -273,7 +275,7 @@ export default function Admin() {
}, 100)
} catch (error) {
console.error('Error in handleEdit:', error)
alert('Error loading product: ' + error.message)
setToast({ type: 'error', message: 'Error loading product: ' + error.message })
}
}
@ -282,11 +284,11 @@ export default function Admin() {
try {
await api.delete(`/products/${id}`)
alert('Product deleted successfully!')
setToast({ type: 'success', message: 'Product deleted successfully!' })
fetchProducts()
} catch (error) {
console.error('Error deleting product:', error)
alert('Error deleting product')
setToast({ type: 'error', message: 'Error deleting product' })
}
}
@ -341,10 +343,10 @@ export default function Admin() {
? response.data.url
: `http://localhost:8000${response.data.url}`
setCategoryFormData({ ...categoryFormData, image: imageUrl })
alert('Image uploaded successfully!')
setToast({ type: 'success', message: 'Image uploaded successfully!' })
} catch (error) {
console.error('Error uploading image:', error)
alert('Error uploading image')
setToast({ type: 'error', message: 'Error uploading image' })
} finally {
setUploadingImage(false)
}
@ -356,10 +358,10 @@ export default function Admin() {
try {
if (editingCategory) {
await api.put(`/categories/${editingCategory.id}`, categoryFormData)
alert('Category updated successfully!')
setToast({ type: 'success', message: 'Category updated successfully!' })
} else {
await api.post('/categories', categoryFormData)
alert('Category created successfully!')
setToast({ type: 'success', message: 'Category created successfully!' })
}
setShowCategoryForm(false)
setEditingCategory(null)
@ -367,7 +369,7 @@ export default function Admin() {
fetchCategories()
} catch (error) {
console.error('Error saving category:', error)
alert('Error saving category: ' + (error.response?.data?.detail || 'Unknown error'))
setToast({ type: 'error', message: 'Error saving category: ' + (error.response?.data?.detail || 'Unknown error') })
}
}
@ -390,11 +392,11 @@ export default function Admin() {
try {
await api.delete(`/categories/${id}`)
alert('Category deleted successfully!')
setToast({ type: 'success', message: 'Category deleted successfully!' })
fetchCategories()
} catch (error) {
console.error('Error deleting category:', error)
alert('Error deleting category: ' + (error.response?.data?.detail || 'Unknown error'))
setToast({ type: 'error', message: 'Error deleting category: ' + (error.response?.data?.detail || 'Unknown error') })
}
}
@ -412,7 +414,7 @@ export default function Admin() {
const newBrand = brandFormData.name.trim()
if (!newBrand) {
alert('Please enter a brand name')
setToast({ type: 'error', message: 'Please enter a brand name' })
return
}
@ -421,10 +423,10 @@ export default function Admin() {
setShowBrandForm(false)
setBrandFormData({ name: '' })
fetchBrands()
alert('Brand added successfully!')
setToast({ type: 'success', message: 'Brand added successfully!' })
} catch (error) {
console.error('Error adding brand:', error)
alert('Error adding brand: ' + (error.response?.data?.detail || 'Unknown error'))
setToast({ type: 'error', message: 'Error adding brand: ' + (error.response?.data?.detail || 'Unknown error') })
}
}
@ -433,7 +435,7 @@ export default function Admin() {
const newBrand = brandFormData.name.trim()
if (!newBrand) {
alert('Please enter a brand name')
setToast({ type: 'error', message: 'Please enter a brand name' })
return
}
@ -451,7 +453,7 @@ export default function Admin() {
// Find the brand ID
const brandToUpdate = brandsList.find(b => b.name === oldBrand)
if (!brandToUpdate) {
alert('Brand not found')
setToast({ type: 'error', message: 'Brand not found' })
return
}
@ -479,10 +481,10 @@ export default function Admin() {
fetchProducts()
fetchModels()
alert('Brand updated successfully!')
setToast({ type: 'success', message: 'Brand updated successfully!' })
} catch (error) {
console.error('Error updating brand:', error)
alert('Error updating brand: ' + (error.response?.data?.detail || 'Unknown error'))
setToast({ type: 'error', message: 'Error updating brand: ' + (error.response?.data?.detail || 'Unknown error') })
}
}
@ -491,7 +493,7 @@ export default function Admin() {
const modelCount = models.filter(m => m.brand === brand).length
if (productCount > 0 || modelCount > 0) {
alert(`Cannot delete "${brand}". It is used in ${productCount} product(s) and ${modelCount} model(s).`)
setToast({ type: 'error', message: `Cannot delete "${brand}". It is used in ${productCount} product(s) and ${modelCount} model(s).` })
return
}
@ -503,16 +505,16 @@ export default function Admin() {
// Find the brand ID
const brandToDelete = brandsList.find(b => b.name === brand)
if (!brandToDelete) {
alert('Brand not found')
setToast({ type: 'error', message: 'Brand not found' })
return
}
await api.delete(`/brands/${brandToDelete.id}`)
fetchBrands()
alert('Brand deleted successfully!')
setToast({ type: 'success', message: 'Brand deleted successfully!' })
} catch (error) {
console.error('Error deleting brand:', error)
alert('Error deleting brand: ' + (error.response?.data?.detail || 'Unknown error'))
setToast({ type: 'error', message: 'Error deleting brand: ' + (error.response?.data?.detail || 'Unknown error') })
}
}
@ -536,10 +538,10 @@ export default function Admin() {
try {
if (editingModel) {
await api.put(`/models/${editingModel.id}`, modelData)
alert('Model updated successfully!')
setToast({ type: 'success', message: 'Model updated successfully!' })
} else {
await api.post('/models', modelData)
alert('Model created successfully!')
setToast({ type: 'success', message: 'Model created successfully!' })
}
setShowModelForm(false)
setEditingModel(null)
@ -547,7 +549,7 @@ export default function Admin() {
fetchModels()
} catch (error) {
console.error('Error saving model:', error)
alert('Error saving model: ' + (error.response?.data?.detail || 'Unknown error'))
setToast({ type: 'error', message: 'Error saving model: ' + (error.response?.data?.detail || 'Unknown error') })
}
}
@ -573,11 +575,11 @@ export default function Admin() {
try {
await api.delete(`/models/${id}`)
alert('Model deleted successfully!')
setToast({ type: 'success', message: 'Model deleted successfully!' })
fetchModels()
} catch (error) {
console.error('Error deleting model:', error)
alert('Error deleting model: ' + (error.response?.data?.detail || 'Unknown error'))
setToast({ type: 'error', message: 'Error deleting model: ' + (error.response?.data?.detail || 'Unknown error') })
}
}
@ -1514,6 +1516,14 @@ export default function Admin() {
</div>
</>
)}
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/>
)}
</div>
)
}

View File

@ -1,19 +1,21 @@
import React, { useContext } from 'react'
import React, { useContext, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { CartContext } from '../context/CartContext'
import { AuthContext } from '../context/AuthContext'
import Toast from '../components/Toast'
import '../styles/global.css'
export default function Cart() {
const { cart, removeFromCart, updateQuantity, total, clearCart } = useContext(CartContext)
const { token } = useContext(AuthContext)
const navigate = useNavigate()
const [toast, setToast] = useState(null)
const handleCheckout = () => {
if (!token) {
navigate('/login')
} else if (cart.length === 0) {
alert('Your cart is empty')
setToast({ type: 'info', message: 'Your cart is empty' })
} else {
navigate('/checkout')
}
@ -113,6 +115,14 @@ export default function Cart() {
</div>
</div>
)}
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/>
)}
</div>
)
}

View File

@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
import api from '../api'
import { CartContext } from '../context/CartContext'
import { AuthContext } from '../context/AuthContext'
import Toast from '../components/Toast'
import '../styles/global.css'
export default function Checkout() {
@ -10,37 +11,133 @@ export default function Checkout() {
const { cart, total, clearCart } = useContext(CartContext)
const { token, user } = useContext(AuthContext)
const [loading, setLoading] = useState(false)
const [toast, setToast] = useState(null)
const [formData, setFormData] = useState({
full_name: user?.username || '',
email: user?.email || '',
phone: '',
shipping_address: user?.address || '',
shipping_city: user?.city || '',
shipping_postal_code: user?.postal_code || '',
shipping_country: user?.country || '',
shipping_country: user?.country || 'Israel',
})
const [errors, setErrors] = useState({})
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
})
// Clear error when user starts typing
if (errors[e.target.name]) {
setErrors({
...errors,
[e.target.name]: null,
})
}
}
const validateForm = () => {
const newErrors = {}
if (!formData.full_name.trim()) {
newErrors.full_name = 'Full name is required'
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required'
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Please enter a valid email'
}
if (!formData.phone.trim()) {
newErrors.phone = 'Phone number is required'
}
if (!formData.shipping_address.trim()) {
newErrors.shipping_address = 'Address is required'
}
if (!formData.shipping_city.trim()) {
newErrors.shipping_city = 'City is required'
}
if (!formData.shipping_country.trim()) {
newErrors.shipping_country = 'Country is required'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const generateWhatsAppMessage = () => {
const orderTotal = total + 10 + total * 0.1
let message = `🛍️ *New Order from Brand Master*\n\n`
message += `📋 *Customer Information:*\n`
message += `Name: ${formData.full_name}\n`
message += `Email: ${formData.email}\n`
message += `Phone: ${formData.phone}\n\n`
message += `📍 *Shipping Address:*\n`
message += `${formData.shipping_address}\n`
message += `${formData.shipping_city}, ${formData.shipping_postal_code}\n`
message += `${formData.shipping_country}\n\n`
message += `🛒 *Order Items:*\n`
cart.forEach((item, index) => {
const itemPrice = item.product.discount_price || item.product.price
message += `${index + 1}. ${item.product.name}\n`
message += ` Quantity: ${item.quantity}\n`
message += ` Price: ₪${itemPrice.toFixed(2)} x ${item.quantity} = ₪${(itemPrice * item.quantity).toFixed(2)}\n\n`
})
message += `💰 *Order Summary:*\n`
message += `Subtotal: ₪${total.toFixed(2)}\n`
message += `Shipping: ₪10.00\n`
message += `Tax: ₪${(total * 0.1).toFixed(2)}\n`
message += `*Total: ₪${orderTotal.toFixed(2)}*\n`
return message
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!token) {
navigate('/login')
return
}
if (!validateForm()) {
setToast({ type: 'error', message: 'Please fill in all required fields' })
return
}
try {
setLoading(true)
// Generate WhatsApp message
const whatsappMessage = generateWhatsAppMessage()
const whatsappNumber = '972532441361'
const whatsappUrl = `https://wa.me/${whatsappNumber}?text=${encodeURIComponent(whatsappMessage)}`
// Save order to backend
const response = await api.post('/orders', formData)
alert('Order placed successfully!')
// Clear cart and show success
clearCart()
navigate(`/orders/${response.data.id}`)
setToast({ type: 'success', message: 'Order placed successfully!' })
// Open WhatsApp after a short delay
setTimeout(() => {
window.open(whatsappUrl, '_blank')
navigate('/orders')
}, 1500)
} catch (error) {
console.error('Error placing order:', error)
alert('Error placing order')
setToast({ type: 'error', message: 'Error placing order. Please try again.' })
} finally {
setLoading(false)
}
@ -52,30 +149,89 @@ export default function Checkout() {
<div className="checkout-container">
<form onSubmit={handleSubmit} className="checkout-form">
<div className="form-section">
<h2>Contact Information</h2>
<div className="form-group">
<label>
Full Name <span className="required">*</span>
</label>
<input
type="text"
name="full_name"
value={formData.full_name}
onChange={handleChange}
className={errors.full_name ? 'error' : ''}
placeholder="Enter your full name"
/>
{errors.full_name && <span className="error-message">{errors.full_name}</span>}
</div>
<div className="form-row">
<div className="form-group">
<label>
Email <span className="required">*</span>
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? 'error' : ''}
placeholder="your@email.com"
/>
{errors.email && <span className="error-message">{errors.email}</span>}
</div>
<div className="form-group">
<label>
Phone <span className="required">*</span>
</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
className={errors.phone ? 'error' : ''}
placeholder="05X-XXX-XXXX"
/>
{errors.phone && <span className="error-message">{errors.phone}</span>}
</div>
</div>
</div>
<div className="form-section">
<h2>Shipping Address</h2>
<div className="form-group">
<label>Address</label>
<label>
Street Address <span className="required">*</span>
</label>
<input
type="text"
name="shipping_address"
value={formData.shipping_address}
onChange={handleChange}
required
className={errors.shipping_address ? 'error' : ''}
placeholder="Street address, apartment, suite, etc."
/>
{errors.shipping_address && <span className="error-message">{errors.shipping_address}</span>}
</div>
<div className="form-row">
<div className="form-group">
<label>City</label>
<label>
City <span className="required">*</span>
</label>
<input
type="text"
name="shipping_city"
value={formData.shipping_city}
onChange={handleChange}
required
className={errors.shipping_city ? 'error' : ''}
placeholder="City"
/>
{errors.shipping_city && <span className="error-message">{errors.shipping_city}</span>}
</div>
<div className="form-group">
@ -85,30 +241,34 @@ export default function Checkout() {
name="shipping_postal_code"
value={formData.shipping_postal_code}
onChange={handleChange}
required
placeholder="Postal code"
/>
</div>
<div className="form-group">
<label>Country</label>
<label>
Country <span className="required">*</span>
</label>
<input
type="text"
name="shipping_country"
value={formData.shipping_country}
onChange={handleChange}
required
className={errors.shipping_country ? 'error' : ''}
placeholder="Country"
/>
{errors.shipping_country && <span className="error-message">{errors.shipping_country}</span>}
</div>
</div>
</div>
<div className="form-section">
<h2>Payment Method</h2>
<p>💳 Credit/Debit Card</p>
<p className="text-muted">Payment processing simulated</p>
<p className="payment-info">💳 Payment will be processed via WhatsApp</p>
<p className="text-muted">After placing your order, you'll be redirected to WhatsApp to confirm payment details.</p>
</div>
<button type="submit" className="btn btn-full" disabled={loading}>
<button type="submit" className="btn btn-full" disabled={loading || cart.length === 0}>
{loading ? 'Processing...' : 'Place Order'}
</button>
</form>
@ -128,6 +288,14 @@ export default function Checkout() {
</div>
</div>
</div>
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/>
)}
</div>
)
}

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react'
import api from '../api'
import Toast from '../components/Toast'
import '../styles/global.css'
export default function Contact() {
@ -11,6 +12,7 @@ export default function Contact() {
})
const [loading, setLoading] = useState(false)
const [submitted, setSubmitted] = useState(false)
const [toast, setToast] = useState(null)
const handleChange = (e) => {
setFormData({
@ -32,13 +34,14 @@ export default function Contact() {
subject: '',
message: '',
})
setToast({ type: 'success', message: 'Message sent successfully! We\'ll get back to you soon.' })
setTimeout(() => {
setSubmitted(false)
}, 3000)
} catch (error) {
console.error('Error sending message:', error)
alert('Error sending message')
setToast({ type: 'error', message: 'Error sending message. Please try again.' })
} finally {
setLoading(false)
}
@ -54,28 +57,43 @@ export default function Contact() {
<div className="info-item">
<h3>📞 Phone</h3>
<p>+1 (555) 123-4567</p>
<p>+972 53-244-1361</p>
</div>
<div className="info-item">
<h3> Email</h3>
<p>info@stylehub.com</p>
</div>
<div className="info-item">
<h3>📍 Address</h3>
<p>123 Fashion Street</p>
<p>New York, NY 10001</p>
<p>United States</p>
<p>info@brandmaster.com</p>
</div>
<div className="info-item">
<h3>Follow Us</h3>
<div className="social-links">
<a href="#">Facebook</a>
<a href="#">Twitter</a>
<a href="#">Instagram</a>
<a href="#">LinkedIn</a>
<div className="social-icons">
<a
href="https://instagram.com"
target="_blank"
rel="noopener noreferrer"
className="social-icon instagram"
title="Follow us on Instagram"
>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
</svg>
<span>Instagram</span>
</a>
<a
href="https://wa.me/972532441361"
target="_blank"
rel="noopener noreferrer"
className="social-icon whatsapp"
title="Contact us on WhatsApp"
>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
</svg>
<span>WhatsApp</span>
</a>
</div>
</div>
</div>
@ -138,6 +156,14 @@ export default function Contact() {
</button>
</form>
</div>
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/>
)}
</div>
)
}

View File

@ -43,7 +43,7 @@ export default function Home() {
{/* Hero Section */}
<section className="hero">
<div className="hero-content">
<h1>Welcome to StyleHub</h1>
<h1>Welcome to Brand Master</h1>
<p>Discover the latest in fashion and footwear</p>
<Link to="/products" className="btn btn-large">
Shop Now

View File

@ -1,7 +1,9 @@
import React, { useState, useContext } from 'react'
import { useNavigate } from 'react-router-dom'
import { useNavigate, Link } from 'react-router-dom'
import api from '../api'
import { AuthContext } from '../context/AuthContext'
import Toast from '../components/Toast'
import Modal from '../components/Modal'
import '../styles/global.css'
export default function Login() {
@ -12,7 +14,10 @@ export default function Login() {
password: '',
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [toast, setToast] = useState(null)
const [showForgotPassword, setShowForgotPassword] = useState(false)
const [resetEmail, setResetEmail] = useState('')
const [resetLoading, setResetLoading] = useState(false)
const handleChange = (e) => {
setFormData({
@ -23,7 +28,6 @@ export default function Login() {
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
setLoading(true)
try {
@ -36,22 +40,38 @@ export default function Login() {
setToken(response.data.access_token)
setUser(response.data.user)
navigate('/')
setToast({ type: 'success', message: 'Login successful!' })
setTimeout(() => navigate('/'), 1000)
} catch (error) {
setError('Invalid email or password')
setToast({ type: 'error', message: 'Invalid email or password' })
console.error('Login error:', error)
} finally {
setLoading(false)
}
}
const handleForgotPassword = async (e) => {
e.preventDefault()
setResetLoading(true)
try {
await api.post('/auth/forgot-password', { email: resetEmail })
setToast({ type: 'success', message: 'Password reset link sent to your email!' })
setShowForgotPassword(false)
setResetEmail('')
} catch (error) {
setToast({ type: 'error', message: 'Error sending reset link. Please try again.' })
console.error('Forgot password error:', error)
} finally {
setResetLoading(false)
}
}
return (
<div className="auth-page">
<div className="auth-container">
<h1>Login</h1>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Email</label>
@ -75,23 +95,66 @@ export default function Login() {
/>
</div>
<div className="forgot-password-link">
<button
type="button"
className="link-button"
onClick={() => setShowForgotPassword(true)}
>
Forgot Password?
</button>
</div>
<button type="submit" className="btn btn-full" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
<p>
Don't have an account? <a href="/register">Sign up here</a>
<p className="auth-switch">
Don't have an account? <Link to="/register">Sign up here</Link>
</p>
<div className="demo-account">
<p><strong>Demo Accounts:</strong></p>
<p style={{ color: '#ff6b6b', fontWeight: 'bold' }}>
<p className="admin-demo">
Admin: admin@example.com / password123
</p>
<p>User: user@example.com / password123</p>
</div>
</div>
<Modal
isOpen={showForgotPassword}
onClose={() => setShowForgotPassword(false)}
title="Reset Password"
>
<form onSubmit={handleForgotPassword}>
<p className="modal-description">
Enter your email address and we'll send you a link to reset your password.
</p>
<div className="form-group">
<label>Email</label>
<input
type="email"
value={resetEmail}
onChange={(e) => setResetEmail(e.target.value)}
placeholder="your@email.com"
required
/>
</div>
<button type="submit" className="btn btn-full" disabled={resetLoading}>
{resetLoading ? 'Sending...' : 'Send Reset Link'}
</button>
</form>
</Modal>
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/>
)}
</div>
)
}

View File

@ -2,11 +2,13 @@ import React, { useState, useEffect, useContext } from 'react'
import { useNavigate } from 'react-router-dom'
import api from '../api'
import { AuthContext } from '../context/AuthContext'
import Toast from '../components/Toast'
import '../styles/global.css'
export default function Models() {
const navigate = useNavigate()
const { user } = useContext(AuthContext)
const [toast, setToast] = useState(null)
const [models, setModels] = useState([])
const [categories, setCategories] = useState([])
const [brands, setBrands] = useState([])
@ -82,10 +84,10 @@ export default function Models() {
try {
if (editingModel) {
await api.put(`/models/${editingModel.id}`, modelData)
alert('Model updated successfully!')
setToast({ type: 'success', message: 'Model updated successfully!' })
} else {
await api.post('/models', modelData)
alert('Model created successfully!')
setToast({ type: 'success', message: 'Model created successfully!' })
}
setShowForm(false)
setEditingModel(null)
@ -93,7 +95,7 @@ export default function Models() {
fetchModels()
} catch (error) {
console.error('Error saving model:', error)
alert('Error saving model: ' + (error.response?.data?.detail || 'Unknown error'))
setToast({ type: 'error', message: 'Error saving model: ' + (error.response?.data?.detail || 'Unknown error') })
}
}
@ -118,11 +120,11 @@ export default function Models() {
try {
await api.delete(`/models/${id}`)
alert('Model deleted successfully!')
setToast({ type: 'success', message: 'Model deleted successfully!' })
fetchModels()
} catch (error) {
console.error('Error deleting model:', error)
alert('Error deleting model: ' + (error.response?.data?.detail || 'Unknown error'))
setToast({ type: 'error', message: 'Error deleting model: ' + (error.response?.data?.detail || 'Unknown error') })
}
}
@ -326,6 +328,14 @@ export default function Models() {
</div>
)}
</div>
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/>
)}
</div>
)
}

View File

@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
import api from '../api'
import { CartContext } from '../context/CartContext'
import { AuthContext } from '../context/AuthContext'
import AddToCartModal from '../components/AddToCartModal'
import '../styles/global.css'
export default function ProductDetail() {
@ -14,6 +15,7 @@ export default function ProductDetail() {
const [quantity, setQuantity] = useState(1)
const [inWishlist, setInWishlist] = useState(false)
const [selectedImageIndex, setSelectedImageIndex] = useState(0)
const [showCartModal, setShowCartModal] = useState(false)
const { addToCart } = useContext(CartContext)
const { token } = useContext(AuthContext)
@ -48,7 +50,7 @@ export default function ProductDetail() {
})
addToCart(product, quantity, selectedSize)
alert('Product added to cart!')
setShowCartModal(true)
} catch (error) {
console.error('Error adding to cart:', error)
}
@ -214,6 +216,12 @@ export default function ProductDetail() {
</div>
</div>
</div>
<AddToCartModal
isOpen={showCartModal}
onClose={() => setShowCartModal(false)}
productName={product.name}
/>
</div>
)
}

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect, useContext } from 'react'
import { useNavigate } from 'react-router-dom'
import api from '../api'
import { AuthContext } from '../context/AuthContext'
import Toast from '../components/Toast'
import '../styles/global.css'
export default function Profile() {
@ -17,6 +18,7 @@ export default function Profile() {
})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [toast, setToast] = useState(null)
useEffect(() => {
if (!token) {
@ -54,10 +56,10 @@ export default function Profile() {
params: { token },
})
setUser(response.data)
alert('Profile updated successfully!')
setToast({ type: 'success', message: 'Profile updated successfully!' })
} catch (error) {
console.error('Error updating profile:', error)
alert('Error updating profile')
setToast({ type: 'error', message: 'Error updating profile. Please try again.' })
} finally {
setSaving(false)
}
@ -161,6 +163,14 @@ export default function Profile() {
</button>
</form>
</div>
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/>
)}
</div>
)
}

View File

@ -1,7 +1,8 @@
import React, { useState, useContext } from 'react'
import { useNavigate } from 'react-router-dom'
import { useNavigate, Link } from 'react-router-dom'
import api from '../api'
import { AuthContext } from '../context/AuthContext'
import Toast from '../components/Toast'
import '../styles/global.css'
export default function Register() {
@ -13,7 +14,7 @@ export default function Register() {
full_name: '',
})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [toast, setToast] = useState(null)
const handleChange = (e) => {
setFormData({
@ -24,7 +25,6 @@ export default function Register() {
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
setLoading(true)
try {
@ -34,7 +34,7 @@ export default function Register() {
full_name: formData.full_name,
})
alert('Account created successfully! Logging you in...')
setToast({ type: 'success', message: 'Account created successfully! Logging you in...' })
const loginResponse = await api.post('/auth/login', null, {
params: {
@ -45,9 +45,10 @@ export default function Register() {
setToken(loginResponse.data.access_token)
setUser(loginResponse.data.user)
navigate('/')
setTimeout(() => navigate('/'), 1500)
} catch (error) {
setError(error.response?.data?.detail || 'Registration failed')
setToast({ type: 'error', message: error.response?.data?.detail || 'Registration failed' })
console.error('Registration error:', error)
} finally {
setLoading(false)
@ -59,8 +60,6 @@ export default function Register() {
<div className="auth-container">
<h1>Create Account</h1>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Full Name</label>
@ -101,10 +100,18 @@ export default function Register() {
</button>
</form>
<p>
Already have an account? <a href="/login">Login here</a>
<p className="auth-switch">
Already have an account? <Link to="/login">Login here</Link>
</p>
</div>
{toast && (
<Toast
message={toast.message}
type={toast.type}
onClose={() => setToast(null)}
/>
)}
</div>
)
}

View File

@ -99,12 +99,24 @@ button {
color: var(--secondary);
font-weight: 500;
transition: color 0.3s;
padding-bottom: 0.25rem;
border-bottom: 2px solid transparent;
}
.navbar-menu a:hover {
color: var(--primary);
}
.navbar-menu a.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.navbar-menu a.admin-link {
color: var(--primary);
font-weight: bold;
}
.navbar-icons {
display: flex;
align-items: center;
@ -907,6 +919,43 @@ button {
border-radius: var(--border-radius);
font-size: 0.95rem;
font-family: inherit;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
}
.form-group input.error,
.form-group select.error,
.form-group textarea.error {
border-color: #f44336;
}
.required {
color: #f44336;
margin-left: 2px;
}
.error-message {
display: block;
color: #f44336;
font-size: 0.85rem;
margin-top: 0.25rem;
}
.payment-info {
font-size: 1rem;
margin-bottom: 0.5rem;
color: var(--secondary);
}
.text-muted {
color: var(--gray);
font-size: 0.9rem;
}
.form-row {
@ -977,6 +1026,42 @@ button {
font-weight: 600;
}
.forgot-password-link {
text-align: right;
margin-bottom: 1rem;
margin-top: -0.5rem;
}
.link-button {
background: none;
border: none;
color: var(--primary);
font-size: 0.9rem;
cursor: pointer;
padding: 0;
text-decoration: underline;
transition: color 0.3s;
}
.link-button:hover {
color: var(--primary-dark);
}
.auth-switch {
margin-top: 1.5rem !important;
}
.modal-description {
color: var(--gray);
margin-bottom: 1.5rem;
text-align: left !important;
}
.admin-demo {
color: #ff6b6b !important;
font-weight: bold !important;
}
.error-message {
background: #f8d7da;
color: #721c24;
@ -1266,6 +1351,49 @@ button {
background: var(--primary-dark);
}
.social-icons {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.social-icon {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border-radius: var(--border-radius);
background: var(--light);
color: var(--secondary);
transition: all 0.3s;
text-decoration: none;
}
.social-icon:hover {
transform: translateX(8px);
box-shadow: var(--shadow);
}
.social-icon svg {
flex-shrink: 0;
}
.social-icon.instagram:hover {
background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%);
color: white;
}
.social-icon.whatsapp:hover {
background: #25D366;
color: white;
}
.social-icon span {
font-weight: 500;
font-size: 1rem;
}
.contact-form {
background: var(--light);
padding: 2rem;
@ -1359,6 +1487,216 @@ button {
max-width: 400px;
}
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal-content {
background: white;
border-radius: var(--border-radius);
padding: 2rem;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-lg);
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--gray-light);
}
.modal-header h2 {
margin: 0;
font-size: 1.5rem;
}
.modal-close {
background: none;
border: none;
font-size: 2rem;
color: var(--gray);
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.3s;
}
.modal-close:hover {
color: var(--secondary);
}
.modal-body {
margin-bottom: 0;
}
/* Cart Modal Specific */
.cart-modal {
text-align: center;
max-width: 400px;
}
.cart-modal-icon {
width: 60px;
height: 60px;
background: #4CAF50;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
margin: 0 auto 1rem;
}
.cart-modal h2 {
margin-bottom: 0.5rem;
color: var(--secondary);
}
.cart-modal-message {
color: var(--gray);
margin-bottom: 1.5rem;
}
.cart-modal-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.cart-modal-actions .btn {
flex: 1;
max-width: 200px;
}
/* Toast Notifications */
.toast {
position: fixed;
top: 20px;
right: 20px;
background: white;
padding: 1rem 1.5rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
gap: 1rem;
z-index: 1001;
min-width: 300px;
animation: slideInRight 0.3s ease-out;
}
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.toast-success {
border-left: 4px solid #4CAF50;
}
.toast-error {
border-left: 4px solid #f44336;
}
.toast-info {
border-left: 4px solid #2196F3;
}
.toast-icon {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
flex-shrink: 0;
}
.toast-success .toast-icon {
background: #4CAF50;
color: white;
}
.toast-error .toast-icon {
background: #f44336;
color: white;
}
.toast-info .toast-icon {
background: #2196F3;
color: white;
}
.toast-message {
flex: 1;
color: var(--secondary);
}
.toast-close {
background: none;
border: none;
font-size: 1.5rem;
color: var(--gray);
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.toast-close:hover {
color: var(--secondary);
}
/* Footer */
.footer {
background: var(--secondary);
@ -1383,6 +1721,22 @@ button {
font-size: 1rem;
}
.footer-brand {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.footer-logo {
font-size: 1.5rem;
}
.footer-brand h3 {
margin: 0;
font-size: 1.2rem;
}
.footer-section p {
color: rgba(255, 255, 255, 0.7);
font-size: 0.9rem;