Added Repository
This commit is contained in:
19
.editorconfig
Normal file
19
.editorconfig
Normal file
@@ -0,0 +1,19 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 120
|
||||
|
||||
[*.py]
|
||||
indent_size = 2
|
||||
|
||||
[*.{html,css,js}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
13
.env.example
Normal file
13
.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# Flask
|
||||
FLASK_ENV=development
|
||||
FLASK_DEBUG=1
|
||||
SECRET_KEY=this-should-be-long-and-random
|
||||
|
||||
# MariaDB database
|
||||
DB_USER=wearwell
|
||||
DB_PASSWORD=password
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_NAME=wearwell
|
||||
|
||||
DATABASE_URL=mysql+pymysql://wearwell:password@127.0.0.1:3306/wearwell
|
||||
129
.gitignore
vendored
Normal file
129
.gitignore
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
static/uploads/products/
|
||||
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
.scrapy
|
||||
|
||||
docs/_build/
|
||||
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
.ipynb_checkpoints
|
||||
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
.python-version
|
||||
|
||||
Pipfile.lock
|
||||
|
||||
poetry.lock
|
||||
|
||||
.pdm.toml
|
||||
|
||||
__pypackages__/
|
||||
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
*.sage.py
|
||||
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
.ropeproject
|
||||
|
||||
/site
|
||||
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
.pyre/
|
||||
|
||||
.pytype/
|
||||
|
||||
cython_debug/
|
||||
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
migrations/
|
||||
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
*.log
|
||||
|
||||
venv*/
|
||||
env*/
|
||||
229
README.md
229
README.md
@@ -1,3 +1,228 @@
|
||||
# wearwell
|
||||
# 🛍️ Wearwell - Modern E-commerce Clothing Store
|
||||
|
||||
WearWell Clothing Website
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
**WearWell - a simple flask shopping web app for clothes**
|
||||
|
||||
<img src="http://g.broombox.org/amanfromspace/wearwell/raw/branch/main/homepage.png" width="100%" height="500px">
|
||||
|
||||
</div>
|
||||
|
||||
## 📋 Table of Contents
|
||||
- [✨ Features](#-features)
|
||||
- [📁 Project Structure](#-project-structure)
|
||||
- [🌐 Application Routes](#-application-routes)
|
||||
- [🚀 Setup](#-setup)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### 🛒 Core E-commerce Features
|
||||
- **User Management**: Registration, authentication, profiles, order history
|
||||
- **Product Catalog**: Categories, filters, search, product variations
|
||||
- **Shopping Cart**: Session-based cart, cart persistence, quantity management
|
||||
- **Checkout Process**: Multi-step checkout, shipping options, payment integration
|
||||
- **Order Management**: Order tracking, status updates, cancellation/returns
|
||||
- **Inventory Management**: Stock tracking, low stock alerts, backorders
|
||||
- **Reviews & Ratings**: Product reviews, star ratings, photo reviews
|
||||
|
||||
### 🎨 User Experience
|
||||
- **Responsive Design**: Mobile-first, desktop-optimized interface
|
||||
- **Image Gallery**: High-quality product images
|
||||
- **Size Guides**: Interactive sizing charts, fit recommendations
|
||||
|
||||
### 🔧 Admin Features
|
||||
- **Dashboard**: inventory overview
|
||||
|
||||
### 🛠️ Technical Features
|
||||
- **Search Engine**: fast product search
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
### Tree Structure of WearWell Project
|
||||
|
||||
```
|
||||
wearwell/
|
||||
├── .env.example # Example environment of project (Database configures, etc)
|
||||
├── .editorconfig # Configurations of editor for formatting codes
|
||||
├── app.py # Main Flask application entry point - initializes app, configures extensions, registers blueprints
|
||||
├── extensions.py # Centralized Flask extensions initialization (SQLAlchemy, Migrate, LoginManager, etc)
|
||||
├── LICENSE # Project license file (GPL)
|
||||
├── models.py # Database models definition (SQLAlchemy ORM - User, Product, Order, Cart, etc)
|
||||
├── README.md # Project documentation, setup instructions, features overview
|
||||
├── requirements.txt # Python dependencies list for pip installation
|
||||
├── seed_db.py # Database seeding script - populates database with sample/test data
|
||||
├── routes/ # Flask route blueprints - modular URL routing by feature
|
||||
│ ├── admin.py # Admin panel routes - product management, user management, analytics
|
||||
│ ├── auth.py # Authentication routes - login, logout, registration, password reset
|
||||
│ ├── cart.py # Shopping cart operations - add/remove items, update quantities, view cart
|
||||
│ ├── __init__.py # Routes package initialization - exports blueprints
|
||||
│ ├── main.py # Public/main routes - homepage, product listings, search functionality
|
||||
│ └── user.py # User profile routes - profile management, order history, settings
|
||||
├── static/ # Static assets served directly by web server
|
||||
│ ├── css/ # Stylesheets for each page/component
|
||||
│ │ ├── add_product.css # Admin product addition form styling
|
||||
│ │ ├── auth.css # Login/registration form styling
|
||||
│ │ ├── cart.css # Shopping cart page styling
|
||||
│ │ ├── categories.css # Category listing/browsing styling
|
||||
│ │ ├── checkout.css # Checkout process styling
|
||||
│ │ ├── dashboard.css # Admin dashboard styling
|
||||
│ │ ├── edit_product.css # Admin product editing form styling
|
||||
│ │ ├── home.css # Homepage/landing page styling
|
||||
│ │ ├── order_details.css # Order summary and details page styling
|
||||
│ │ ├── product_detail.css # Individual product page styling
|
||||
│ │ ├── products.css # Product grid/listings styling
|
||||
│ │ ├── profile.css # User profile page styling
|
||||
│ │ └── styles.css # Global/base styles
|
||||
│ ├── favicon.ico # Browser tab/address bar icon
|
||||
│ ├── images/ # Image assets (logos, backgrounds, etc)
|
||||
│ │ └── default-product.jpg # Fallback product image when none uploaded
|
||||
│ └── js/ # Client-side JavaScript files
|
||||
│ ├── add_product.js # Product form validation, image upload preview
|
||||
│ ├── edit_product.js # Product editing form interactions
|
||||
│ ├── home.js # Homepage animations, featured product carousel
|
||||
│ ├── login.js # Login form validation, password visibility toggle
|
||||
│ ├── product_detail.js # Product image gallery, quantity selector, add to cart
|
||||
│ ├── products.js # Product filtering, sorting, pagination
|
||||
│ └── register.js # Registration form validation, password strength
|
||||
└── templates/ # Jinja2 HTML templates
|
||||
├── admin/ # Admin panel templates
|
||||
│ ├── add_product.html # Form for adding new products
|
||||
│ ├── categories.html # Category management interface
|
||||
│ ├── dashboard.html # Admin overview with statistics like CRUD table
|
||||
│ └── edit_product.html # Form for editing existing products
|
||||
├── base.html # Base template with common layout (header, footer, nav)
|
||||
├── cart.html # Shopping cart display and management
|
||||
├── checkout.html # Checkout process steps and payment form
|
||||
├── home.html # Homepage/landing page
|
||||
├── login.html # User login form
|
||||
├── order_details.html # Detailed order confirmation and summary
|
||||
├── product_detail.html # Individual product display page
|
||||
├── products.html # Product grid/listings with filters
|
||||
├── register.html # User registration form
|
||||
└── user_profile.html # User profile and order history
|
||||
```
|
||||
|
||||
### Database Structure
|
||||
|
||||
<img src="http://g.broombox.org/amanfromspace/wearwell/raw/branch/main/diagram_wearwell_db.png" width="100%" height="768px">
|
||||
|
||||
## 🌐 Application Routes
|
||||
|
||||
### Main Routes
|
||||
|
||||
| Route | Template | Description |
|
||||
|-------|----------|--------------|
|
||||
| `/` | `home.html` | Homepage with featured products and promotions |
|
||||
| `/products` | `products.html` | Product catalog with filtering and sorting |
|
||||
| `/login` | `login.html` | User authentication form |
|
||||
| `/register` | `register.html` | New user registration form |
|
||||
| `/cart` | `cart.html` | Shopping cart with items and quantities |
|
||||
| `/profile` | `user_profile.html` | User profile management and personal information |
|
||||
| `/checkout` | `checkout.html` | Order checkout process with payment and shipping |
|
||||
| `/admin` | `admin/dashboard.html` | Admin dashboard with statistics and overview |
|
||||
|
||||
## 🚀 Setup
|
||||
|
||||
### Prerequisites
|
||||
- **Python 3.12+** (also pip for installing requirements)
|
||||
- **MariaDB 10+ (MySQL)** (Linux)
|
||||
- **XAMPP 3.3.0+ (MySQL)** (Windows)
|
||||
|
||||
**Linux debian-based distros** (this apt command is only for practice it may be different)
|
||||
```
|
||||
sudo apt install python3.12 python3.12-venv mariadb-server mariadb-client
|
||||
```
|
||||
|
||||
After installations of prerequisites depending on your OS follow the instructions below to run the web app
|
||||
|
||||
### Installation
|
||||
First clone the project
|
||||
```
|
||||
git clone http://g.broombox.org/amanfromspace/wearwell.git
|
||||
```
|
||||
then go to the folder by using cd command
|
||||
```
|
||||
cd ./wearwell
|
||||
```
|
||||
now make virtual environment with python in your project (development mode not for production)
|
||||
```
|
||||
python3 -m venv venv
|
||||
```
|
||||
activate venv (linux)
|
||||
```
|
||||
source ./venv/bin/activate
|
||||
```
|
||||
activate venv (windows)
|
||||
```
|
||||
source venv/scripts/activate
|
||||
```
|
||||
you are ready to install requirements
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
it is time for your database to be ready - enter to mariadb and do the instructions
|
||||
```
|
||||
CREATE DATABASE wearwell;
|
||||
```
|
||||
better to use this specially with **utf8mb4_unicode_ci**
|
||||
```
|
||||
CREATE DATABASE IF NOT EXISTS wearwell
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
create a user with password
|
||||
```
|
||||
CREATE USER 'wearwell'@'localhost'
|
||||
IDENTIFIED BY 'password';
|
||||
```
|
||||
grant all privileges
|
||||
```
|
||||
GRANT ALL PRIVILEGES ON wearwell.*
|
||||
TO 'wearwell'@'localhost';
|
||||
```
|
||||
apply it
|
||||
```
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
now you can exit from mariadb and edit your .env file (use .env.example template of project environment)
|
||||
```
|
||||
DB_USER=wearwell
|
||||
DB_PASSWORD=password
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_NAME=wearwell
|
||||
```
|
||||
here is the structure of databse url in .env
|
||||
```
|
||||
mysql+pymysql://wearwell:password@127.0.0.1:3306/wearwell
|
||||
├─────────────┘ ├──────┘ ├──────┘ ├───────┘ ├──┘ ├──────┘
|
||||
│ │ │ │ │ └─── Database name
|
||||
│ │ │ │ └─── Port
|
||||
│ │ │ └─── Host/IP
|
||||
│ │ └─── Password
|
||||
│ └─── Username
|
||||
└─── Database driver (mysql with pymysql connector)
|
||||
```
|
||||
> **⚠️ Warning:** Generate a random key as secret for your .env in production mode
|
||||
it is time to initialize the models from models.py to migrate tables, ... into database
|
||||
```
|
||||
flask db init
|
||||
flask db migrate -m "Init"
|
||||
flask db upgrade
|
||||
```
|
||||
> **📝 Important:** You can use seed_db.py for fake data ( users, products, ... )
|
||||
```
|
||||
python3 ./seed_db.py
|
||||
```
|
||||
resets data then fills data again
|
||||
```
|
||||
python ./seed_db.py --fresh
|
||||
```
|
||||
run the app
|
||||
```
|
||||
python3 ./app.py
|
||||
```
|
||||
|
||||
48
app.py
Normal file
48
app.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from flask import Flask
|
||||
from extensions import db, migrate, login_manager
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = (
|
||||
f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}"
|
||||
f"@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
|
||||
)
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'devsecret')
|
||||
app.config['UPLOAD_FOLDER'] = 'static/uploads'
|
||||
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
|
||||
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'}
|
||||
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = 'auth.login'
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
from models import User
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
from models import User, Product, Category, Cart, CartItem, Order, OrderItem
|
||||
from routes.main import main_bp
|
||||
from routes.cart import cart_bp
|
||||
from routes.auth import auth_bp
|
||||
from routes.admin import admin_bp
|
||||
from routes.user import user_bp
|
||||
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(cart_bp)
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(admin_bp)
|
||||
app.register_blueprint(user_bp)
|
||||
|
||||
return app
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
BIN
diagram_wearwell_db.png
Normal file
BIN
diagram_wearwell_db.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
8
extensions.py
Normal file
8
extensions.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_login import LoginManager
|
||||
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
login_manager = LoginManager()
|
||||
login_manager.login_view = 'auth.login'
|
||||
BIN
homepage.png
Normal file
BIN
homepage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 337 KiB |
196
models.py
Normal file
196
models.py
Normal file
@@ -0,0 +1,196 @@
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from flask_login import UserMixin
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
from extensions import db
|
||||
|
||||
class SizeEnum:
|
||||
XS = 'XS'
|
||||
S = 'S'
|
||||
M = 'M'
|
||||
L = 'L'
|
||||
XL = 'XL'
|
||||
XXL = 'XXL'
|
||||
XXXL = 'XXXL'
|
||||
ALL = [XS, S, M, L, XL, XXL, XXXL]
|
||||
|
||||
class CategoryEnum:
|
||||
T_SHIRTS = 'T-Shirts'
|
||||
SHIRTS = 'Shirts'
|
||||
JEANS = 'Jeans'
|
||||
PANTS = 'Pants'
|
||||
JACKETS = 'Jackets'
|
||||
HOODIES = 'Hoodies'
|
||||
SWEATERS = 'Sweaters'
|
||||
SHORTS = 'Shorts'
|
||||
DRESSES = 'Dresses'
|
||||
SKIRTS = 'Skirts'
|
||||
ACTIVEWEAR = 'Activewear'
|
||||
SHOES = 'Shoes'
|
||||
ACCESSORIES = 'Accessories'
|
||||
ALL = [
|
||||
T_SHIRTS, SHIRTS, JEANS, PANTS, JACKETS, HOODIES,
|
||||
SWEATERS, SHORTS, DRESSES, SKIRTS, ACTIVEWEAR,
|
||||
SHOES, ACCESSORIES
|
||||
]
|
||||
|
||||
class User(db.Model, UserMixin):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(255))
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
cart = db.relationship('Cart', backref='user', uselist=False)
|
||||
orders = db.relationship('Order', backref='user', lazy=True)
|
||||
reviews = db.relationship('Review', backref='user', lazy=True)
|
||||
likes = db.relationship('ProductLike', backref='user', lazy=True)
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
class Category(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50), nullable=False)
|
||||
products = db.relationship('Product', backref='category', lazy=True)
|
||||
|
||||
class Product(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(120), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
price = db.Column(db.Float, nullable=False)
|
||||
stock = db.Column(db.Integer, default=0)
|
||||
color = db.Column(db.String(50))
|
||||
size = db.Column(db.String(10))
|
||||
material = db.Column(db.String(50))
|
||||
company = db.Column(db.String(100))
|
||||
sku = db.Column(db.String(50), unique=True)
|
||||
weight = db.Column(db.Float)
|
||||
dimensions = db.Column(db.String(50))
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
|
||||
images = db.relationship(
|
||||
'ProductImage', backref='product', lazy=True, cascade="all, delete-orphan")
|
||||
reviews = db.relationship('Review', backref='product',
|
||||
lazy=True, cascade="all, delete-orphan")
|
||||
likes = db.relationship('ProductLike', backref='product',
|
||||
lazy=True, cascade="all, delete-orphan")
|
||||
|
||||
@property
|
||||
def average_rating(self):
|
||||
if not self.reviews:
|
||||
return 0
|
||||
total = sum(review.rating for review in self.reviews)
|
||||
return round(total / len(self.reviews), 1)
|
||||
|
||||
@property
|
||||
def like_count(self):
|
||||
return len([like for like in self.likes if like.is_like])
|
||||
|
||||
@property
|
||||
def dislike_count(self):
|
||||
return len([like for like in self.likes if not like.is_like])
|
||||
|
||||
@property
|
||||
def review_count(self):
|
||||
return len(self.reviews)
|
||||
|
||||
@property
|
||||
def primary_image(self):
|
||||
if self.images:
|
||||
primary = next((img for img in self.images if img.is_primary), None)
|
||||
if primary:
|
||||
return primary
|
||||
return self.images[0]
|
||||
return None
|
||||
|
||||
def get_image_url(self):
|
||||
"""Alias for template compatibility"""
|
||||
return self.get_primary_image_url()
|
||||
|
||||
def in_stock(self):
|
||||
return self.stock > 0
|
||||
|
||||
def get_primary_image_url(self):
|
||||
primary = self.primary_image
|
||||
if primary:
|
||||
return f"/static/uploads/products/{primary.filename}"
|
||||
return "/static/images/default-product.jpg"
|
||||
|
||||
def get_all_image_urls(self):
|
||||
return [f"/static/uploads/products/{img.filename}" for img in self.images]
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Product {self.name}>'
|
||||
|
||||
class ProductImage(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
product_id = db.Column(
|
||||
db.Integer, db.ForeignKey('product.id'), nullable=False)
|
||||
filename = db.Column(db.String(200), nullable=False)
|
||||
is_primary = db.Column(db.Boolean, default=False)
|
||||
display_order = db.Column(db.Integer, default=0)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
class ProductLike(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
product_id = db.Column(
|
||||
db.Integer, db.ForeignKey('product.id'), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
is_like = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
__table_args__ = (db.UniqueConstraint(
|
||||
'product_id', 'user_id', name='unique_product_user_like'),)
|
||||
|
||||
class Review(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
product_id = db.Column(
|
||||
db.Integer, db.ForeignKey('product.id'), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
rating = db.Column(db.Integer, nullable=False)
|
||||
title = db.Column(db.String(200))
|
||||
comment = db.Column(db.Text)
|
||||
is_verified_purchase = db.Column(db.Boolean, default=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
|
||||
|
||||
class Cart(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
items = db.relationship('CartItem', backref='cart',
|
||||
cascade="all, delete-orphan", lazy=True)
|
||||
|
||||
class CartItem(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
cart_id = db.Column(db.Integer, db.ForeignKey('cart.id'))
|
||||
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
|
||||
quantity = db.Column(db.Integer, default=1)
|
||||
selected_size = db.Column(db.String(10))
|
||||
selected_color = db.Column(db.String(50))
|
||||
|
||||
product = db.relationship('Product')
|
||||
|
||||
class Order(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
status = db.Column(db.String(50), default='pending')
|
||||
|
||||
items = db.relationship('OrderItem', backref='order',
|
||||
cascade='all, delete-orphan', lazy=True)
|
||||
|
||||
class OrderItem(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
order_id = db.Column(db.Integer, db.ForeignKey('order.id'))
|
||||
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
|
||||
quantity = db.Column(db.Integer, default=1)
|
||||
price_at_purchase = db.Column(db.Float)
|
||||
selected_size = db.Column(db.String(10))
|
||||
selected_color = db.Column(db.String(50))
|
||||
|
||||
product = db.relationship('Product')
|
||||
|
||||
26
requirements.txt
Normal file
26
requirements.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
alembic==1.17.2
|
||||
autopep8==2.3.2
|
||||
blinker==1.9.0
|
||||
click==8.3.1
|
||||
Faker==40.1.0
|
||||
Flask==3.1.2
|
||||
Flask-Login==0.6.3
|
||||
Flask-Migrate==4.1.0
|
||||
Flask-SQLAlchemy==3.0.5
|
||||
greenlet==3.3.0
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
Mako==1.3.10
|
||||
MarkupSafe==3.0.3
|
||||
mypy_extensions==1.1.0
|
||||
packaging==25.0
|
||||
pathspec==0.12.1
|
||||
platformdirs==4.5.1
|
||||
pycodestyle==2.14.0
|
||||
PyMySQL==1.1.2
|
||||
python-dotenv==1.2.1
|
||||
pytokens==0.3.0
|
||||
SQLAlchemy==1.4.22
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2025.3
|
||||
Werkzeug==3.1.4
|
||||
7
routes/__init__.py
Normal file
7
routes/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .main import main_bp
|
||||
from .cart import cart_bp
|
||||
from .auth import auth_bp
|
||||
from .admin import admin_bp
|
||||
from .user import user_bp
|
||||
|
||||
__all__ = ['main_bp', 'cart_bp', 'auth_bp', 'admin_bp', 'user_bp']
|
||||
526
routes/admin.py
Normal file
526
routes/admin.py
Normal file
@@ -0,0 +1,526 @@
|
||||
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app
|
||||
from flask_login import login_required, current_user
|
||||
from models import db, Product, Category, ProductImage, Review, SizeEnum, User, Order, OrderItem
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
import json
|
||||
from functools import wraps
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func
|
||||
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
|
||||
|
||||
|
||||
def admin_required(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash("Please login to access admin area", "danger")
|
||||
return redirect(url_for('auth.login'))
|
||||
if not current_user.is_admin:
|
||||
flash("Admin access only!", "danger")
|
||||
return redirect(url_for('main.home'))
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||||
return '.' in filename and \
|
||||
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
def get_or_create_default_category():
|
||||
default = Category.query.filter_by(name='Uncategorized').first()
|
||||
if not default:
|
||||
default = Category(name='Uncategorized')
|
||||
db.session.add(default)
|
||||
db.session.commit()
|
||||
return default
|
||||
|
||||
|
||||
def get_categories():
|
||||
categories = Category.query.all()
|
||||
if not categories:
|
||||
default_cat = get_or_create_default_category()
|
||||
categories = [default_cat]
|
||||
return categories
|
||||
|
||||
|
||||
def get_admin_stats():
|
||||
try:
|
||||
total_revenue_result = db.session.query(
|
||||
func.sum(OrderItem.price_at_purchase * OrderItem.quantity)
|
||||
).join(Order).filter(Order.status.in_(['delivered', 'completed'])).scalar()
|
||||
|
||||
total_revenue = float(
|
||||
total_revenue_result) if total_revenue_result else 0.0
|
||||
|
||||
except:
|
||||
|
||||
total_revenue = 0.0
|
||||
|
||||
stats = {
|
||||
'total_products': Product.query.count(),
|
||||
'total_categories': Category.query.count(),
|
||||
'total_reviews': Review.query.count(),
|
||||
'total_orders': Order.query.count() if hasattr(Order, 'query') else 0,
|
||||
'total_users': User.query.count() if hasattr(User, 'query') else 1,
|
||||
'total_revenue': total_revenue,
|
||||
'low_stock': Product.query.filter(Product.stock > 0, Product.stock <= 5).count(),
|
||||
'out_of_stock': Product.query.filter(Product.stock == 0).count(),
|
||||
'total_value': db.session.query(func.sum(Product.price * Product.stock)).scalar() or 0,
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
@admin_bp.route('/')
|
||||
@login_required
|
||||
@admin_required
|
||||
def dashboard():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 20
|
||||
products = Product.query.order_by(
|
||||
Product.id.desc()).paginate(page=page, per_page=per_page)
|
||||
|
||||
stats = get_admin_stats()
|
||||
|
||||
return render_template('admin/dashboard.html',
|
||||
products=products,
|
||||
stats=stats,
|
||||
SizeEnum=SizeEnum)
|
||||
|
||||
@admin_bp.route('/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def add_product():
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
name = request.form['name']
|
||||
description = request.form.get('description', '')
|
||||
price = float(request.form['price'])
|
||||
stock = int(request.form.get('stock', 0))
|
||||
sku = request.form.get('sku', '')
|
||||
color = request.form.get('color', '')
|
||||
size = request.form.get('size', '')
|
||||
product_type = request.form.get('product_type', '')
|
||||
material = request.form.get('material', '')
|
||||
company = request.form.get('company', '')
|
||||
weight = request.form.get('weight')
|
||||
dimensions = request.form.get('dimensions', '')
|
||||
|
||||
if weight:
|
||||
try:
|
||||
weight = float(weight)
|
||||
except ValueError:
|
||||
weight = None
|
||||
else:
|
||||
weight = None
|
||||
|
||||
category_id = request.form.get('category_id')
|
||||
if not category_id:
|
||||
category = get_or_create_default_category()
|
||||
category_id = category.id
|
||||
|
||||
product = Product(
|
||||
name=name,
|
||||
description=description,
|
||||
price=price,
|
||||
stock=stock,
|
||||
sku=sku,
|
||||
color=color,
|
||||
size=size,
|
||||
material=material,
|
||||
company=company,
|
||||
weight=weight,
|
||||
dimensions=dimensions,
|
||||
category_id=category_id
|
||||
)
|
||||
|
||||
db.session.add(product)
|
||||
db.session.flush()
|
||||
|
||||
if 'images[]' in request.files:
|
||||
files = request.files.getlist('images[]')
|
||||
for i, file in enumerate(files):
|
||||
if file and file.filename and allowed_file(file.filename):
|
||||
upload_dir = 'static/uploads/products'
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
|
||||
filename = secure_filename(file.filename)
|
||||
name_part, ext = os.path.splitext(filename)
|
||||
unique_filename = f"{name_part}_{product.id}_{i}{ext}"
|
||||
file_path = os.path.join(upload_dir, unique_filename)
|
||||
file.save(file_path)
|
||||
|
||||
product_image = ProductImage(
|
||||
product_id=product.id,
|
||||
filename=unique_filename,
|
||||
is_primary=(i == 0),
|
||||
display_order=i
|
||||
)
|
||||
db.session.add(product_image)
|
||||
|
||||
db.session.commit()
|
||||
flash(f"Product '{name}' added successfully!", "success")
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f"Error adding product: {str(e)}", "danger")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
categories = get_categories()
|
||||
return render_template('admin/add_product.html',
|
||||
categories=categories,
|
||||
sizes=SizeEnum.ALL)
|
||||
|
||||
@admin_bp.route('/edit/<int:id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def edit_product(id):
|
||||
product = Product.query.get_or_404(id)
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
product.name = request.form['name']
|
||||
product.description = request.form.get('description', '')
|
||||
product.price = float(request.form['price'])
|
||||
product.stock = int(request.form.get('stock', 0))
|
||||
product.sku = request.form.get('sku', '')
|
||||
product.color = request.form.get('color', '')
|
||||
product.size = request.form.get('size', '')
|
||||
product.material = request.form.get('material', '')
|
||||
product.company = request.form.get('company', '')
|
||||
weight = request.form.get('weight')
|
||||
if weight:
|
||||
try:
|
||||
product.weight = float(weight)
|
||||
except ValueError:
|
||||
product.weight = None
|
||||
else:
|
||||
product.weight = None
|
||||
|
||||
product.dimensions = request.form.get('dimensions', '')
|
||||
|
||||
category_id = request.form.get('category_id')
|
||||
if category_id:
|
||||
product.category_id = category_id
|
||||
|
||||
if 'images[]' in request.files:
|
||||
files = request.files.getlist('images[]')
|
||||
for i, file in enumerate(files):
|
||||
if file and file.filename and allowed_file(file.filename):
|
||||
upload_dir = 'static/uploads/products'
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
filename = secure_filename(file.filename)
|
||||
name_part, ext = os.path.splitext(filename)
|
||||
unique_filename = f"{name_part}_{product.id}_{len(product.images) + i}{ext}"
|
||||
file_path = os.path.join(upload_dir, unique_filename)
|
||||
file.save(file_path)
|
||||
product_image = ProductImage(
|
||||
product_id=product.id,
|
||||
filename=unique_filename,
|
||||
display_order=len(product.images) + i
|
||||
)
|
||||
db.session.add(product_image)
|
||||
|
||||
if 'deleted_images' in request.form:
|
||||
deleted_images_str = request.form['deleted_images']
|
||||
if deleted_images_str:
|
||||
try:
|
||||
deleted_ids = json.loads(deleted_images_str)
|
||||
for img_id in deleted_ids:
|
||||
image = ProductImage.query.get(img_id)
|
||||
if image and image.product_id == product.id:
|
||||
|
||||
file_path = os.path.join(
|
||||
'static/uploads/products', image.filename)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
db.session.delete(image)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
|
||||
if 'primary_image' in request.form:
|
||||
primary_id = request.form['primary_image']
|
||||
if primary_id:
|
||||
try:
|
||||
primary_id = int(primary_id)
|
||||
for img in product.images:
|
||||
img.is_primary = (img.id == primary_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
db.session.commit()
|
||||
flash("Product updated successfully!", "success")
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f"Error updating product: {str(e)}", "danger")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
categories = get_categories()
|
||||
return render_template('admin/edit_product.html',
|
||||
product=product,
|
||||
categories=categories,
|
||||
sizes=SizeEnum.ALL)
|
||||
|
||||
@admin_bp.route('/delete/<int:id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_product(id):
|
||||
product = Product.query.get_or_404(id)
|
||||
try:
|
||||
for image in product.images:
|
||||
file_path = os.path.join('static/uploads/products', image.filename)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
db.session.delete(product)
|
||||
db.session.commit()
|
||||
flash("Product deleted successfully!", "info")
|
||||
except Exception as e:
|
||||
flash(f"Error deleting product: {str(e)}", "danger")
|
||||
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
@admin_bp.route('/image/delete/<int:id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_image(id):
|
||||
try:
|
||||
image = ProductImage.query.get_or_404(id)
|
||||
|
||||
file_path = os.path.join('static/uploads/products', image.filename)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
db.session.delete(image)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@admin_bp.route('/reviews')
|
||||
@login_required
|
||||
@admin_required
|
||||
def manage_reviews():
|
||||
reviews = Review.query.order_by(Review.created_at.desc()).all()
|
||||
return render_template('admin/reviews.html', reviews=reviews)
|
||||
|
||||
@admin_bp.route('/review/delete/<int:id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_review(id):
|
||||
review = Review.query.get_or_404(id)
|
||||
try:
|
||||
db.session.delete(review)
|
||||
db.session.commit()
|
||||
flash("Review deleted successfully!", "info")
|
||||
except Exception as e:
|
||||
flash(f"Error deleting review: {str(e)}", "danger")
|
||||
|
||||
return redirect(url_for('admin.manage_reviews'))
|
||||
|
||||
@admin_bp.route('/categories')
|
||||
@login_required
|
||||
@admin_required
|
||||
def manage_categories():
|
||||
categories = Category.query.order_by(Category.name.asc()).all()
|
||||
return render_template('admin/categories.html', categories=categories)
|
||||
|
||||
@admin_bp.route('/category/add', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def add_category():
|
||||
name = request.form.get('name', '').strip()
|
||||
if not name:
|
||||
flash("Category name cannot be empty", "danger")
|
||||
return redirect(url_for('admin.manage_categories'))
|
||||
|
||||
existing = Category.query.filter_by(name=name).first()
|
||||
if existing:
|
||||
flash(f"Category '{name}' already exists", "warning")
|
||||
return redirect(url_for('admin.manage_categories'))
|
||||
|
||||
try:
|
||||
category = Category(name=name)
|
||||
db.session.add(category)
|
||||
db.session.commit()
|
||||
flash(f"Category '{name}' added successfully!", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error adding category: {str(e)}", "danger")
|
||||
|
||||
return redirect(url_for('admin.manage_categories'))
|
||||
|
||||
@admin_bp.route('/category/delete/<int:id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_category(id):
|
||||
category = Category.query.get_or_404(id)
|
||||
if category.products:
|
||||
flash(
|
||||
f"Cannot delete category '{category.name}' because it has {len(category.products)} product(s)", "danger")
|
||||
return redirect(url_for('admin.manage_categories'))
|
||||
|
||||
try:
|
||||
db.session.delete(category)
|
||||
db.session.commit()
|
||||
flash(f"Category '{category.name}' deleted successfully!", "info")
|
||||
except Exception as e:
|
||||
flash(f"Error deleting category: {str(e)}", "danger")
|
||||
|
||||
return redirect(url_for('admin.manage_categories'))
|
||||
|
||||
@admin_bp.route('/stats')
|
||||
@login_required
|
||||
@admin_required
|
||||
def stats():
|
||||
stats_data = get_admin_stats()
|
||||
return render_template('admin/stats.html', stats=stats_data)
|
||||
|
||||
@admin_bp.route('/users')
|
||||
@login_required
|
||||
@admin_required
|
||||
def manage_users():
|
||||
users = User.query.order_by(User.id.desc()).all()
|
||||
return render_template('admin/users.html', users=users)
|
||||
|
||||
@admin_bp.route('/user/toggle_admin/<int:id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def toggle_user_admin(id):
|
||||
user = User.query.get_or_404(id)
|
||||
|
||||
if user.id == current_user.id:
|
||||
flash("You cannot remove admin privileges from yourself", "warning")
|
||||
return redirect(url_for('admin.manage_users'))
|
||||
|
||||
try:
|
||||
user.is_admin = not user.is_admin
|
||||
db.session.commit()
|
||||
status = "granted" if user.is_admin else "removed"
|
||||
flash(f"Admin privileges {status} for {user.email}", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error updating user: {str(e)}", "danger")
|
||||
|
||||
return redirect(url_for('admin.manage_users'))
|
||||
|
||||
@admin_bp.route('/user/delete/<int:id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_user(id):
|
||||
user = User.query.get_or_404(id)
|
||||
|
||||
if user.id == current_user.id:
|
||||
flash("You cannot delete your own account", "warning")
|
||||
return redirect(url_for('admin.manage_users'))
|
||||
|
||||
try:
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash(f"User {user.email} deleted successfully", "info")
|
||||
except Exception as e:
|
||||
flash(f"Error deleting user: {str(e)}", "danger")
|
||||
|
||||
return redirect(url_for('admin.manage_users'))
|
||||
|
||||
@admin_bp.route('/orders')
|
||||
@login_required
|
||||
@admin_required
|
||||
def manage_orders():
|
||||
orders = Order.query.order_by(Order.created_at.desc()).all()
|
||||
return render_template('admin/orders.html', orders=orders)
|
||||
|
||||
@admin_bp.route('/order/<int:id>')
|
||||
@login_required
|
||||
@admin_required
|
||||
def order_detail(id):
|
||||
order = Order.query.get_or_404(id)
|
||||
return render_template('admin/order_detail.html', order=order)
|
||||
|
||||
@admin_bp.route('/order/update_status/<int:id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def update_order_status(id):
|
||||
order = Order.query.get_or_404(id)
|
||||
new_status = request.form.get('status')
|
||||
|
||||
if new_status in ['pending', 'processing', 'shipped', 'delivered', 'cancelled']:
|
||||
try:
|
||||
order.status = new_status
|
||||
db.session.commit()
|
||||
flash(f"Order #{order.id} status updated to {new_status}", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error updating order: {str(e)}", "danger")
|
||||
else:
|
||||
flash("Invalid status", "danger")
|
||||
|
||||
return redirect(url_for('admin.order_detail', id=id))
|
||||
|
||||
@admin_bp.route('/quick/restock_low')
|
||||
@login_required
|
||||
@admin_required
|
||||
def quick_restock_low():
|
||||
"""Quick action to restock low inventory items"""
|
||||
try:
|
||||
low_stock_products = Product.query.filter(
|
||||
Product.stock > 0,
|
||||
Product.stock <= 5
|
||||
).all()
|
||||
|
||||
count = 0
|
||||
for product in low_stock_products:
|
||||
product.stock += 10
|
||||
count += 1
|
||||
|
||||
if count > 0:
|
||||
db.session.commit()
|
||||
flash(f"Restocked {count} low inventory products", "success")
|
||||
else:
|
||||
flash("No low inventory products found", "info")
|
||||
|
||||
except Exception as e:
|
||||
flash(f"Error restocking products: {str(e)}", "danger")
|
||||
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
@admin_bp.route('/quick/update_prices', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def quick_update_prices():
|
||||
"""Quick action to update prices by percentage"""
|
||||
try:
|
||||
percentage = float(request.form.get('percentage', 0))
|
||||
|
||||
if percentage != 0:
|
||||
|
||||
multiplier = 1 + (percentage / 100)
|
||||
|
||||
products = Product.query.all()
|
||||
for product in products:
|
||||
product.price = round(product.price * multiplier, 2)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
action = "increased" if percentage > 0 else "decreased"
|
||||
flash(
|
||||
f"Prices {action} by {abs(percentage)}% for all products", "success")
|
||||
else:
|
||||
flash("No price change specified", "warning")
|
||||
|
||||
except Exception as e:
|
||||
flash(f"Error updating prices: {str(e)}", "danger")
|
||||
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
43
routes/auth.py
Normal file
43
routes/auth.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from models import db, User
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if request.method == 'POST':
|
||||
email = request.form['email']
|
||||
password = request.form['password']
|
||||
if User.query.filter_by(email=email).first():
|
||||
flash("Email already registered", "danger")
|
||||
return redirect(url_for('auth.register'))
|
||||
user = User(email=email)
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash("Registered successfully!", "success")
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template('register.html')
|
||||
|
||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
email = request.form['email']
|
||||
password = request.form['password']
|
||||
user = User.query.filter_by(email=email).first()
|
||||
if user and user.check_password(password):
|
||||
login_user(user)
|
||||
flash("Logged in successfully!", "success")
|
||||
next_page = request.args.get('next')
|
||||
return redirect(next_page or url_for('main.home'))
|
||||
flash("Invalid credentials", "danger")
|
||||
return render_template('login.html')
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
flash("Logged out successfully!", "info")
|
||||
return redirect(url_for('main.home'))
|
||||
218
routes/cart.py
Normal file
218
routes/cart.py
Normal file
@@ -0,0 +1,218 @@
|
||||
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_required, current_user
|
||||
from models import db, Cart, CartItem, Product
|
||||
|
||||
cart_bp = Blueprint('cart', __name__)
|
||||
|
||||
@cart_bp.route('/cart')
|
||||
@login_required
|
||||
def view_cart():
|
||||
|
||||
cart = Cart.query.filter_by(user_id=current_user.id).first()
|
||||
if not cart:
|
||||
cart = Cart(user_id=current_user.id)
|
||||
db.session.add(cart)
|
||||
db.session.commit()
|
||||
|
||||
items = cart.items
|
||||
total = 0
|
||||
item_details = []
|
||||
|
||||
for item in items:
|
||||
product = Product.query.get(item.product_id)
|
||||
if product:
|
||||
subtotal = product.price * item.quantity
|
||||
total += subtotal
|
||||
item_details.append({
|
||||
'id': item.id,
|
||||
'product': product,
|
||||
'quantity': item.quantity,
|
||||
'subtotal': subtotal,
|
||||
'max_quantity': product.stock
|
||||
})
|
||||
|
||||
return render_template('cart.html', items=item_details, total=total)
|
||||
|
||||
@cart_bp.route('/add/<int:product_id>')
|
||||
@login_required
|
||||
def add_to_cart(product_id):
|
||||
|
||||
cart = Cart.query.filter_by(user_id=current_user.id).first()
|
||||
if not cart:
|
||||
cart = Cart(user_id=current_user.id)
|
||||
db.session.add(cart)
|
||||
db.session.commit()
|
||||
|
||||
product = Product.query.get(product_id)
|
||||
if not product:
|
||||
flash("Product not found", "danger")
|
||||
return redirect(url_for('main.products'))
|
||||
|
||||
if product.stock <= 0:
|
||||
flash(f"Sorry, {product.name} is out of stock", "danger")
|
||||
return redirect(url_for('main.products'))
|
||||
|
||||
cart_item = CartItem.query.filter_by(
|
||||
cart_id=cart.id, product_id=product_id).first()
|
||||
|
||||
if cart_item:
|
||||
|
||||
if cart_item.quantity + 1 > product.stock:
|
||||
flash(f"Only {product.stock} items available in stock", "warning")
|
||||
return redirect(url_for('main.products'))
|
||||
|
||||
cart_item.quantity += 1
|
||||
else:
|
||||
|
||||
cart_item = CartItem(cart_id=cart.id, product_id=product_id, quantity=1)
|
||||
db.session.add(cart_item)
|
||||
|
||||
db.session.commit()
|
||||
flash(f"Added {product.name} to cart!", "success")
|
||||
|
||||
referrer = request.referrer
|
||||
if referrer and ('/product/' in referrer or '/products' in referrer):
|
||||
return redirect(referrer)
|
||||
return redirect(url_for('main.products'))
|
||||
|
||||
@cart_bp.route('/remove/<int:item_id>', methods=['POST'])
|
||||
@login_required
|
||||
def remove_from_cart(item_id):
|
||||
cart_item = CartItem.query.get_or_404(item_id)
|
||||
|
||||
if cart_item.cart.user_id != current_user.id:
|
||||
flash("You don't have permission to remove this item", "danger")
|
||||
return redirect(url_for('cart.view_cart'))
|
||||
|
||||
product_name = cart_item.product.name
|
||||
db.session.delete(cart_item)
|
||||
db.session.commit()
|
||||
flash(f"{product_name} removed from cart", "info")
|
||||
return redirect(url_for('cart.view_cart'))
|
||||
|
||||
@cart_bp.route('/update/<int:item_id>', methods=['POST'])
|
||||
@login_required
|
||||
def update_cart(item_id):
|
||||
cart_item = CartItem.query.get_or_404(item_id)
|
||||
|
||||
|
||||
if cart_item.cart.user_id != current_user.id:
|
||||
flash("You don't have permission to update this item", "danger")
|
||||
return redirect(url_for('cart.view_cart'))
|
||||
|
||||
new_quantity = request.form.get('quantity')
|
||||
product = Product.query.get(cart_item.product_id)
|
||||
|
||||
try:
|
||||
new_quantity = int(new_quantity)
|
||||
if new_quantity > 0:
|
||||
if new_quantity > product.stock:
|
||||
flash(
|
||||
f"Only {product.stock} items available in stock. Quantity adjusted.", "warning")
|
||||
new_quantity = product.stock
|
||||
|
||||
cart_item.quantity = new_quantity
|
||||
db.session.commit()
|
||||
flash("Cart updated", "success")
|
||||
elif new_quantity == 0:
|
||||
db.session.delete(cart_item)
|
||||
db.session.commit()
|
||||
flash("Item removed from cart", "info")
|
||||
else:
|
||||
flash("Quantity must be at least 0", "danger")
|
||||
|
||||
except ValueError:
|
||||
flash("Invalid quantity", "danger")
|
||||
|
||||
return redirect(url_for('cart.view_cart'))
|
||||
|
||||
@cart_bp.route('/clear', methods=['POST'])
|
||||
@login_required
|
||||
def clear_cart():
|
||||
cart = Cart.query.filter_by(user_id=current_user.id).first()
|
||||
if cart:
|
||||
items = cart.items
|
||||
if items:
|
||||
item_count = len(items)
|
||||
|
||||
CartItem.query.filter_by(cart_id=cart.id).delete()
|
||||
db.session.commit()
|
||||
flash(f"Cart cleared ({item_count} items removed)", "info")
|
||||
else:
|
||||
flash("Your cart is already empty", "info")
|
||||
|
||||
return redirect(url_for('cart.view_cart'))
|
||||
|
||||
@cart_bp.route('/checkout', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def checkout():
|
||||
cart = Cart.query.filter_by(user_id=current_user.id).first()
|
||||
if not cart or not cart.items:
|
||||
flash("Your cart is empty", "warning")
|
||||
return redirect(url_for('cart.view_cart'))
|
||||
|
||||
for item in cart.items:
|
||||
product = Product.query.get(item.product_id)
|
||||
if product.stock < item.quantity:
|
||||
flash(
|
||||
f"Sorry, {product.name} only has {product.stock} items in stock (you have {item.quantity} in cart)", "danger")
|
||||
return redirect(url_for('cart.view_cart'))
|
||||
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
total = 0
|
||||
order_items = []
|
||||
|
||||
for item in cart.items:
|
||||
product = Product.query.get(item.product_id)
|
||||
|
||||
product.stock -= item.quantity
|
||||
|
||||
total += product.price * item.quantity
|
||||
|
||||
order_items.append({
|
||||
'product_id': product.id,
|
||||
'quantity': item.quantity,
|
||||
'price': product.price
|
||||
})
|
||||
|
||||
from models import Order, OrderItem
|
||||
from datetime import datetime
|
||||
|
||||
order = Order(user_id=current_user.id, created_at=datetime.utcnow())
|
||||
db.session.add(order)
|
||||
db.session.flush()
|
||||
|
||||
for item_data in order_items:
|
||||
order_item = OrderItem(
|
||||
order_id=order.id,
|
||||
product_id=item_data['product_id'],
|
||||
quantity=item_data['quantity']
|
||||
)
|
||||
db.session.add(order_item)
|
||||
|
||||
CartItem.query.filter_by(cart_id=cart.id).delete()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flash(f"Order placed successfully! Total: ${total:.2f}", "success")
|
||||
return redirect(url_for('user.profile'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f"Checkout failed: {str(e)}", "danger")
|
||||
return redirect(url_for('cart.view_cart'))
|
||||
|
||||
total = sum(item.product.price * item.quantity for item in cart.items)
|
||||
return render_template('checkout.html', cart=cart, total=total)
|
||||
|
||||
@cart_bp.route('/count')
|
||||
@login_required
|
||||
def cart_count():
|
||||
cart = Cart.query.filter_by(user_id=current_user.id).first()
|
||||
if cart:
|
||||
count = sum(item.quantity for item in cart.items)
|
||||
else:
|
||||
count = 0
|
||||
return str(count)
|
||||
149
routes/main.py
Normal file
149
routes/main.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from flask import Blueprint, render_template, request
|
||||
from flask_login import login_required, current_user
|
||||
from models import Product, Category, SizeEnum, db
|
||||
from sqlalchemy import or_, and_
|
||||
from extensions import db
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
|
||||
@main_bp.route('/')
|
||||
def home():
|
||||
featured_products = Product.query.order_by(Product.id.desc()).limit(8).all()
|
||||
|
||||
|
||||
best_sellers = Product.query.order_by(
|
||||
Product.stock.desc(),
|
||||
Product.price.desc()
|
||||
).limit(4).all()
|
||||
|
||||
categories = Category.query.all()
|
||||
|
||||
return render_template('home.html',
|
||||
featured_products=featured_products,
|
||||
best_sellers=best_sellers,
|
||||
categories=categories)
|
||||
|
||||
@main_bp.route('/products')
|
||||
def products():
|
||||
search_query = request.args.get('q', '').strip()
|
||||
category_id = request.args.get('category', '')
|
||||
size = request.args.get('size', '')
|
||||
color = request.args.get('color', '')
|
||||
material = request.args.get('material', '')
|
||||
company = request.args.get('company', '')
|
||||
min_price = request.args.get('min_price', '')
|
||||
max_price = request.args.get('max_price', '')
|
||||
in_stock_only = request.args.get('in_stock') == '1'
|
||||
sort_by = request.args.get('sort', 'newest')
|
||||
|
||||
query = Product.query
|
||||
|
||||
if search_query:
|
||||
search_term = f"%{search_query}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
Product.name.ilike(search_term),
|
||||
Product.description.ilike(search_term),
|
||||
Product.color.ilike(search_term),
|
||||
Product.material.ilike(search_term),
|
||||
Product.company.ilike(search_term),
|
||||
Product.category.has(Category.name.ilike(search_term))
|
||||
)
|
||||
)
|
||||
|
||||
if category_id:
|
||||
try:
|
||||
query = query.filter(Product.category_id == int(category_id))
|
||||
selected_category_name = Category.query.get(int(category_id)).name
|
||||
except (ValueError, TypeError):
|
||||
selected_category_name = None
|
||||
else:
|
||||
selected_category_name = None
|
||||
|
||||
if size:
|
||||
query = query.filter(Product.size == size)
|
||||
|
||||
if color:
|
||||
query = query.filter(Product.color.ilike(f"%{color}%"))
|
||||
|
||||
if material:
|
||||
query = query.filter(Product.material.ilike(f"%{material}%"))
|
||||
|
||||
if company:
|
||||
query = query.filter(Product.company.ilike(f"%{company}%"))
|
||||
|
||||
if min_price:
|
||||
try:
|
||||
query = query.filter(Product.price >= float(min_price))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if max_price:
|
||||
try:
|
||||
query = query.filter(Product.price <= float(max_price))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if in_stock_only:
|
||||
query = query.filter(Product.stock > 0)
|
||||
|
||||
if sort_by == 'price_low':
|
||||
query = query.order_by(Product.price.asc())
|
||||
elif sort_by == 'price_high':
|
||||
query = query.order_by(Product.price.desc())
|
||||
elif sort_by == 'name':
|
||||
query = query.order_by(Product.name.asc())
|
||||
elif sort_by == 'rating':
|
||||
|
||||
query = query.order_by(Product.id.desc())
|
||||
else:
|
||||
query = query.order_by(Product.id.desc())
|
||||
|
||||
colors = db.session.query(Product.color).distinct().filter(
|
||||
Product.color != None, Product.color != '').all()
|
||||
colors = [c[0] for c in colors if c[0]]
|
||||
|
||||
materials = db.session.query(Product.material).distinct().filter(
|
||||
Product.material != None, Product.material != '').all()
|
||||
materials = [m[0] for m in materials if m[0]]
|
||||
|
||||
companies = db.session.query(Product.company).distinct().filter(
|
||||
Product.company != None, Product.company != '').all()
|
||||
companies = [c[0] for c in companies if c[0]]
|
||||
|
||||
products = query.all()
|
||||
|
||||
categories = Category.query.order_by(Category.name.asc()).all()
|
||||
|
||||
return render_template('products.html',
|
||||
products=products,
|
||||
categories=categories,
|
||||
sizes=SizeEnum.ALL,
|
||||
colors=colors,
|
||||
materials=materials,
|
||||
companies=companies,
|
||||
search_query=search_query,
|
||||
selected_category=category_id,
|
||||
selected_category_name=selected_category_name,
|
||||
selected_size=size,
|
||||
selected_color=color,
|
||||
selected_material=material,
|
||||
selected_company=company,
|
||||
min_price=min_price,
|
||||
max_price=max_price,
|
||||
sort_by=sort_by,
|
||||
in_stock_only=in_stock_only)
|
||||
|
||||
@main_bp.route('/product/<int:id>')
|
||||
def product_detail(id):
|
||||
product = Product.query.get_or_404(id)
|
||||
|
||||
|
||||
related_products = Product.query.filter(
|
||||
Product.category_id == product.category_id,
|
||||
Product.id != product.id
|
||||
).limit(4).all()
|
||||
|
||||
return render_template('product_detail.html',
|
||||
product=product,
|
||||
related_products=related_products)
|
||||
22
routes/user.py
Normal file
22
routes/user.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash
|
||||
from flask_login import login_required, current_user
|
||||
from models import Order
|
||||
|
||||
user_bp = Blueprint('user', __name__)
|
||||
|
||||
@user_bp.route('/profile')
|
||||
@login_required
|
||||
def profile():
|
||||
|
||||
orders = Order.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('user_profile.html', orders=orders)
|
||||
|
||||
@user_bp.route('/orders/<int:order_id>')
|
||||
@login_required
|
||||
def order_details(order_id):
|
||||
order = Order.query.get_or_404(order_id)
|
||||
|
||||
if order.user_id != current_user.id:
|
||||
flash("Access denied", "danger")
|
||||
return redirect(url_for('user.profile'))
|
||||
return render_template('order_details.html', order=order)
|
||||
388
seed_db.py
Normal file
388
seed_db.py
Normal file
@@ -0,0 +1,388 @@
|
||||
import random
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from faker import Faker
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
from app import create_app, db
|
||||
from models import (
|
||||
Cart,
|
||||
CartItem,
|
||||
Category,
|
||||
CategoryEnum,
|
||||
Order,
|
||||
OrderItem,
|
||||
Product,
|
||||
ProductImage,
|
||||
ProductLike,
|
||||
Review,
|
||||
User,
|
||||
)
|
||||
|
||||
def create_seed_data():
|
||||
"""Create dummy data for testing"""
|
||||
fake = Faker()
|
||||
|
||||
print("🌱 Starting database seeding...")
|
||||
|
||||
|
||||
print("👤 Checking/Creating admin user...")
|
||||
admin = User.query.filter_by(email="admin@wearwell.com").first()
|
||||
if not admin:
|
||||
admin = User(
|
||||
email="admin@wearwell.com",
|
||||
password_hash=generate_password_hash("admin123"),
|
||||
is_admin=True,
|
||||
)
|
||||
db.session.add(admin)
|
||||
print("✅ Created admin user")
|
||||
else:
|
||||
print("⚠️ Admin user already exists, skipping...")
|
||||
|
||||
print("👥 Creating regular users...")
|
||||
users = [admin] if admin else []
|
||||
|
||||
existing_users = User.query.count()
|
||||
users_to_create = max(
|
||||
0, 11 - existing_users
|
||||
)
|
||||
|
||||
for i in range(users_to_create):
|
||||
email = f"user{i + 1}@example.com"
|
||||
|
||||
|
||||
if not User.query.filter_by(email=email).first():
|
||||
user = User(
|
||||
email=email,
|
||||
password_hash=generate_password_hash(f"password{i + 1}"),
|
||||
is_admin=False,
|
||||
)
|
||||
db.session.add(user)
|
||||
users.append(user)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
print("🏷️ Creating categories...")
|
||||
categories = []
|
||||
|
||||
for cat_name in CategoryEnum.ALL:
|
||||
category = Category.query.filter_by(name=cat_name).first()
|
||||
if not category:
|
||||
category = Category(name=cat_name)
|
||||
db.session.add(category)
|
||||
categories.append(category)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
print("👕 Creating products...")
|
||||
|
||||
existing_products_count = Product.query.count()
|
||||
products_to_create = max(0, 50 - existing_products_count)
|
||||
|
||||
if products_to_create > 0:
|
||||
clothing_brands = [
|
||||
"Nike",
|
||||
"Adidas",
|
||||
"Levi's",
|
||||
"H&M",
|
||||
"Zara",
|
||||
"Uniqlo",
|
||||
"Gap",
|
||||
"Puma",
|
||||
"Under Armour",
|
||||
"Calvin Klein",
|
||||
]
|
||||
colors = [
|
||||
"Black",
|
||||
"White",
|
||||
"Blue",
|
||||
"Red",
|
||||
"Green",
|
||||
"Gray",
|
||||
"Navy",
|
||||
"Brown",
|
||||
"Beige",
|
||||
"Maroon",
|
||||
]
|
||||
materials = [
|
||||
"Cotton",
|
||||
"Polyester",
|
||||
"Denim",
|
||||
"Wool",
|
||||
"Linen",
|
||||
"Silk",
|
||||
"Leather",
|
||||
"Nylon",
|
||||
"Rayon",
|
||||
"Spandex",
|
||||
]
|
||||
product_types = [
|
||||
"T-Shirt",
|
||||
"Shirt",
|
||||
"Jeans",
|
||||
"Pants",
|
||||
"Jacket",
|
||||
"Hoodie",
|
||||
"Sweater",
|
||||
"Shorts",
|
||||
"Dress",
|
||||
]
|
||||
|
||||
existing_products = Product.query.all()
|
||||
products = existing_products.copy()
|
||||
|
||||
existing_skus = {p.sku for p in existing_products if p.sku}
|
||||
new_skus = set()
|
||||
|
||||
for i in range(products_to_create):
|
||||
while True:
|
||||
sku = f"SKU-{uuid.uuid4().hex[:8].upper()}"
|
||||
if sku not in existing_skus and sku not in new_skus:
|
||||
new_skus.add(sku)
|
||||
break
|
||||
|
||||
category = random.choice(categories)
|
||||
price = round(random.uniform(15, 150), 2)
|
||||
stock = random.randint(0, 100)
|
||||
|
||||
product = Product(
|
||||
name=f"{random.choice(product_types)} - {fake.word().title()}",
|
||||
description=fake.paragraph(nb_sentences=3),
|
||||
price=price,
|
||||
stock=stock,
|
||||
color=random.choice(colors),
|
||||
size=random.choice(["XS", "S", "M", "L", "XL", "XXL"]),
|
||||
material=random.choice(materials),
|
||||
company=random.choice(clothing_brands),
|
||||
sku=sku,
|
||||
weight=round(random.uniform(0.1, 2.0), 2),
|
||||
dimensions=f"{
|
||||
random.randint(
|
||||
30,
|
||||
60)}x{
|
||||
random.randint(
|
||||
20,
|
||||
40)}x{
|
||||
random.randint(
|
||||
2,
|
||||
10)} cm",
|
||||
category_id=category.id,
|
||||
)
|
||||
db.session.add(product)
|
||||
products.append(product)
|
||||
|
||||
db.session.commit()
|
||||
print(f"✅ Created {products_to_create} new products")
|
||||
else:
|
||||
products = Product.query.all()
|
||||
print(f"⚠️ Already have {existing_products_count} products, skipping...")
|
||||
|
||||
print("🖼️ Creating product images...")
|
||||
new_products = [p for p in products if not p.images]
|
||||
|
||||
for product in new_products:
|
||||
num_images = random.randint(1, 4)
|
||||
for j in range(num_images):
|
||||
image = ProductImage(
|
||||
product_id=product.id,
|
||||
filename=f"product_{product.id}_img_{j + 1}.jpg",
|
||||
is_primary=(j == 0),
|
||||
display_order=j,
|
||||
)
|
||||
db.session.add(image)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
existing_reviews = Review.query.count()
|
||||
if existing_reviews < 100:
|
||||
print("⭐ Creating reviews...")
|
||||
products_without_reviews = [p for p in products if not p.reviews]
|
||||
products_to_review = random.sample(
|
||||
products_without_reviews, min(30, len(products_without_reviews))
|
||||
)
|
||||
|
||||
for product in products_to_review:
|
||||
num_reviews = random.randint(1, 8)
|
||||
for _ in range(num_reviews):
|
||||
user = random.choice(users[1:]) if len(users) > 1 else users[0]
|
||||
review = Review(
|
||||
product_id=product.id,
|
||||
user_id=user.id,
|
||||
rating=random.randint(1, 5),
|
||||
title=fake.sentence(),
|
||||
comment=fake.paragraph(),
|
||||
is_verified_purchase=random.choice([True, False]),
|
||||
created_at=fake.date_time_between(
|
||||
start_date="-1y", end_date="now"),
|
||||
)
|
||||
db.session.add(review)
|
||||
|
||||
db.session.commit()
|
||||
print(f"✅ Created {Review.query.count() - existing_reviews} new reviews")
|
||||
else:
|
||||
print(f"⚠️ Already have {existing_reviews} reviews, skipping...")
|
||||
|
||||
print("🛒 Creating carts...")
|
||||
users_without_carts = [u for u in users if not u.cart]
|
||||
|
||||
for user in users_without_carts[:5]:
|
||||
cart = Cart(user_id=user.id)
|
||||
db.session.add(cart)
|
||||
db.session.flush()
|
||||
|
||||
num_items = random.randint(1, 5)
|
||||
for _ in range(num_items):
|
||||
product = random.choice(products)
|
||||
cart_item = CartItem(
|
||||
cart_id=cart.id,
|
||||
product_id=product.id,
|
||||
quantity=random.randint(1, 3),
|
||||
selected_size=product.size,
|
||||
selected_color=product.color,
|
||||
)
|
||||
db.session.add(cart_item)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
existing_orders = Order.query.count()
|
||||
if existing_orders < 15:
|
||||
print("📦 Creating orders...")
|
||||
order_statuses = ["pending", "processing",
|
||||
"shipped", "delivered", "cancelled"]
|
||||
|
||||
orders_to_create = 15 - existing_orders
|
||||
for i in range(orders_to_create):
|
||||
user = random.choice(users)
|
||||
order = Order(
|
||||
user_id=user.id,
|
||||
status=random.choice(order_statuses),
|
||||
created_at=fake.date_time_between(start_date="-6m", end_date="now"),
|
||||
)
|
||||
db.session.add(order)
|
||||
db.session.flush()
|
||||
|
||||
num_items = random.randint(1, 5)
|
||||
|
||||
for _ in range(num_items):
|
||||
product = random.choice(products)
|
||||
quantity = random.randint(1, 3)
|
||||
price = product.price
|
||||
|
||||
order_item = OrderItem(
|
||||
order_id=order.id,
|
||||
product_id=product.id,
|
||||
quantity=quantity,
|
||||
price_at_purchase=price,
|
||||
selected_size=product.size,
|
||||
selected_color=product.color,
|
||||
)
|
||||
db.session.add(order_item)
|
||||
|
||||
db.session.commit()
|
||||
print(f"✅ Created {orders_to_create} new orders")
|
||||
else:
|
||||
print(f"⚠️ Already have {existing_orders} orders, skipping...")
|
||||
|
||||
print("❤️ Creating product likes...")
|
||||
existing_likes = ProductLike.query.count()
|
||||
|
||||
if existing_likes < 50:
|
||||
products_to_like = random.sample(products, min(20, len(products)))
|
||||
likes_created = 0
|
||||
|
||||
for product in products_to_like:
|
||||
|
||||
available_users = [u for u in users if u.id != 1]
|
||||
num_users_to_like = min(random.randint(3, 10), len(available_users))
|
||||
users_to_like = random.sample(available_users, num_users_to_like)
|
||||
|
||||
for user in users_to_like:
|
||||
|
||||
existing_like = ProductLike.query.filter_by(
|
||||
product_id=product.id, user_id=user.id
|
||||
).first()
|
||||
|
||||
if not existing_like:
|
||||
like = ProductLike(
|
||||
product_id=product.id,
|
||||
user_id=user.id,
|
||||
is_like=random.choice([True, False]),
|
||||
created_at=fake.date_time_between(
|
||||
start_date="-3m", end_date="now"
|
||||
),
|
||||
)
|
||||
db.session.add(like)
|
||||
likes_created += 1
|
||||
|
||||
db.session.commit()
|
||||
print(f"✅ Created {likes_created} new likes")
|
||||
else:
|
||||
print(f"⚠️ Already have {existing_likes} likes, skipping...")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("✅ DATABASE SEEDING COMPLETED SUCCESSFULLY!")
|
||||
print("=" * 50)
|
||||
print(f"\n📊 FINAL COUNTS:")
|
||||
print(f" 👥 Users: {User.query.count()}")
|
||||
print(f" 🏷️ Categories: {Category.query.count()}")
|
||||
print(f" 👕 Products: {Product.query.count()}")
|
||||
print(f" 🖼️ Product Images: {ProductImage.query.count()}")
|
||||
print(f" ⭐ Reviews: {Review.query.count()}")
|
||||
print(f" 🛒 Carts: {Cart.query.count()}")
|
||||
print(f" 📦 Orders: {Order.query.count()}")
|
||||
print(f" ❤️ Likes: {ProductLike.query.count()}")
|
||||
|
||||
print("\n🔑 LOGIN CREDENTIALS:")
|
||||
print(" 👑 Admin:")
|
||||
print(" Email: admin@wearwell.com")
|
||||
print(" Password: admin123")
|
||||
print("\n 👤 Test Users (10 available):")
|
||||
print(" Email: user1@example.com")
|
||||
print(" Password: password1")
|
||||
print(" Email: user2@example.com")
|
||||
print(" Password: password2")
|
||||
print(" ... and 8 more users (user3@example.com to user10@example.com)")
|
||||
|
||||
print("\n💡 TIPS:")
|
||||
print(" • Run this script multiple times to add more data")
|
||||
print(" • Data won't duplicate (checks for existing records)")
|
||||
print(" • To start fresh, clear your database first")
|
||||
|
||||
|
||||
def clear_and_seed():
|
||||
"""Clear all data and seed fresh"""
|
||||
print("⚠️ WARNING: This will delete ALL data from the database!")
|
||||
response = input("Are you sure? Type 'YES' to continue: ")
|
||||
|
||||
if response != "YES":
|
||||
print("❌ Operation cancelled")
|
||||
return
|
||||
|
||||
print("🗑️ Clearing database...")
|
||||
|
||||
ProductLike.query.delete()
|
||||
OrderItem.query.delete()
|
||||
Order.query.delete()
|
||||
CartItem.query.delete()
|
||||
Cart.query.delete()
|
||||
Review.query.delete()
|
||||
ProductImage.query.delete()
|
||||
Product.query.delete()
|
||||
Category.query.delete()
|
||||
User.query.delete()
|
||||
|
||||
db.session.commit()
|
||||
print("✅ Database cleared!")
|
||||
|
||||
create_seed_data()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--fresh":
|
||||
clear_and_seed()
|
||||
else:
|
||||
create_seed_data()
|
||||
169
static/css/add_product.css
Normal file
169
static/css/add_product.css
Normal file
@@ -0,0 +1,169 @@
|
||||
.admin-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.image-upload-area {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-upload-area:hover {
|
||||
border-color: #007bff;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.upload-prompt {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.upload-prompt svg {
|
||||
margin-bottom: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 0.9em;
|
||||
color: #888;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.image-input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.preview-item img {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.remove-image {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background: rgba(220, 53, 69, 0.9);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 24px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
336
static/css/auth.css
Normal file
336
static/css/auth.css
Normal file
@@ -0,0 +1,336 @@
|
||||
.auth-container {
|
||||
max-width: 500px;
|
||||
margin: 50px auto;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.auth-container h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
font-size: 2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
.auth-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 14px 16px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
font-size: 1em;
|
||||
transition: all 0.3s;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.form-control:hover {
|
||||
border-color: #b0b7c3;
|
||||
}
|
||||
|
||||
|
||||
.password-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle-password {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.toggle-password:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
|
||||
.btn-submit {
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-submit:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 7px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-submit:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
|
||||
.auth-links {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.auth-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
.remember-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.remember-me input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
|
||||
.error-message {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
color: #dc3545;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: rgba(40, 167, 69, 0.1);
|
||||
color: #28a745;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
|
||||
.flash-messages {
|
||||
max-width: 500px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.flash {
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.flash.success {
|
||||
background: rgba(40, 167, 69, 0.1);
|
||||
color: #28a745;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.flash.error,
|
||||
.flash.danger {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
color: #dc3545;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.flash.info {
|
||||
background: rgba(23, 162, 184, 0.1);
|
||||
color: #17a2b8;
|
||||
border-left: 4px solid #17a2b8;
|
||||
}
|
||||
|
||||
.flash.warning {
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
color: #ffc107;
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
|
||||
|
||||
.social-login {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.social-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.social-divider::before,
|
||||
.social-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.social-divider span {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.social-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-social {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.btn-social:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.btn-social.google {
|
||||
color: #db4437;
|
||||
}
|
||||
|
||||
.btn-social.facebook {
|
||||
color: #4267B2;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.auth-container {
|
||||
margin: 20px;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.auth-container h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.social-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-social {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.password-strength {
|
||||
margin-top: 5px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: #e9ecef;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.strength-bar {
|
||||
height: 100%;
|
||||
transition: width 0.3s, background 0.3s;
|
||||
}
|
||||
|
||||
.strength-weak {
|
||||
background: #dc3545;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.strength-medium {
|
||||
background: #ffc107;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.strength-strong {
|
||||
background: #28a745;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.strength-very-strong {
|
||||
background: #28a745;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.password-hints {
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.password-hints ul {
|
||||
margin: 5px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.terms {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.terms input[type="checkbox"] {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.terms a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.terms a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
349
static/css/cart.css
Normal file
349
static/css/cart.css
Normal file
@@ -0,0 +1,349 @@
|
||||
.cart-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.cart-container h1 {
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
|
||||
.cart-items {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cart-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.cart-table thead {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.cart-table th {
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
font-size: 0.9em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.cart-table tbody tr {
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.cart-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.cart-table td {
|
||||
padding: 20px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
.product-info {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.product-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.product-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.product-details h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1.1em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.product-description {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stock-info {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.in-stock {
|
||||
color: #28a745;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.low-stock {
|
||||
color: #ffc107;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.out-of-stock {
|
||||
color: #dc3545;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
.quantity-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-update {
|
||||
padding: 8px 12px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-update:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.max-quantity {
|
||||
font-size: 0.8em;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.price,
|
||||
.subtotal {
|
||||
font-weight: 600;
|
||||
color: #007bff;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
|
||||
.actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
padding: 8px 16px;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-remove:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
|
||||
.cart-summary {
|
||||
padding: 30px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
max-width: 400px;
|
||||
margin-left: auto;
|
||||
background: #f8f9fa;
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.summary-card h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #333;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.summary-row.total {
|
||||
font-size: 1.2em;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
border-bottom: none;
|
||||
padding-top: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
.cart-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.btn-continue,
|
||||
.btn-clear,
|
||||
.btn-checkout {
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.btn-continue {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-continue:hover {
|
||||
background: #5a6268;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-clear {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-clear:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.btn-checkout {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.btn-checkout:hover {
|
||||
background: #218838;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
.empty-cart {
|
||||
text-align: center;
|
||||
padding: 60px 40px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4em;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-cart h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.empty-cart p {
|
||||
color: #666;
|
||||
margin-bottom: 25px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.btn-shop {
|
||||
display: inline-block;
|
||||
padding: 12px 30px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.btn-shop:hover {
|
||||
background: #0056b3;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cart-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.cart-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-checkout {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
57
static/css/categories.css
Normal file
57
static/css/categories.css
Normal file
@@ -0,0 +1,57 @@
|
||||
.admin-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: #f8f9fa;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
186
static/css/checkout.css
Normal file
186
static/css/checkout.css
Normal file
@@ -0,0 +1,186 @@
|
||||
.checkout-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.checkout-container h1 {
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.checkout-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
|
||||
.order-summary {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.order-summary h3 {
|
||||
margin: 0 0 25px 0;
|
||||
color: #333;
|
||||
font-size: 1.3em;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #007bff;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.item-info h4 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1em;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.item-info p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.item-total {
|
||||
font-weight: 600;
|
||||
color: #007bff;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.order-total {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.order-total h3 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 1.4em;
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
.checkout-form {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.checkout-form h3 {
|
||||
margin: 0 0 25px 0;
|
||||
color: #333;
|
||||
font-size: 1.3em;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #007bff;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 1em;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.btn-back,
|
||||
.btn-confirm {
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
font-size: 1em;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
background: #5a6268;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.btn-confirm:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.checkout-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-back,
|
||||
.btn-confirm {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
343
static/css/dashboard.css
Normal file
343
static/css/dashboard.css
Normal file
@@ -0,0 +1,343 @@
|
||||
.admin-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
|
||||
.admin-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #007bff;
|
||||
}
|
||||
|
||||
.admin-header h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
|
||||
.admin-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2.5em;
|
||||
font-weight: 700;
|
||||
color: #007bff;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 2em;
|
||||
color: #007bff;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.admin-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-admin {
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0056b3;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ffc107;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #e0a800;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
.admin-table-container {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.admin-table thead {
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
.admin-table th {
|
||||
padding: 18px 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
font-size: 0.9em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.admin-table tbody tr {
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.admin-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.admin-table td {
|
||||
padding: 15px;
|
||||
vertical-align: middle;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
|
||||
.admin-table td:first-child {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.admin-table img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
background-color: #f8f9fa;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
.stock-high {
|
||||
color: #28a745;
|
||||
font-weight: 600;
|
||||
background: rgba(40, 167, 69, 0.1);
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stock-low {
|
||||
color: #ffc107;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stock-out {
|
||||
color: #dc3545;
|
||||
font-weight: 600;
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
|
||||
.admin-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
margin-bottom: 30px;
|
||||
background: white;
|
||||
border-radius: 10px 10px 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 15px 30px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #007bff;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #007bff;
|
||||
border-bottom-color: #007bff;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 0 0 10px 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.page-link {
|
||||
padding: 8px 15px;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: #007bff;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.9em;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-link:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.page-link.active {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
padding: 8px 5px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
|
||||
.badge-active,
|
||||
.badge-inactive {
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background: rgba(40, 167, 69, 0.1);
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background: rgba(108, 117, 125, 0.1);
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.admin-stats {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.admin-tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
227
static/css/edit_product.css
Normal file
227
static/css/edit_product.css
Normal file
@@ -0,0 +1,227 @@
|
||||
.admin-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.current-images {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.image-item img {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.image-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.primary-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-delete-image {
|
||||
padding: 5px 10px;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.btn-delete-image:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.no-images {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #ddd;
|
||||
}
|
||||
|
||||
.image-upload-area {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.image-upload-area:hover {
|
||||
border-color: #007bff;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.upload-prompt {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.upload-prompt svg {
|
||||
margin-bottom: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.image-input {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.preview-item img {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.remove-image {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background: rgba(220, 53, 69, 0.9);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 24px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
447
static/css/home.css
Normal file
447
static/css/home.css
Normal file
@@ -0,0 +1,447 @@
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 80px 20px;
|
||||
text-align: center;
|
||||
margin-bottom: 50px;
|
||||
border-radius: 0 0 20px 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="%23ffffff" fill-opacity="0.1" d="M0,96L48,112C96,128,192,160,288,160C384,160,480,128,576,112C672,96,768,96,864,112C960,128,1056,160,1152,160C1248,160,1344,128,1392,112L1440,96L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path></svg>');
|
||||
background-size: cover;
|
||||
background-position: bottom;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3.5em;
|
||||
font-weight: 700;
|
||||
margin-bottom: 20px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.3em;
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.9;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hero-search {
|
||||
max-width: 600px;
|
||||
margin: 30px auto 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-search-input {
|
||||
width: 100%;
|
||||
padding: 18px 60px 18px 25px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
font-size: 1.1em;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.hero-search-button {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.hero-search-button:hover {
|
||||
background: #ff5252;
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
.categories-section {
|
||||
padding: 40px 20px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2em;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.view-all-link {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.view-all-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.categories-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.category-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.category-image {
|
||||
height: 180px;
|
||||
background: #f8f9fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.category-count {
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
font-weight: normal;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
|
||||
.featured-section {
|
||||
padding: 40px 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.product-card-home {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.product-card-home:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.product-badge {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.product-image-home {
|
||||
height: 200px;
|
||||
background: #f8f9fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.product-image-home img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.product-info-home {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.product-title-home {
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.product-price-home {
|
||||
font-size: 1.3em;
|
||||
font-weight: 700;
|
||||
color: #007bff;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.product-rating-home {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.stars {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.rating-count {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.product-meta-home {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
|
||||
.promo-banner {
|
||||
background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
padding: 60px 40px;
|
||||
border-radius: 15px;
|
||||
margin: 50px 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.promo-content h2 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.promo-content p {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.promo-button {
|
||||
display: inline-block;
|
||||
background: white;
|
||||
color: #4facfe;
|
||||
padding: 15px 40px;
|
||||
border-radius: 50px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 1.1em;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.promo-button:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
|
||||
color: #4facfe;
|
||||
}
|
||||
|
||||
|
||||
.benefits-section {
|
||||
padding: 60px 20px;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.benefits-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.benefit-card {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.benefit-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.benefit-icon {
|
||||
font-size: 3em;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.benefit-title {
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.benefit-description {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
|
||||
.newsletter-section {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
padding: 60px 20px;
|
||||
border-radius: 15px;
|
||||
margin: 50px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.newsletter-content {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.newsletter-content h2 {
|
||||
font-size: 2.2em;
|
||||
margin-bottom: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.newsletter-content p {
|
||||
font-size: 1.1em;
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.newsletter-form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.newsletter-input {
|
||||
flex: 1;
|
||||
padding: 15px 20px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.newsletter-button {
|
||||
padding: 15px 30px;
|
||||
background: #333;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.newsletter-button:hover {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-title {
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.categories-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.category-image {
|
||||
height: 140px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.promo-content h2 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.newsletter-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.newsletter-input,
|
||||
.newsletter-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
281
static/css/order_details.css
Normal file
281
static/css/order_details.css
Normal file
@@ -0,0 +1,281 @@
|
||||
.order-details-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.order-details-container h2 {
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.order-meta {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.order-meta p {
|
||||
margin: 8px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.order-meta strong {
|
||||
color: #333;
|
||||
min-width: 120px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.order-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
|
||||
.items-section h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-size: 1.4em;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.items-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.items-table thead {
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
.items-table th {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.items-table tbody tr {
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.items-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.items-table td {
|
||||
padding: 15px;
|
||||
color: #495057;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.quantity-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.price-cell,
|
||||
.subtotal-cell {
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
|
||||
.order-summary {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
max-width: 400px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.summary-row:last-child {
|
||||
border-bottom: none;
|
||||
font-size: 1.2em;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
padding-top: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-weight: 600;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
|
||||
.order-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
padding: 10px 20px;
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
background: #5a6268;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-print {
|
||||
padding: 10px 20px;
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-print:hover {
|
||||
background: #138496;
|
||||
}
|
||||
|
||||
|
||||
.no-items {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.no-items p {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
|
||||
.product-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.product-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.product-variants {
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.order-details-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.items-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.items-table th,
|
||||
.items-table td {
|
||||
padding: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.product-cell {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.order-summary {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.order-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-back,
|
||||
.btn-print {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
280
static/css/product_detail.css
Normal file
280
static/css/product_detail.css
Normal file
@@ -0,0 +1,280 @@
|
||||
.product-detail-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.product-detail {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 50px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
.product-gallery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.main-image-container {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.main-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.thumbnail-gallery {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.thumbnail-container {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.2s;
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.thumbnail-container.active {
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
|
||||
.product-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.product-info h1 {
|
||||
margin: 0;
|
||||
font-size: 2em;
|
||||
color: #333;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.product-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid #e9ecef;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.meta-item strong {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
|
||||
.product-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.stars {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.star {
|
||||
color: #ddd;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.star.filled {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.rating-value {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.review-count {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
|
||||
.price-section {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 2.5em;
|
||||
font-weight: 700;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
|
||||
.description-section {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.description-section p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
|
||||
.specifications {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.spec-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.spec-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.spec-label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.spec-value {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
|
||||
.actions-section {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: white;
|
||||
color: #007bff;
|
||||
border: 2px solid #007bff;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
.related-products {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.related-products h2 {
|
||||
margin-bottom: 30px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.related-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.product-detail {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.main-image-container {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.product-meta,
|
||||
.spec-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
441
static/css/products.css
Normal file
441
static/css/products.css
Normal file
@@ -0,0 +1,441 @@
|
||||
.products-page {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 30px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.filters-sidebar {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.filters-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.filters-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.clear-filters {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.clear-filters:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.filters-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.filter-input,
|
||||
.filter-select {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 0.95em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-input:focus,
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.price-range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.price-input {
|
||||
flex: 1;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.price-separator {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.btn-apply-filters {
|
||||
padding: 12px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-apply-filters:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
|
||||
.products-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.products-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.results-info h1 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.results-count {
|
||||
color: #666;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.active-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
background: #e9ecef;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.filter-tag a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.filter-tag a:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sort-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sort-controls label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.sort-select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 0.95em;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.products-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-card:hover .image-carousel {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.image-carousel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.image-carousel img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.product-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
position: relative;
|
||||
height: 280px;
|
||||
|
||||
overflow: hidden;
|
||||
background-color: #f8f9fa;
|
||||
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.product-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
transition: transform 0.3s ease;
|
||||
padding: 15px;
|
||||
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
|
||||
.product-card:hover .product-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.out-of-stock,
|
||||
.low-stock {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.out-of-stock {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.low-stock {
|
||||
background: #ffc107;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.product-rating {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 12px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 6px 10px;
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.rating-stars {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.rating-value {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.product-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
background: #f8f9fa;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.meta-item svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.product-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-size: 1.3em;
|
||||
font-weight: 700;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-add-to-cart,
|
||||
.btn-out-of-stock,
|
||||
.btn-login-to-buy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-add-to-cart {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-add-to-cart:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.btn-out-of-stock {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-login-to-buy {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-login-to-buy:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
|
||||
.no-products {
|
||||
text-align: center;
|
||||
padding: 60px 40px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.no-products-icon {
|
||||
font-size: 4em;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.no-products h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.no-products p {
|
||||
color: #666;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.btn-view-all {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-view-all:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
211
static/css/profile.css
Normal file
211
static/css/profile.css
Normal file
@@ -0,0 +1,211 @@
|
||||
.profile-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.profile-container h2 {
|
||||
margin-bottom: 25px;
|
||||
color: #333;
|
||||
font-size: 1.8em;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.profile-info {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.profile-info p {
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.profile-info strong {
|
||||
color: #333;
|
||||
min-width: 100px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.orders-section h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.orders-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.orders-table thead {
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
.orders-table th {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 0.9em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.orders-table tbody tr {
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.orders-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.orders-table td {
|
||||
padding: 15px;
|
||||
color: #495057;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.order-id {
|
||||
font-weight: 600;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.order-date {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.order-items {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.order-actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.view-details-link {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.view-details-link:hover {
|
||||
background: #218838;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
.no-orders {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.no-orders p {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.shop-now-link {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.shop-now-link:hover {
|
||||
background: #0056b3;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
.status-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background: rgba(0, 123, 255, 0.1);
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.status-shipped {
|
||||
background: rgba(23, 162, 184, 0.1);
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.status-delivered {
|
||||
background: rgba(40, 167, 69, 0.1);
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-cancelled {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.profile-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.orders-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.orders-table th,
|
||||
.orders-table td {
|
||||
padding: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
207
static/css/styles.css
Normal file
207
static/css/styles.css
Normal file
@@ -0,0 +1,207 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: calc(100vh - 200px);
|
||||
|
||||
}
|
||||
|
||||
nav {
|
||||
background: #333;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
|
||||
.flash-messages {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.flash {
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.flash.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.flash.danger {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.flash.info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cart-count {
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.8em;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.visible {
|
||||
display: block;
|
||||
}
|
||||
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
static/images/default-product.jpg
Normal file
BIN
static/images/default-product.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
88
static/js/add_product.js
Normal file
88
static/js/add_product.js
Normal file
@@ -0,0 +1,88 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const imageInput = document.getElementById('image-input');
|
||||
const imagePreview = document.getElementById('image-preview');
|
||||
const uploadArea = document.getElementById('image-upload-area');
|
||||
|
||||
imageInput.addEventListener('change', function (e) {
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, highlight, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, unhighlight, false);
|
||||
});
|
||||
|
||||
function highlight() {
|
||||
uploadArea.style.borderColor = '#007bff';
|
||||
uploadArea.style.background = '#f8f9fa';
|
||||
}
|
||||
|
||||
function unhighlight() {
|
||||
uploadArea.style.borderColor = '#ccc';
|
||||
uploadArea.style.background = '';
|
||||
}
|
||||
|
||||
uploadArea.addEventListener('drop', handleDrop, false);
|
||||
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
imageInput.files = files;
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
imagePreview.innerHTML = '';
|
||||
const files = imageInput.files;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (!file.type.match('image.*')) continue;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (e) {
|
||||
const previewItem = document.createElement('div');
|
||||
previewItem.className = 'preview-item';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = e.target.result;
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'remove-image';
|
||||
removeBtn.innerHTML = '×';
|
||||
removeBtn.onclick = function () {
|
||||
previewItem.remove();
|
||||
const dt = new DataTransfer();
|
||||
const inputFiles = imageInput.files;
|
||||
|
||||
for (let j = 0; j < inputFiles.length; j++) {
|
||||
if (j !== i) {
|
||||
dt.items.add(inputFiles[j]);
|
||||
}
|
||||
}
|
||||
|
||||
imageInput.files = dt.files;
|
||||
};
|
||||
|
||||
previewItem.appendChild(img);
|
||||
previewItem.appendChild(removeBtn);
|
||||
imagePreview.appendChild(previewItem);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
118
static/js/edit_product.js
Normal file
118
static/js/edit_product.js
Normal file
@@ -0,0 +1,118 @@
|
||||
let deletedImages = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const imageInput = document.getElementById('image-input');
|
||||
const imagePreview = document.getElementById('image-preview');
|
||||
const uploadArea = document.getElementById('image-upload-area');
|
||||
|
||||
imageInput.addEventListener('change', function (e) {
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, highlight, false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, unhighlight, false);
|
||||
});
|
||||
|
||||
function highlight() {
|
||||
uploadArea.style.borderColor = '#007bff';
|
||||
uploadArea.style.background = '#f8f9fa';
|
||||
}
|
||||
|
||||
function unhighlight() {
|
||||
uploadArea.style.borderColor = '#ccc';
|
||||
uploadArea.style.background = '';
|
||||
}
|
||||
|
||||
uploadArea.addEventListener('drop', handleDrop, false);
|
||||
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
imageInput.files = files;
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
imagePreview.innerHTML = '';
|
||||
const files = imageInput.files;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (!file.type.match('image.*')) continue;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (e) {
|
||||
const previewItem = document.createElement('div');
|
||||
previewItem.className = 'preview-item';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = e.target.result;
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'remove-image';
|
||||
removeBtn.innerHTML = '×';
|
||||
removeBtn.onclick = function () {
|
||||
previewItem.remove();
|
||||
const dt = new DataTransfer();
|
||||
const inputFiles = imageInput.files;
|
||||
|
||||
for (let j = 0; j < inputFiles.length; j++) {
|
||||
if (j !== i) {
|
||||
dt.items.add(inputFiles[j]);
|
||||
}
|
||||
}
|
||||
|
||||
imageInput.files = dt.files;
|
||||
};
|
||||
|
||||
previewItem.appendChild(img);
|
||||
previewItem.appendChild(removeBtn);
|
||||
imagePreview.appendChild(previewItem);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function deleteImage(imageId) {
|
||||
if (confirm('Delete this image?')) {
|
||||
deletedImages.push(imageId);
|
||||
document.getElementById('deleted-images').value = JSON.stringify(deletedImages);
|
||||
|
||||
const imageItem = document.querySelector('.image-item[data-id="' + imageId + '"]');
|
||||
if (imageItem) {
|
||||
imageItem.style.display = 'none';
|
||||
}
|
||||
|
||||
fetch('/admin/image/delete/' + imageId, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
}).then(function (response) {
|
||||
return response.json();
|
||||
}).then(function (data) {
|
||||
if (!data.success) {
|
||||
alert('Error deleting image: ' + data.error);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
24
static/js/home.js
Normal file
24
static/js/home.js
Normal file
@@ -0,0 +1,24 @@
|
||||
document.querySelector('.newsletter-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const email = this.querySelector('.newsletter-input').value;
|
||||
|
||||
alert(`Thank you for subscribing with: ${email}\nYou'll hear from us soon!`);
|
||||
this.querySelector('.newsletter-input').value = '';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.product-card-home').forEach(card => {
|
||||
card.addEventListener('mouseenter', function () {
|
||||
const img = this.querySelector('img');
|
||||
if (img) {
|
||||
img.style.transform = 'scale(1.05)';
|
||||
img.style.transition = 'transform 0.3s ease';
|
||||
}
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', function () {
|
||||
const img = this.querySelector('img');
|
||||
if (img) {
|
||||
img.style.transform = 'scale(1)';
|
||||
}
|
||||
});
|
||||
});
|
||||
47
static/js/login.js
Normal file
47
static/js/login.js
Normal file
@@ -0,0 +1,47 @@
|
||||
function togglePassword(inputId) {
|
||||
const input = document.getElementById(inputId);
|
||||
const button = event.target;
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
button.textContent = '🙈';
|
||||
} else {
|
||||
input.type = 'password';
|
||||
button.textContent = '👁️';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.querySelector('.auth-form');
|
||||
const emailInput = document.getElementById('email');
|
||||
const passwordInput = document.getElementById('password');
|
||||
|
||||
[emailInput, passwordInput].forEach(input => {
|
||||
input.addEventListener('focus', function () {
|
||||
this.parentElement.classList.add('focused');
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function () {
|
||||
this.parentElement.classList.remove('focused');
|
||||
});
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
const email = emailInput.value.trim();
|
||||
const password = passwordInput.value.trim();
|
||||
|
||||
if (!email || !password) {
|
||||
e.preventDefault();
|
||||
alert('Please fill in all fields');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!email.includes('@')) {
|
||||
e.preventDefault();
|
||||
alert('Please enter a valid email address');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
70
static/js/product_detail.js
Normal file
70
static/js/product_detail.js
Normal file
@@ -0,0 +1,70 @@
|
||||
function changeImage(thumbnailElement) {
|
||||
const imageUrl = thumbnailElement.getAttribute('data-image-url');
|
||||
|
||||
document.getElementById('mainImage').src = imageUrl;
|
||||
|
||||
document.querySelectorAll('.thumbnail-container').forEach(thumb => {
|
||||
thumb.classList.remove('active');
|
||||
});
|
||||
thumbnailElement.classList.add('active');
|
||||
}
|
||||
|
||||
function deleteImage(imageId) {
|
||||
if (confirm('Are you sure you want to delete this image?')) {
|
||||
fetch(`/admin/delete-image/${imageId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const imageElement = document.querySelector(`[data-image-id="${imageId}"]`);
|
||||
if (imageElement) {
|
||||
imageElement.remove();
|
||||
}
|
||||
alert('Image deleted successfully');
|
||||
} else {
|
||||
alert('Failed to delete image');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error deleting image');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const thumbnails = document.querySelectorAll('.thumbnail-container');
|
||||
|
||||
thumbnails.forEach((thumb, index) => {
|
||||
thumb.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
changeImage(this);
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
const next = thumbnails[index + 1];
|
||||
if (next) {
|
||||
next.focus();
|
||||
changeImage(next);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
const prev = thumbnails[index - 1];
|
||||
if (prev) {
|
||||
prev.focus();
|
||||
changeImage(prev);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
thumb.setAttribute('tabindex', '0');
|
||||
});
|
||||
});
|
||||
3
static/js/products.js
Normal file
3
static/js/products.js
Normal file
@@ -0,0 +1,3 @@
|
||||
document.getElementById('sort').addEventListener('change', function () {
|
||||
this.closest('form').submit();
|
||||
});
|
||||
159
static/js/register.js
Normal file
159
static/js/register.js
Normal file
@@ -0,0 +1,159 @@
|
||||
function togglePassword(inputId) {
|
||||
const input = document.getElementById(inputId);
|
||||
const button = event.target;
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
button.textContent = '🙈';
|
||||
} else {
|
||||
input.type = 'password';
|
||||
button.textContent = '👁️';
|
||||
}
|
||||
}
|
||||
|
||||
function checkPasswordStrength(password) {
|
||||
const strengthBar = document.getElementById('strengthBar');
|
||||
const hints = {
|
||||
lengthHint: document.getElementById('lengthHint'),
|
||||
uppercaseHint: document.getElementById('uppercaseHint'),
|
||||
lowercaseHint: document.getElementById('lowercaseHint'),
|
||||
numberHint: document.getElementById('numberHint'),
|
||||
specialHint: document.getElementById('specialHint')
|
||||
};
|
||||
|
||||
let strength = 0;
|
||||
let messages = [];
|
||||
|
||||
if (password.length >= 8) {
|
||||
strength += 20;
|
||||
hints.lengthHint.style.color = '#28a745';
|
||||
hints.lengthHint.style.textDecoration = 'line-through';
|
||||
} else {
|
||||
hints.lengthHint.style.color = '#666';
|
||||
hints.lengthHint.style.textDecoration = 'none';
|
||||
}
|
||||
|
||||
if (/[A-Z]/.test(password)) {
|
||||
strength += 20;
|
||||
hints.uppercaseHint.style.color = '#28a745';
|
||||
hints.uppercaseHint.style.textDecoration = 'line-through';
|
||||
} else {
|
||||
hints.uppercaseHint.style.color = '#666';
|
||||
hints.uppercaseHint.style.textDecoration = 'none';
|
||||
}
|
||||
|
||||
if (/[a-z]/.test(password)) {
|
||||
strength += 20;
|
||||
hints.lowercaseHint.style.color = '#28a745';
|
||||
hints.lowercaseHint.style.textDecoration = 'line-through';
|
||||
} else {
|
||||
hints.lowercaseHint.style.color = '#666';
|
||||
hints.lowercaseHint.style.textDecoration = 'none';
|
||||
}
|
||||
|
||||
if (/[0-9]/.test(password)) {
|
||||
strength += 20;
|
||||
hints.numberHint.style.color = '#28a745';
|
||||
hints.numberHint.style.textDecoration = 'line-through';
|
||||
} else {
|
||||
hints.numberHint.style.color = '#666';
|
||||
hints.numberHint.style.textDecoration = 'none';
|
||||
}
|
||||
|
||||
if (/[^A-Za-z0-9]/.test(password)) {
|
||||
strength += 20;
|
||||
hints.specialHint.style.color = '#28a745';
|
||||
hints.specialHint.style.textDecoration = 'line-through';
|
||||
} else {
|
||||
hints.specialHint.style.color = '#666';
|
||||
hints.specialHint.style.textDecoration = 'none';
|
||||
}
|
||||
|
||||
strengthBar.className = 'strength-bar';
|
||||
if (strength <= 25) {
|
||||
strengthBar.classList.add('strength-weak');
|
||||
} else if (strength <= 50) {
|
||||
strengthBar.classList.add('strength-medium');
|
||||
} else if (strength <= 75) {
|
||||
strengthBar.classList.add('strength-strong');
|
||||
} else {
|
||||
strengthBar.classList.add('strength-very-strong');
|
||||
}
|
||||
strengthBar.style.width = strength + '%';
|
||||
}
|
||||
|
||||
function checkPasswordMatch() {
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = document.getElementById('confirm_password').value;
|
||||
const matchElement = document.getElementById('passwordMatch');
|
||||
|
||||
if (!confirmPassword) {
|
||||
matchElement.textContent = '';
|
||||
matchElement.style.color = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (password === confirmPassword) {
|
||||
matchElement.textContent = '✓ Passwords match';
|
||||
matchElement.style.color = '#28a745';
|
||||
} else {
|
||||
matchElement.textContent = '✗ Passwords do not match';
|
||||
matchElement.style.color = '#dc3545';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const form = document.querySelector('.auth-form');
|
||||
const emailInput = document.getElementById('email');
|
||||
const passwordInput = document.getElementById('password');
|
||||
const confirmInput = document.getElementById('confirm_password');
|
||||
const termsCheckbox = document.getElementById('terms');
|
||||
|
||||
[emailInput, passwordInput, confirmInput].forEach(input => {
|
||||
input.addEventListener('focus', function () {
|
||||
this.parentElement.classList.add('focused');
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function () {
|
||||
this.parentElement.classList.remove('focused');
|
||||
});
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
const email = emailInput.value.trim();
|
||||
const password = passwordInput.value.trim();
|
||||
const confirmPassword = confirmInput.value.trim();
|
||||
|
||||
if (!email || !password || !confirmPassword) {
|
||||
e.preventDefault();
|
||||
alert('Please fill in all fields');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!email.includes('@')) {
|
||||
e.preventDefault();
|
||||
alert('Please enter a valid email address');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
e.preventDefault();
|
||||
alert('Password must be at least 8 characters long');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
e.preventDefault();
|
||||
alert('Passwords do not match');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!termsCheckbox.checked) {
|
||||
e.preventDefault();
|
||||
alert('You must agree to the Terms of Service');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
});
|
||||
131
templates/admin/add_product.html
Normal file
131
templates/admin/add_product.html
Normal file
@@ -0,0 +1,131 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/add_product.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/add_product.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="admin-container">
|
||||
<h2>Add New Product</h2>
|
||||
<form method="POST" enctype="multipart/form-data" class="product-form">
|
||||
<div class="form-grid">
|
||||
<div class="form-section">
|
||||
<h3>Basic Information</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Product Name *</label>
|
||||
<input type="text" id="name" name="name" required class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sku">SKU (Stock Keeping Unit)</label>
|
||||
<input type="text" id="sku" name="sku" class="form-control" placeholder="e.g., TSHIRT-RED-M">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" class="form-control" rows="4"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="price">Price ($) *</label>
|
||||
<input type="number" step="0.01" id="price" name="price" required class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="stock">Stock Quantity *</label>
|
||||
<input type="number" id="stock" name="stock" value="0" min="0" required class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h3>Product Details</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="category_id">Category *</label>
|
||||
<select id="category_id" name="category_id" class="form-control">
|
||||
<option value="">-- Select Category --</option>
|
||||
{% for c in categories %}
|
||||
<option value="{{ c.id }}">{{ c.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="material">Material</label>
|
||||
<input type="text" id="material" name="material" class="form-control" placeholder="e.g., Cotton, Polyester">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="size">Size</label>
|
||||
<select id="size" name="size" class="form-control">
|
||||
<option value="">-- Select Size --</option>
|
||||
{% for size in sizes %}
|
||||
<option value="{{ size }}">{{ size }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="color">Color</label>
|
||||
<input type="text" id="color" name="color" class="form-control" placeholder="e.g., Red, Blue">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="company">Brand/Company</label>
|
||||
<input type="text" id="company" name="company" class="form-control" placeholder="e.g., Nike, Adidas">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="weight">Weight (kg)</label>
|
||||
<input type="number" step="0.01" id="weight" name="weight" class="form-control" placeholder="0.5">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dimensions">Dimensions</label>
|
||||
<input type="text" id="dimensions" name="dimensions" class="form-control" placeholder="10x5x3 cm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h3>Product Images</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Upload Images *</label>
|
||||
<div class="image-upload-area" id="image-upload-area">
|
||||
<div class="upload-prompt">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
<p>Click or drag images here</p>
|
||||
<p class="upload-hint">First image will be primary</p>
|
||||
</div>
|
||||
<input type="file" name="images[]" multiple accept="image/*" class="image-input" id="image-input">
|
||||
</div>
|
||||
|
||||
<div class="image-preview" id="image-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Save Product</button>
|
||||
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
72
templates/admin/categories.html
Normal file
72
templates/admin/categories.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/categories.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="admin-container">
|
||||
<h2>Manage Categories</h2>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h3>Add New Category</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('admin.add_category') }}" class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<input type="text" name="name" class="form-control" placeholder="Category name" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="submit" class="btn btn-primary w-100">Add Category</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>All Categories ({{ categories|length }})</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if categories %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Products</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for category in categories %}
|
||||
<tr>
|
||||
<td>{{ category.id }}</td>
|
||||
<td>{{ category.name }}</td>
|
||||
<td>{{ category.products|length }}</td>
|
||||
<td>
|
||||
{% if category.products|length == 0 %}
|
||||
<form method="POST" action="{{ url_for('admin.delete_category', id=category.id) }}"
|
||||
style="display: inline;" onsubmit="return confirm('Delete category {{ category.name }}?')">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-secondary" disabled title="Cannot delete category with products">
|
||||
Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">No categories found.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
54
templates/admin/dashboard.html
Normal file
54
templates/admin/dashboard.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Admin Dashboard</h2>
|
||||
<a href="{{ url_for('admin.add_product') }}" class="btn btn-success">Add New Product</a>
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Price</th>
|
||||
<th>Stock</th>
|
||||
<th>Category</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in products %}
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ p.get_image_url() }}" alt="{{ p.name }}" style="width: 60px; height: 60px; object-fit: cover;">
|
||||
</td>
|
||||
<td>{{ p.id }}</td>
|
||||
<td>{{ p.name }}</td>
|
||||
<td>{{ p.description[:50] }}{% if p.description|length > 50 %}...{% endif %}</td>
|
||||
<td>${{ "%.2f"|format(p.price) }}</td>
|
||||
<td>
|
||||
{% if p.stock > 10 %}
|
||||
<span class="stock-high">{{ p.stock }}</span>
|
||||
{% elif p.stock > 0 %}
|
||||
<span class="stock-low">{{ p.stock }}</span>
|
||||
{% else %}
|
||||
<span class="stock-out">Out of Stock</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ p.category.name if p.category else 'N/A' }}</td>
|
||||
<td class="actions">
|
||||
<a href="{{ url_for('admin.edit_product', id=p.id) }}" class="btn btn-sm btn-primary">Edit</a>
|
||||
<form method="POST" action="{{ url_for('admin.delete_product', id=p.id) }}" style="display:inline;"
|
||||
onsubmit="return confirm('Delete this product?')">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
169
templates/admin/edit_product.html
Normal file
169
templates/admin/edit_product.html
Normal file
@@ -0,0 +1,169 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/edit_product.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/edit_product.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="admin-container">
|
||||
<h2>Edit Product: {{ product.name }}</h2>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data" class="product-form" id="product-form">
|
||||
<div class="form-grid">
|
||||
|
||||
<div class="form-section">
|
||||
<h3>Basic Information</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Product Name *</label>
|
||||
<input type="text" id="name" name="name" value="{{ product.name }}" required class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sku">SKU (Stock Keeping Unit)</label>
|
||||
<input type="text" id="sku" name="sku" value="{{ product.sku or '' }}" class="form-control"
|
||||
placeholder="e.g., TSHIRT-RED-M">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" class="form-control"
|
||||
rows="4">{{ product.description or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="price">Price ($) *</label>
|
||||
<input type="number" step="0.01" id="price" name="price" value="{{ product.price }}" required
|
||||
class="form-control" min="0.01">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="stock">Stock Quantity *</label>
|
||||
<input type="number" id="stock" name="stock" value="{{ product.stock }}" min="0" required
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h3>Product Details</h3>
|
||||
<div class="form-group">
|
||||
<label for="category_id">Category *</label>
|
||||
<select id="category_id" name="category_id" class="form-control" required>
|
||||
<option value="">-- Select Category --</option>
|
||||
{% for c in categories %}
|
||||
<option value="{{ c.id }}" {% if product.category_id==c.id %}selected{% endif %}>
|
||||
{{ c.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="material">Material</label>
|
||||
<input type="text" id="material" name="material" value="{{ product.material or '' }}" class="form-control"
|
||||
placeholder="e.g., Cotton, Polyester, Denim">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="size">Size</label>
|
||||
<select id="size" name="size" class="form-control">
|
||||
<option value="">-- Select Size --</option>
|
||||
{% for size in sizes %}
|
||||
<option value="{{ size }}" {% if product.size==size %}selected{% endif %}>
|
||||
{{ size }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="color">Color</label>
|
||||
<input type="text" id="color" name="color" value="{{ product.color or '' }}" class="form-control"
|
||||
placeholder="e.g., Red, Blue, Black">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="company">Brand/Company</label>
|
||||
<input type="text" id="company" name="company" value="{{ product.company or '' }}" class="form-control"
|
||||
placeholder="e.g., Nike, Adidas, Levi's">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="weight">Weight (kg)</label>
|
||||
<input type="number" step="0.01" id="weight" name="weight" value="{{ product.weight or '' }}"
|
||||
class="form-control" placeholder="0.5" min="0">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dimensions">Dimensions</label>
|
||||
<input type="text" id="dimensions" name="dimensions" value="{{ product.dimensions or '' }}"
|
||||
class="form-control" placeholder="10x5x3 cm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h3>Current Images</h3>
|
||||
<div class="current-images">
|
||||
{% if product.images %}
|
||||
<div class="image-grid">
|
||||
{% for image in product.images %}
|
||||
<div class="image-item" data-id="{{ image.id }}">
|
||||
<img src="/static/uploads/products/{{ image.filename }}" alt="Product Image {{ loop.index }}">
|
||||
<div class="image-actions">
|
||||
<label class="primary-checkbox">
|
||||
<input type="radio" name="primary_image" value="{{ image.id }}" {% if image.is_primary %}checked{%
|
||||
endif %}>
|
||||
Primary
|
||||
</label>
|
||||
<button type="button" class="btn-delete-image" onclick="deleteImage('{{ image.id }}')">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="no-images">No images uploaded yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 30px;">Add New Images</h3>
|
||||
<div class="form-group">
|
||||
<div class="image-upload-area" id="image-upload-area">
|
||||
<div class="upload-prompt">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
<p>Click or drag images here</p>
|
||||
</div>
|
||||
<input type="file" name="images[]" multiple accept="image/*" class="image-input" id="image-input">
|
||||
</div>
|
||||
|
||||
<div class="image-preview" id="image-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="deleted_images" id="deleted-images" value="">
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Update Product</button>
|
||||
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
61
templates/base.html
Normal file
61
templates/base.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WearWell Shop{% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
|
||||
{% block styles %}{% endblock %}
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="{{ url_for('main.home') }}">Home</a>
|
||||
<a href="{{ url_for('main.products') }}">Products</a>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('cart.view_cart') }}">
|
||||
Cart {% if current_user.cart and current_user.cart.items|length > 0 %}
|
||||
<span class="cart-count">{{ current_user.cart.items|length }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="{{ url_for('user.profile') }}">My Profile</a>
|
||||
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('admin.dashboard') }}">Admin Panel</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('auth.logout') }}">Logout</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login') }}">Login</a>
|
||||
<a href="{{ url_for('auth.register') }}">Register</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
|
||||
<div class="flash-messages">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %} {% endwith %}
|
||||
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="container text-center">
|
||||
<p>© 2025 WearWell Shop. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
114
templates/cart.html
Normal file
114
templates/cart.html
Normal file
@@ -0,0 +1,114 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/cart.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="cart-container">
|
||||
<h1>Your Shopping Cart</h1>
|
||||
|
||||
{% if items %}
|
||||
<div class="cart-items">
|
||||
<table class="cart-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Price</th>
|
||||
<th>Quantity</th>
|
||||
<th>Subtotal</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td class="product-info">
|
||||
<div class="product-image">
|
||||
<img src="{{ item.product.get_image_url() }}" alt="{{ item.product.name }}">
|
||||
</div>
|
||||
<div class="product-details">
|
||||
<h4>{{ item.product.name }}</h4>
|
||||
<p class="product-description">{{ item.product.description[:80] }}...</p>
|
||||
<div class="stock-info">
|
||||
{% if item.product.stock > 10 %}
|
||||
<span class="in-stock">✓ In Stock</span>
|
||||
{% elif item.product.stock > 0 %}
|
||||
<span class="low-stock">⚠ Low Stock ({{ item.product.stock }} left)</span>
|
||||
{% else %}
|
||||
<span class="out-of-stock">✗ Out of Stock</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="price">${{ "%.2f"|format(item.product.price) }}</td>
|
||||
<td class="quantity">
|
||||
<form method="POST" action="{{ url_for('cart.update_cart', item_id=item.id) }}" class="quantity-form">
|
||||
<input type="number" name="quantity" value="{{ item.quantity }}" min="1" max="{{ item.max_quantity }}"
|
||||
class="quantity-input">
|
||||
<button type="submit" class="btn-update">Update</button>
|
||||
<div class="max-quantity">
|
||||
Max: {{ item.max_quantity }}
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
<td class="subtotal">${{ "%.2f"|format(item.subtotal) }}</td>
|
||||
<td class="actions">
|
||||
<form method="POST" action="{{ url_for('cart.remove_from_cart', item_id=item.id) }}"
|
||||
onsubmit="return confirm('Remove {{ item.product.name }} from cart?')">
|
||||
<button type="submit" class="btn-remove">
|
||||
Remove
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="cart-summary">
|
||||
<div class="summary-card">
|
||||
<h3>Order Summary</h3>
|
||||
<div class="summary-row">
|
||||
<span>Subtotal:</span>
|
||||
<span>${{ "%.2f"|format(total) }}</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span>Shipping:</span>
|
||||
<span>Free</span>
|
||||
</div>
|
||||
<div class="summary-row total">
|
||||
<span>Total:</span>
|
||||
<span>${{ "%.2f"|format(total) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="cart-actions">
|
||||
<a href="{{ url_for('main.products') }}" class="btn-continue">
|
||||
← Continue Shopping
|
||||
</a>
|
||||
|
||||
<form method="POST" action="{{ url_for('cart.clear_cart') }}" style="display: inline;">
|
||||
<button type="submit" class="btn-clear" onclick="return confirm('Clear entire cart?')">
|
||||
Clear Cart
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<a href="{{ url_for('cart.checkout') }}" class="btn-checkout">
|
||||
Proceed to Checkout →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-cart">
|
||||
<div class="empty-icon">🛒</div>
|
||||
<h3>Your cart is empty</h3>
|
||||
<p>Add some products to your cart and they will appear here.</p>
|
||||
<a href="{{ url_for('main.products') }}" class="btn-shop">
|
||||
Browse Products
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
71
templates/checkout.html
Normal file
71
templates/checkout.html
Normal file
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/checkout.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="checkout-container">
|
||||
<h1>Checkout</h1>
|
||||
|
||||
<div class="checkout-content">
|
||||
<div class="order-summary">
|
||||
<h3>Order Summary</h3>
|
||||
{% for item in cart.items %}
|
||||
<div class="order-item">
|
||||
<div class="item-info">
|
||||
<h4>{{ item.product.name }}</h4>
|
||||
<p>Quantity: {{ item.quantity }} × ${{ "%.2f"|format(item.product.price) }}</p>
|
||||
</div>
|
||||
<div class="item-total">
|
||||
${{ "%.2f"|format(item.product.price * item.quantity) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="order-total">
|
||||
<h3>Total: ${{ "%.2f"|format(total) }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkout-form">
|
||||
<h3>Shipping Information</h3>
|
||||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>Full Name:</label>
|
||||
<input type="text" name="name" required class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Email:</label>
|
||||
<input type="email" name="email" value="{{ current_user.email }}" required class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Address:</label>
|
||||
<textarea name="address" required class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>City:</label>
|
||||
<input type="text" name="city" required class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Postal Code:</label>
|
||||
<input type="text" name="postal_code" required class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{{ url_for('cart.view_cart') }}" class="btn-back">
|
||||
← Back to Cart
|
||||
</a>
|
||||
<button type="submit" class="btn-confirm">
|
||||
Place Order
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
229
templates/home.html
Normal file
229
templates/home.html
Normal file
@@ -0,0 +1,229 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/home.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/home.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<section class="hero-section">
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">Welcome to WearWell Shop</h1>
|
||||
<p class="hero-subtitle">Discover premium fashion for every occasion. Quality clothing, unbeatable prices, and style
|
||||
that lasts.</p>
|
||||
|
||||
<div class="hero-search">
|
||||
<form action="{{ url_for('main.products') }}" method="GET">
|
||||
<input type="text" name="q" placeholder="Search for products, brands, or categories..."
|
||||
class="hero-search-input">
|
||||
<button type="submit" class="hero-search-button">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="categories-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Shop by Category</h2>
|
||||
<a href="{{ url_for('main.products') }}" class="view-all-link">
|
||||
View All Categories →
|
||||
</a>
|
||||
</div>
|
||||
<div class="categories-grid">
|
||||
{% for category in categories[:6] %}
|
||||
<a href="{{ url_for('main.products', category=category.id) }}" class="category-card">
|
||||
<div class="category-image">
|
||||
{% if category.name == "T-Shirts" %}👕
|
||||
{% elif category.name == "Jeans" %}👖
|
||||
{% elif category.name == "Jackets" %}🧥
|
||||
{% elif category.name == "Shoes" %}👟
|
||||
{% elif category.name == "Accessories" %}👜
|
||||
{% elif category.name == "Dresses" %}👗
|
||||
{% elif category.name == "Shirts" %}👔
|
||||
{% elif category.name == "Hoodies" %}🧢
|
||||
{% else %}👚{% endif %}
|
||||
</div>
|
||||
<div class="category-name">
|
||||
{{ category.name }}
|
||||
<span class="category-count">{{ category.products|length }} items</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="featured-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Featured Products</h2>
|
||||
<a href="{{ url_for('main.products') }}" class="view-all-link">
|
||||
View All Products →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="products-grid">
|
||||
{% for product in featured_products[:8] %}
|
||||
<div class="product-card-home">
|
||||
{% if product.stock <= 5 and product.stock> 0 %}
|
||||
<span class="product-badge">Low Stock</span>
|
||||
{% elif product.stock == 0 %}
|
||||
<span class="product-badge" style="background: #666;">Out of Stock</span>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('main.product_detail', id=product.id) }}">
|
||||
<div class="product-image-home">
|
||||
<img src="{{ product.get_image_url() }}" alt="{{ product.name }}">
|
||||
</div>
|
||||
|
||||
<div class="product-info-home">
|
||||
<h3 class="product-title-home">{{ product.name }}</h3>
|
||||
|
||||
<div class="product-price-home">${{ "%.2f"|format(product.price) }}</div>
|
||||
|
||||
{% if product.average_rating > 0 %}
|
||||
<div class="product-rating-home">
|
||||
<div class="stars">
|
||||
{% for i in range(5) %}
|
||||
<span>{% if i < product.average_rating|int %}★{% else %}☆{% endif %}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="rating-count">({{ product.review_count }})</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="product-meta-home">
|
||||
{% if product.company %}
|
||||
<span>{{ product.company }}</span>
|
||||
{% endif %}
|
||||
{% if product.color %}
|
||||
<span>{{ product.color }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="promo-banner">
|
||||
<div class="promo-content">
|
||||
<h2>Summer Sale is Here! 🌞</h2>
|
||||
<p>Get up to 50% off on selected items. Limited time offer!</p>
|
||||
<a href="{{ url_for('main.products') }}" class="promo-button">
|
||||
Shop Now →
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="benefits-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Why Shop With Us?</h2>
|
||||
</div>
|
||||
|
||||
<div class="benefits-grid">
|
||||
<div class="benefit-card">
|
||||
<span class="benefit-icon">🚚</span>
|
||||
<h3 class="benefit-title">Free Shipping</h3>
|
||||
<p class="benefit-description">Free delivery on all orders over $50. Fast and reliable shipping nationwide.</p>
|
||||
</div>
|
||||
|
||||
<div class="benefit-card">
|
||||
<span class="benefit-icon">🔄</span>
|
||||
<h3 class="benefit-title">Easy Returns</h3>
|
||||
<p class="benefit-description">30-day return policy. If you're not satisfied, we'll make it right.</p>
|
||||
</div>
|
||||
|
||||
<div class="benefit-card">
|
||||
<span class="benefit-icon">🔒</span>
|
||||
<h3 class="benefit-title">Secure Payment</h3>
|
||||
<p class="benefit-description">Your payment information is protected with bank-level security.</p>
|
||||
</div>
|
||||
|
||||
<div class="benefit-card">
|
||||
<span class="benefit-icon">⭐</span>
|
||||
<h3 class="benefit-title">Quality Guarantee</h3>
|
||||
<p class="benefit-description">Premium materials and craftsmanship in every product we sell.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="featured-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Best Sellers</h2>
|
||||
<a href="{{ url_for('main.products') }}" class="view-all-link">
|
||||
View More →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="products-grid">
|
||||
{% for product in best_sellers[:4] %}
|
||||
<div class="product-card-home">
|
||||
{% if product.stock <= 5 and product.stock> 0 %}
|
||||
<span class="product-badge">Popular</span>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('main.product_detail', id=product.id) }}">
|
||||
<div class="product-image-home">
|
||||
<img src="{{ product.get_image_url() }}" alt="{{ product.name }}">
|
||||
</div>
|
||||
|
||||
<div class="product-info-home">
|
||||
<h3 class="product-title-home">{{ product.name }}</h3>
|
||||
|
||||
<div class="product-price-home">${{ "%.2f"|format(product.price) }}</div>
|
||||
|
||||
{% if product.average_rating > 0 %}
|
||||
<div class="product-rating-home">
|
||||
<div class="stars">
|
||||
{% for i in range(5) %}
|
||||
<span>{% if i < product.average_rating|int %}★{% else %}☆{% endif %}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="rating-count">({{ product.review_count }})</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="product-meta-home">
|
||||
<span>{{ product.stock }} in stock</span>
|
||||
<span>{{ product.like_count }} likes</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="newsletter-section">
|
||||
<div class="newsletter-content">
|
||||
<h2>Stay in the Loop</h2>
|
||||
<p>Subscribe to our newsletter for exclusive deals, new arrivals, and style tips.</p>
|
||||
|
||||
<form class="newsletter-form">
|
||||
<input type="email" placeholder="Enter your email address" class="newsletter-input" required>
|
||||
<button type="submit" class="newsletter-button">Subscribe</button>
|
||||
</form>
|
||||
|
||||
<p style="margin-top: 15px; font-size: 0.9em; opacity: 0.8;">
|
||||
By subscribing, you agree to our Privacy Policy and consent to receive updates.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
91
templates/login.html
Normal file
91
templates/login.html
Normal file
@@ -0,0 +1,91 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<h2>Welcome Back</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flash-messages">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<div class="password-container">
|
||||
<input type="password" id="password" name="password" class="form-control" placeholder="Enter your password"
|
||||
required>
|
||||
<button type="button" class="toggle-password" onclick="togglePassword('password')">
|
||||
👁️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="remember-me">
|
||||
<input type="checkbox" id="remember" name="remember">
|
||||
<label for="remember">Remember me</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit">
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-links">
|
||||
<a href="{{ url_for('auth.register') }}" class="auth-link">
|
||||
Don't have an account? Register here
|
||||
</a>
|
||||
<a href="#" class="auth-link">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="social-login">
|
||||
<div class="social-divider">
|
||||
<span>Or continue with</span>
|
||||
</div>
|
||||
|
||||
<div class="social-buttons">
|
||||
<button type="button" class="btn-social google">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
||||
<path fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
||||
<path fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
||||
<path fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
||||
</svg>
|
||||
Google
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn-social facebook">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
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>
|
||||
Facebook
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
93
templates/order_details.html
Normal file
93
templates/order_details.html
Normal file
@@ -0,0 +1,93 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/order_details.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="order-details-container">
|
||||
<h2>Order #{{ order.id }}</h2>
|
||||
|
||||
<div class="order-meta">
|
||||
<p><strong>Order Date:</strong> {{ order.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
|
||||
<div class="order-status">
|
||||
<span class="status-label">Status:</span>
|
||||
<span class="status-badge status-{{ order.status }}">
|
||||
{{ order.status|title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="items-section">
|
||||
<h3>Order Items</h3>
|
||||
|
||||
{% if order.items %}
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Quantity</th>
|
||||
<th>Price</th>
|
||||
<th>Subtotal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in order.items %}
|
||||
<tr>
|
||||
<td class="product-cell">
|
||||
<div class="product-image">
|
||||
<img src="{{ item.product.get_image_url() }}" alt="{{ item.product.name }}">
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<div class="product-name">{{ item.product.name }}</div>
|
||||
{% if item.selected_size or item.selected_color %}
|
||||
<div class="product-variants">
|
||||
{% if item.selected_size %}Size: {{ item.selected_size }}{% endif %}
|
||||
{% if item.selected_color %} | Color: {{ item.selected_color }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="quantity-cell">{{ item.quantity }}</td>
|
||||
<td class="price-cell">${{ "%.2f"|format(item.product.price) }}</td>
|
||||
<td class="subtotal-cell">${{ "%.2f"|format(item.product.price * item.quantity) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="order-summary">
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Subtotal:</span>
|
||||
<span class="summary-value">
|
||||
${{ "%.2f"|format(order.items|sum(attribute='product.price') * order.items|sum(attribute='quantity')) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Shipping:</span>
|
||||
<span class="summary-value">Free</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Total:</span>
|
||||
<span class="summary-value">
|
||||
${{ "%.2f"|format(order.items|sum(attribute='product.price') * order.items|sum(attribute='quantity')) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="no-items">
|
||||
<p>No items in this order.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="order-actions">
|
||||
<a href="{{ url_for('user.profile') }}" class="btn-back">
|
||||
← Back to My Orders
|
||||
</a>
|
||||
<button onclick="window.print()" class="btn-print">
|
||||
📄 Print Order
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
178
templates/product_detail.html
Normal file
178
templates/product_detail.html
Normal file
@@ -0,0 +1,178 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/product_detail.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/product_detail.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="product-detail-container">
|
||||
|
||||
<div class="product-detail">
|
||||
|
||||
<div class="product-gallery">
|
||||
|
||||
<div class="main-image-container">
|
||||
<img id="mainImage" src="{{ product.get_image_url() }}" alt="{{ product.name }}" class="main-image">
|
||||
</div>
|
||||
|
||||
{% if product.images|length > 0 %}
|
||||
<div class="thumbnail-gallery">
|
||||
{% for image in product.images|sort(attribute='display_order') %}
|
||||
<div class="thumbnail-container {% if loop.first %}active{% endif %}"
|
||||
data-image-url="{{ url_for('static', filename='uploads/products/' + image.filename) }}"
|
||||
onclick="changeImage(this)">
|
||||
<img src="{{ url_for('static', filename='uploads/products/' + image.filename) }}"
|
||||
alt="{{ product.name }} - Image {{ loop.index }}" class="thumbnail">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="product-info">
|
||||
<h1>{{ product.name }}</h1>
|
||||
|
||||
<div class="stock-status">
|
||||
{% if product.stock > 10 %}
|
||||
<span class="in-stock">✓ In Stock ({{ product.stock }} available)</span>
|
||||
{% elif product.stock > 0 %}
|
||||
<span class="low-stock">⚠ Low Stock (Only {{ product.stock }} left!)</span>
|
||||
{% else %}
|
||||
<span class="out-of-stock">✗ Out of Stock</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if product.average_rating > 0 %}
|
||||
<div class="product-rating">
|
||||
<div class="stars">
|
||||
{% for i in range(5) %}
|
||||
<span class="star {% if i < product.average_rating|int %}filled{% endif %}">★</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="rating-value">{{ product.average_rating }}/5</span>
|
||||
<span class="review-count">({{ product.review_count }} reviews)</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="price-section">
|
||||
<span class="price">${{ "%.2f"|format(product.price) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="product-meta">
|
||||
{% if product.sku %}
|
||||
<div class="meta-item">
|
||||
<strong>SKU:</strong> {{ product.sku }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if product.company %}
|
||||
<div class="meta-item">
|
||||
<strong>Brand:</strong> {{ product.company }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if product.color %}
|
||||
<div class="meta-item">
|
||||
<strong>Color:</strong> {{ product.color }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if product.size %}
|
||||
<div class="meta-item">
|
||||
<strong>Size:</strong> {{ product.size }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if product.category %}
|
||||
<div class="meta-item">
|
||||
<strong>Category:</strong> {{ product.category.name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="description-section">
|
||||
<h3>Description</h3>
|
||||
<p>{{ product.description or 'No description available.' }}</p>
|
||||
</div>
|
||||
|
||||
{% if product.material or product.weight or product.dimensions %}
|
||||
<div class="specifications">
|
||||
<h3>Specifications</h3>
|
||||
<div class="spec-grid">
|
||||
{% if product.material %}
|
||||
<div class="spec-item">
|
||||
<span class="spec-label">Material:</span>
|
||||
<span class="spec-value">{{ product.material }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if product.weight %}
|
||||
<div class="spec-item">
|
||||
<span class="spec-label">Weight:</span>
|
||||
<span class="spec-value">{{ product.weight }} kg</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if product.dimensions %}
|
||||
<div class="spec-item">
|
||||
<span class="spec-label">Dimensions:</span>
|
||||
<span class="spec-value">{{ product.dimensions }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="actions-section">
|
||||
{% if current_user.is_authenticated and product.stock > 0 %}
|
||||
<a href="{{ url_for('cart.add_to_cart', product_id=product.id) }}" class="btn btn-primary">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="9" cy="21" r="1"></circle>
|
||||
<circle cx="20" cy="21" r="1"></circle>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||||
</svg>
|
||||
Add to Cart
|
||||
</a>
|
||||
{% elif current_user.is_authenticated %}
|
||||
<button class="btn btn-secondary" disabled>
|
||||
Out of Stock
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login') }}" class="btn btn-primary">
|
||||
Login to Purchase
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('main.products') }}" class="btn btn-outline">
|
||||
← Back to Products
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if related_products %}
|
||||
<div class="related-products">
|
||||
<h2>Related Products</h2>
|
||||
<div class="related-grid">
|
||||
{% for related in related_products %}
|
||||
<div class="product-card">
|
||||
<a href="{{ url_for('main.product_detail', id=related.id) }}" class="product-link">
|
||||
<div class="product-image">
|
||||
<img src="{{ related.get_image_url() }}" alt="{{ related.name }}">
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<h3 class="product-name">{{ related.name }}</h3>
|
||||
<div class="product-price">${{ "%.2f"|format(related.price) }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
255
templates/products.html
Normal file
255
templates/products.html
Normal file
@@ -0,0 +1,255 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/products.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/products.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="products-page">
|
||||
<aside class="filters-sidebar">
|
||||
<div class="filters-header">
|
||||
<h3>Filters</h3>
|
||||
{% if search_query or selected_category or selected_size or selected_color or selected_type or selected_material
|
||||
or selected_company or min_price or max_price or in_stock_only %}
|
||||
<a href="{{ url_for('main.products') }}" class="clear-filters">Clear All</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="GET" action="{{ url_for('main.products') }}" class="filters-form">
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="search">Search</label>
|
||||
<input type="text" id="search" name="q" value="{{ search_query }}" placeholder="Search products..."
|
||||
class="filter-input">
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="category">Category</label>
|
||||
<select id="category" name="category" class="filter-select">
|
||||
<option value="">All Categories</option>
|
||||
{% for c in categories %}
|
||||
<option value="{{ c.id }}" {% if selected_category==c.id|string %}selected{% endif %}>
|
||||
{{ c.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="size">Size</label>
|
||||
<select id="size" name="size" class="filter-select">
|
||||
<option value="">All Sizes</option>
|
||||
{% for size in sizes %}
|
||||
<option value="{{ size }}" {% if selected_size==size %}selected{% endif %}>
|
||||
{{ size }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="color">Color</label>
|
||||
<select id="color" name="color" class="filter-select">
|
||||
<option value="">All Colors</option>
|
||||
{% for color in colors %}
|
||||
<option value="{{ color }}" {% if selected_color==color %}selected{% endif %}>
|
||||
{{ color }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="material">Material</label>
|
||||
<select id="material" name="material" class="filter-select">
|
||||
<option value="">All Materials</option>
|
||||
{% for material in materials %}
|
||||
<option value="{{ material }}" {% if selected_material==material %}selected{% endif %}>
|
||||
{{ material }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="company">Brand</label>
|
||||
<select id="company" name="company" class="filter-select">
|
||||
<option value="">All Brands</option>
|
||||
{% for company in companies %}
|
||||
<option value="{{ company }}" {% if selected_company==company %}selected{% endif %}>
|
||||
{{ company }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Price Range ($)</label>
|
||||
<div class="price-range">
|
||||
<input type="number" name="min_price" value="{{ min_price }}" placeholder="Min" class="price-input" min="0"
|
||||
step="0.01">
|
||||
<span class="price-separator">-</span>
|
||||
<input type="number" name="max_price" value="{{ max_price }}" placeholder="Max" class="price-input" min="0"
|
||||
step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" name="in_stock" value="1" {% if in_stock_only %}checked{% endif %}>
|
||||
In Stock Only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-apply-filters">Apply Filters</button>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<main class="products-main">
|
||||
<div class="products-header">
|
||||
<div class="results-info">
|
||||
<h1>Clothing</h1>
|
||||
<p class="results-count">{{ products|length }} products found</p>
|
||||
{% if search_query or selected_category or selected_size or selected_color or selected_type or selected_material
|
||||
or selected_company or min_price or max_price or in_stock_only %}
|
||||
<div class="active-filters">
|
||||
{% if search_query %}
|
||||
<span class="filter-tag">Search: "{{ search_query }}"
|
||||
<a
|
||||
href="{{ url_for('main.products', q='', category=selected_category, size=selected_size, color=selected_color, type=selected_type, material=selected_material, company=selected_company, min_price=min_price, max_price=max_price, in_stock=in_stock_only, sort=sort_by) }}">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if selected_category_name %}
|
||||
<span class="filter-tag">Category: {{ selected_category_name }}
|
||||
<a
|
||||
href="{{ url_for('main.products', q=search_query, category='', size=selected_size, color=selected_color, type=selected_type, material=selected_material, company=selected_company, min_price=min_price, max_price=max_price, in_stock=in_stock_only, sort=sort_by) }}">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if selected_size %}
|
||||
<span class="filter-tag">Size: {{ selected_size }}
|
||||
<a
|
||||
href="{{ url_for('main.products', q=search_query, category=selected_category, size='', color=selected_color, type=selected_type, material=selected_material, company=selected_company, min_price=min_price, max_price=max_price, in_stock=in_stock_only, sort=sort_by) }}">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if selected_color %}
|
||||
<span class="filter-tag">Color: {{ selected_color }}
|
||||
<a
|
||||
href="{{ url_for('main.products', q=search_query, category=selected_category, size=selected_size, color='', type=selected_type, material=selected_material, company=selected_company, min_price=min_price, max_price=max_price, in_stock=in_stock_only, sort=sort_by) }}">×</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="sort-controls">
|
||||
<label for="sort">Sort by:</label>
|
||||
<select id="sort" name="sort" class="sort-select" onchange="this.form.submit()">
|
||||
<option value="newest" {% if sort_by=='newest' %}selected{% endif %}>Newest</option>
|
||||
<option value="price_low" {% if sort_by=='price_low' %}selected{% endif %}>Price: Low to High</option>
|
||||
<option value="price_high" {% if sort_by=='price_high' %}selected{% endif %}>Price: High to Low</option>
|
||||
<option value="name" {% if sort_by=='name' %}selected{% endif %}>Name: A-Z</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if products %}
|
||||
<div class="products-grid">
|
||||
{% for product in products %}
|
||||
<div class="product-card">
|
||||
<a href="{{ url_for('main.product_detail', id=product.id) }}" class="product-link">
|
||||
<div class="product-image">
|
||||
<img src="{{ product.get_image_url() }}" alt="{{ product.name }}">
|
||||
{% if product.stock <= 0 %} <div class="out-of-stock">Out of Stock
|
||||
</div>
|
||||
{% elif product.stock <= 5 %} <div class="low-stock">Only {{ product.stock }} left
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if product.average_rating > 0 %}
|
||||
<div class="product-rating">
|
||||
<span class="rating-stars">★★★★★</span>
|
||||
<span class="rating-value">{{ product.average_rating }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="product-info">
|
||||
<h3 class="product-name">{{ product.name }}</h3>
|
||||
|
||||
<div class="product-meta">
|
||||
{% if product.color %}
|
||||
<span class="meta-item">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
</svg>
|
||||
{{ product.color }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if product.size %}
|
||||
<span class="meta-item">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M20 7h-9"></path>
|
||||
<path d="M14 17H5"></path>
|
||||
<circle cx="17" cy="17" r="3"></circle>
|
||||
<circle cx="7" cy="7" r="3"></circle>
|
||||
</svg>
|
||||
{{ product.size }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if product.company %}
|
||||
<span class="meta-item">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
||||
</svg>
|
||||
{{ product.company }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="product-footer">
|
||||
<div class="product-price">${{ "%.2f"|format(product.price) }}</div>
|
||||
|
||||
<div class="product-actions">
|
||||
{% if current_user.is_authenticated and product.stock > 0 %}
|
||||
<a href="{{ url_for('cart.add_to_cart', product_id=product.id) }}" class="btn-add-to-cart"
|
||||
onclick="event.stopPropagation();">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle cx="9" cy="21" r="1"></circle>
|
||||
<circle cx="20" cy="21" r="1"></circle>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||||
</svg>
|
||||
Add to Cart
|
||||
</a>
|
||||
{% elif current_user.is_authenticated %}
|
||||
<button class="btn-out-of-stock" disabled>
|
||||
Out of Stock
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login') }}" class="btn-login-to-buy" onclick="event.stopPropagation();">
|
||||
Login to Buy
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="no-products">
|
||||
<div class="no-products-icon">👕</div>
|
||||
<h3>No products found</h3>
|
||||
<p>Try adjusting your filters or search criteria</p>
|
||||
<a href="{{ url_for('main.products') }}" class="btn-view-all">
|
||||
View All Products
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
</div>
|
||||
{% endblock %}
|
||||
115
templates/register.html
Normal file
115
templates/register.html
Normal file
@@ -0,0 +1,115 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/register.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<h2>Create Account</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flash-messages">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<div class="password-container">
|
||||
<input type="password" id="password" name="password" class="form-control" placeholder="Create a strong password"
|
||||
required oninput="checkPasswordStrength(this.value)">
|
||||
<button type="button" class="toggle-password" onclick="togglePassword('password')">
|
||||
👁️
|
||||
</button>
|
||||
</div>
|
||||
<div class="password-strength">
|
||||
<div class="strength-bar" id="strengthBar"></div>
|
||||
</div>
|
||||
<div class="password-hints" id="passwordHints">
|
||||
<strong>Password should contain:</strong>
|
||||
<ul>
|
||||
<li id="lengthHint">At least 8 characters</li>
|
||||
<li id="uppercaseHint">One uppercase letter</li>
|
||||
<li id="lowercaseHint">One lowercase letter</li>
|
||||
<li id="numberHint">One number</li>
|
||||
<li id="specialHint">One special character</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<div class="password-container">
|
||||
<input type="password" id="confirm_password" name="confirm_password" class="form-control"
|
||||
placeholder="Confirm your password" required oninput="checkPasswordMatch()">
|
||||
<button type="button" class="toggle-password" onclick="togglePassword('confirm_password')">
|
||||
👁️
|
||||
</button>
|
||||
</div>
|
||||
<div id="passwordMatch" style="margin-top: 5px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="terms">
|
||||
<input type="checkbox" id="terms" name="terms" required>
|
||||
<label for="terms">
|
||||
I agree to the <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-submit">
|
||||
Create Account
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-links">
|
||||
<a href="{{ url_for('auth.login') }}" class="auth-link">
|
||||
Already have an account? Sign in
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="social-login">
|
||||
<div class="social-divider">
|
||||
<span>Or sign up with</span>
|
||||
</div>
|
||||
|
||||
<div class="social-buttons">
|
||||
<button type="button" class="btn-social google">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
||||
<path fill="currentColor"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
||||
<path fill="currentColor"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
||||
<path fill="currentColor"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
||||
</svg>
|
||||
Google
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn-social facebook">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path fill="currentColor"
|
||||
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>
|
||||
Facebook
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
63
templates/user_profile.html
Normal file
63
templates/user_profile.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/profile.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="profile-container">
|
||||
<h2>My Profile</h2>
|
||||
|
||||
<div class="profile-info">
|
||||
<p><strong>Email:</strong> {{ current_user.email }}</p>
|
||||
</div>
|
||||
|
||||
<div class="orders-section">
|
||||
<h3>My Orders</h3>
|
||||
|
||||
{% if orders %}
|
||||
<table class="orders-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
<th>Date</th>
|
||||
<th>Status</th>
|
||||
<th>Items</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td class="order-id">#{{ order.id }}</td>
|
||||
<td class="order-date">{{ order.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
<span class="status-badge status-{{ order.status }}">
|
||||
{{ order.status|title }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="order-items">{{ order.items|length }}</td>
|
||||
<td class="order-actions">
|
||||
<a href="{{ url_for('user.order_details', order_id=order.id) }}" class="view-details-link">
|
||||
View Details
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="no-orders">
|
||||
<p>You haven't placed any orders yet.</p>
|
||||
<a href="{{ url_for('main.products') }}" class="shop-now-link">
|
||||
Start Shopping
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('main.home') }}" class="back-link">
|
||||
← Back to Home
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user