Added Repository

This commit is contained in:
2026-01-30 14:02:35 +03:30
parent 8917e625a5
commit dbc8f70b4a
53 changed files with 7758 additions and 2 deletions

19
.editorconfig Normal file
View 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
View 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
View 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
View File

@@ -1,3 +1,228 @@
# wearwell
# 🛍️ Wearwell - Modern E-commerce Clothing Store
WearWell Clothing Website
<div align="center">
![Python](https://img.shields.io/badge/Python-3.12+-yellow.svg)
![Flask](https://img.shields.io/badge/Flask-3.1+-blue.svg)
![MariaDB](https://img.shields.io/badge/MariaDB-10+-orange.svg)
**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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

8
extensions.py Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

196
models.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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%;
}
}

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

View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

88
static/js/add_product.js Normal file
View 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
View 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
View 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
View 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;
});
});

View 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
View File

@@ -0,0 +1,3 @@
document.getElementById('sort').addEventListener('change', function () {
this.closest('form').submit();
});

159
static/js/register.js Normal file
View 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;
});
});

View 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 %}

View 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 %}

View 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 %}

View 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
View 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>&copy; 2025 WearWell Shop. All rights reserved.</p>
</div>
</footer>
{% block scripts %}{% endblock %}
</body>
</html>

114
templates/cart.html Normal file
View 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
View 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
View 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
View 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 %}

View 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 %}

View 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
View 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
View 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 %}

View 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 %}