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

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
**WearWell - a simple flask shopping web app for clothes**
|
||||||
|
|
||||||
|
<img src="http://g.broombox.org/amanfromspace/wearwell/raw/branch/main/homepage.png" width="100%" height="500px">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 📋 Table of Contents
|
||||||
|
- [✨ Features](#-features)
|
||||||
|
- [📁 Project Structure](#-project-structure)
|
||||||
|
- [🌐 Application Routes](#-application-routes)
|
||||||
|
- [🚀 Setup](#-setup)
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### 🛒 Core E-commerce Features
|
||||||
|
- **User Management**: Registration, authentication, profiles, order history
|
||||||
|
- **Product Catalog**: Categories, filters, search, product variations
|
||||||
|
- **Shopping Cart**: Session-based cart, cart persistence, quantity management
|
||||||
|
- **Checkout Process**: Multi-step checkout, shipping options, payment integration
|
||||||
|
- **Order Management**: Order tracking, status updates, cancellation/returns
|
||||||
|
- **Inventory Management**: Stock tracking, low stock alerts, backorders
|
||||||
|
- **Reviews & Ratings**: Product reviews, star ratings, photo reviews
|
||||||
|
|
||||||
|
### 🎨 User Experience
|
||||||
|
- **Responsive Design**: Mobile-first, desktop-optimized interface
|
||||||
|
- **Image Gallery**: High-quality product images
|
||||||
|
- **Size Guides**: Interactive sizing charts, fit recommendations
|
||||||
|
|
||||||
|
### 🔧 Admin Features
|
||||||
|
- **Dashboard**: inventory overview
|
||||||
|
|
||||||
|
### 🛠️ Technical Features
|
||||||
|
- **Search Engine**: fast product search
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
### Tree Structure of WearWell Project
|
||||||
|
|
||||||
|
```
|
||||||
|
wearwell/
|
||||||
|
├── .env.example # Example environment of project (Database configures, etc)
|
||||||
|
├── .editorconfig # Configurations of editor for formatting codes
|
||||||
|
├── app.py # Main Flask application entry point - initializes app, configures extensions, registers blueprints
|
||||||
|
├── extensions.py # Centralized Flask extensions initialization (SQLAlchemy, Migrate, LoginManager, etc)
|
||||||
|
├── LICENSE # Project license file (GPL)
|
||||||
|
├── models.py # Database models definition (SQLAlchemy ORM - User, Product, Order, Cart, etc)
|
||||||
|
├── README.md # Project documentation, setup instructions, features overview
|
||||||
|
├── requirements.txt # Python dependencies list for pip installation
|
||||||
|
├── seed_db.py # Database seeding script - populates database with sample/test data
|
||||||
|
├── routes/ # Flask route blueprints - modular URL routing by feature
|
||||||
|
│ ├── admin.py # Admin panel routes - product management, user management, analytics
|
||||||
|
│ ├── auth.py # Authentication routes - login, logout, registration, password reset
|
||||||
|
│ ├── cart.py # Shopping cart operations - add/remove items, update quantities, view cart
|
||||||
|
│ ├── __init__.py # Routes package initialization - exports blueprints
|
||||||
|
│ ├── main.py # Public/main routes - homepage, product listings, search functionality
|
||||||
|
│ └── user.py # User profile routes - profile management, order history, settings
|
||||||
|
├── static/ # Static assets served directly by web server
|
||||||
|
│ ├── css/ # Stylesheets for each page/component
|
||||||
|
│ │ ├── add_product.css # Admin product addition form styling
|
||||||
|
│ │ ├── auth.css # Login/registration form styling
|
||||||
|
│ │ ├── cart.css # Shopping cart page styling
|
||||||
|
│ │ ├── categories.css # Category listing/browsing styling
|
||||||
|
│ │ ├── checkout.css # Checkout process styling
|
||||||
|
│ │ ├── dashboard.css # Admin dashboard styling
|
||||||
|
│ │ ├── edit_product.css # Admin product editing form styling
|
||||||
|
│ │ ├── home.css # Homepage/landing page styling
|
||||||
|
│ │ ├── order_details.css # Order summary and details page styling
|
||||||
|
│ │ ├── product_detail.css # Individual product page styling
|
||||||
|
│ │ ├── products.css # Product grid/listings styling
|
||||||
|
│ │ ├── profile.css # User profile page styling
|
||||||
|
│ │ └── styles.css # Global/base styles
|
||||||
|
│ ├── favicon.ico # Browser tab/address bar icon
|
||||||
|
│ ├── images/ # Image assets (logos, backgrounds, etc)
|
||||||
|
│ │ └── default-product.jpg # Fallback product image when none uploaded
|
||||||
|
│ └── js/ # Client-side JavaScript files
|
||||||
|
│ ├── add_product.js # Product form validation, image upload preview
|
||||||
|
│ ├── edit_product.js # Product editing form interactions
|
||||||
|
│ ├── home.js # Homepage animations, featured product carousel
|
||||||
|
│ ├── login.js # Login form validation, password visibility toggle
|
||||||
|
│ ├── product_detail.js # Product image gallery, quantity selector, add to cart
|
||||||
|
│ ├── products.js # Product filtering, sorting, pagination
|
||||||
|
│ └── register.js # Registration form validation, password strength
|
||||||
|
└── templates/ # Jinja2 HTML templates
|
||||||
|
├── admin/ # Admin panel templates
|
||||||
|
│ ├── add_product.html # Form for adding new products
|
||||||
|
│ ├── categories.html # Category management interface
|
||||||
|
│ ├── dashboard.html # Admin overview with statistics like CRUD table
|
||||||
|
│ └── edit_product.html # Form for editing existing products
|
||||||
|
├── base.html # Base template with common layout (header, footer, nav)
|
||||||
|
├── cart.html # Shopping cart display and management
|
||||||
|
├── checkout.html # Checkout process steps and payment form
|
||||||
|
├── home.html # Homepage/landing page
|
||||||
|
├── login.html # User login form
|
||||||
|
├── order_details.html # Detailed order confirmation and summary
|
||||||
|
├── product_detail.html # Individual product display page
|
||||||
|
├── products.html # Product grid/listings with filters
|
||||||
|
├── register.html # User registration form
|
||||||
|
└── user_profile.html # User profile and order history
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Structure
|
||||||
|
|
||||||
|
<img src="http://g.broombox.org/amanfromspace/wearwell/raw/branch/main/diagram_wearwell_db.png" width="100%" height="768px">
|
||||||
|
|
||||||
|
## 🌐 Application Routes
|
||||||
|
|
||||||
|
### Main Routes
|
||||||
|
|
||||||
|
| Route | Template | Description |
|
||||||
|
|-------|----------|--------------|
|
||||||
|
| `/` | `home.html` | Homepage with featured products and promotions |
|
||||||
|
| `/products` | `products.html` | Product catalog with filtering and sorting |
|
||||||
|
| `/login` | `login.html` | User authentication form |
|
||||||
|
| `/register` | `register.html` | New user registration form |
|
||||||
|
| `/cart` | `cart.html` | Shopping cart with items and quantities |
|
||||||
|
| `/profile` | `user_profile.html` | User profile management and personal information |
|
||||||
|
| `/checkout` | `checkout.html` | Order checkout process with payment and shipping |
|
||||||
|
| `/admin` | `admin/dashboard.html` | Admin dashboard with statistics and overview |
|
||||||
|
|
||||||
|
## 🚀 Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- **Python 3.12+** (also pip for installing requirements)
|
||||||
|
- **MariaDB 10+ (MySQL)** (Linux)
|
||||||
|
- **XAMPP 3.3.0+ (MySQL)** (Windows)
|
||||||
|
|
||||||
|
**Linux debian-based distros** (this apt command is only for practice it may be different)
|
||||||
|
```
|
||||||
|
sudo apt install python3.12 python3.12-venv mariadb-server mariadb-client
|
||||||
|
```
|
||||||
|
|
||||||
|
After installations of prerequisites depending on your OS follow the instructions below to run the web app
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
First clone the project
|
||||||
|
```
|
||||||
|
git clone http://g.broombox.org/amanfromspace/wearwell.git
|
||||||
|
```
|
||||||
|
then go to the folder by using cd command
|
||||||
|
```
|
||||||
|
cd ./wearwell
|
||||||
|
```
|
||||||
|
now make virtual environment with python in your project (development mode not for production)
|
||||||
|
```
|
||||||
|
python3 -m venv venv
|
||||||
|
```
|
||||||
|
activate venv (linux)
|
||||||
|
```
|
||||||
|
source ./venv/bin/activate
|
||||||
|
```
|
||||||
|
activate venv (windows)
|
||||||
|
```
|
||||||
|
source venv/scripts/activate
|
||||||
|
```
|
||||||
|
you are ready to install requirements
|
||||||
|
```
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
it is time for your database to be ready - enter to mariadb and do the instructions
|
||||||
|
```
|
||||||
|
CREATE DATABASE wearwell;
|
||||||
|
```
|
||||||
|
better to use this specially with **utf8mb4_unicode_ci**
|
||||||
|
```
|
||||||
|
CREATE DATABASE IF NOT EXISTS wearwell
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
create a user with password
|
||||||
|
```
|
||||||
|
CREATE USER 'wearwell'@'localhost'
|
||||||
|
IDENTIFIED BY 'password';
|
||||||
|
```
|
||||||
|
grant all privileges
|
||||||
|
```
|
||||||
|
GRANT ALL PRIVILEGES ON wearwell.*
|
||||||
|
TO 'wearwell'@'localhost';
|
||||||
|
```
|
||||||
|
apply it
|
||||||
|
```
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
```
|
||||||
|
now you can exit from mariadb and edit your .env file (use .env.example template of project environment)
|
||||||
|
```
|
||||||
|
DB_USER=wearwell
|
||||||
|
DB_PASSWORD=password
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_NAME=wearwell
|
||||||
|
```
|
||||||
|
here is the structure of databse url in .env
|
||||||
|
```
|
||||||
|
mysql+pymysql://wearwell:password@127.0.0.1:3306/wearwell
|
||||||
|
├─────────────┘ ├──────┘ ├──────┘ ├───────┘ ├──┘ ├──────┘
|
||||||
|
│ │ │ │ │ └─── Database name
|
||||||
|
│ │ │ │ └─── Port
|
||||||
|
│ │ │ └─── Host/IP
|
||||||
|
│ │ └─── Password
|
||||||
|
│ └─── Username
|
||||||
|
└─── Database driver (mysql with pymysql connector)
|
||||||
|
```
|
||||||
|
> **⚠️ Warning:** Generate a random key as secret for your .env in production mode
|
||||||
|
it is time to initialize the models from models.py to migrate tables, ... into database
|
||||||
|
```
|
||||||
|
flask db init
|
||||||
|
flask db migrate -m "Init"
|
||||||
|
flask db upgrade
|
||||||
|
```
|
||||||
|
> **📝 Important:** You can use seed_db.py for fake data ( users, products, ... )
|
||||||
|
```
|
||||||
|
python3 ./seed_db.py
|
||||||
|
```
|
||||||
|
resets data then fills data again
|
||||||
|
```
|
||||||
|
python ./seed_db.py --fresh
|
||||||
|
```
|
||||||
|
run the app
|
||||||
|
```
|
||||||
|
python3 ./app.py
|
||||||
|
```
|
||||||
|
|||||||
48
app.py
Normal file
48
app.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from flask import Flask
|
||||||
|
from extensions import db, migrate, login_manager
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = (
|
||||||
|
f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}"
|
||||||
|
f"@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
|
||||||
|
)
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'devsecret')
|
||||||
|
app.config['UPLOAD_FOLDER'] = 'static/uploads'
|
||||||
|
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
|
||||||
|
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif'}
|
||||||
|
|
||||||
|
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||||
|
db.init_app(app)
|
||||||
|
migrate.init_app(app, db)
|
||||||
|
login_manager.init_app(app)
|
||||||
|
login_manager.login_view = 'auth.login'
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(user_id):
|
||||||
|
from models import User
|
||||||
|
return User.query.get(int(user_id))
|
||||||
|
|
||||||
|
from models import User, Product, Category, Cart, CartItem, Order, OrderItem
|
||||||
|
from routes.main import main_bp
|
||||||
|
from routes.cart import cart_bp
|
||||||
|
from routes.auth import auth_bp
|
||||||
|
from routes.admin import admin_bp
|
||||||
|
from routes.user import user_bp
|
||||||
|
|
||||||
|
app.register_blueprint(main_bp)
|
||||||
|
app.register_blueprint(cart_bp)
|
||||||
|
app.register_blueprint(auth_bp)
|
||||||
|
app.register_blueprint(admin_bp)
|
||||||
|
app.register_blueprint(user_bp)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = create_app()
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
BIN
diagram_wearwell_db.png
Normal file
BIN
diagram_wearwell_db.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
8
extensions.py
Normal file
8
extensions.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from flask_login import LoginManager
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
migrate = Migrate()
|
||||||
|
login_manager = LoginManager()
|
||||||
|
login_manager.login_view = 'auth.login'
|
||||||
BIN
homepage.png
Normal file
BIN
homepage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 337 KiB |
196
models.py
Normal file
196
models.py
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from flask_login import UserMixin
|
||||||
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
from extensions import db
|
||||||
|
|
||||||
|
class SizeEnum:
|
||||||
|
XS = 'XS'
|
||||||
|
S = 'S'
|
||||||
|
M = 'M'
|
||||||
|
L = 'L'
|
||||||
|
XL = 'XL'
|
||||||
|
XXL = 'XXL'
|
||||||
|
XXXL = 'XXXL'
|
||||||
|
ALL = [XS, S, M, L, XL, XXL, XXXL]
|
||||||
|
|
||||||
|
class CategoryEnum:
|
||||||
|
T_SHIRTS = 'T-Shirts'
|
||||||
|
SHIRTS = 'Shirts'
|
||||||
|
JEANS = 'Jeans'
|
||||||
|
PANTS = 'Pants'
|
||||||
|
JACKETS = 'Jackets'
|
||||||
|
HOODIES = 'Hoodies'
|
||||||
|
SWEATERS = 'Sweaters'
|
||||||
|
SHORTS = 'Shorts'
|
||||||
|
DRESSES = 'Dresses'
|
||||||
|
SKIRTS = 'Skirts'
|
||||||
|
ACTIVEWEAR = 'Activewear'
|
||||||
|
SHOES = 'Shoes'
|
||||||
|
ACCESSORIES = 'Accessories'
|
||||||
|
ALL = [
|
||||||
|
T_SHIRTS, SHIRTS, JEANS, PANTS, JACKETS, HOODIES,
|
||||||
|
SWEATERS, SHORTS, DRESSES, SKIRTS, ACTIVEWEAR,
|
||||||
|
SHOES, ACCESSORIES
|
||||||
|
]
|
||||||
|
|
||||||
|
class User(db.Model, UserMixin):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
|
password_hash = db.Column(db.String(255))
|
||||||
|
is_admin = db.Column(db.Boolean, default=False)
|
||||||
|
cart = db.relationship('Cart', backref='user', uselist=False)
|
||||||
|
orders = db.relationship('Order', backref='user', lazy=True)
|
||||||
|
reviews = db.relationship('Review', backref='user', lazy=True)
|
||||||
|
likes = db.relationship('ProductLike', backref='user', lazy=True)
|
||||||
|
|
||||||
|
def set_password(self, password):
|
||||||
|
self.password_hash = generate_password_hash(password)
|
||||||
|
|
||||||
|
def check_password(self, password):
|
||||||
|
return check_password_hash(self.password_hash, password)
|
||||||
|
|
||||||
|
class Category(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(50), nullable=False)
|
||||||
|
products = db.relationship('Product', backref='category', lazy=True)
|
||||||
|
|
||||||
|
class Product(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(120), nullable=False)
|
||||||
|
description = db.Column(db.Text)
|
||||||
|
price = db.Column(db.Float, nullable=False)
|
||||||
|
stock = db.Column(db.Integer, default=0)
|
||||||
|
color = db.Column(db.String(50))
|
||||||
|
size = db.Column(db.String(10))
|
||||||
|
material = db.Column(db.String(50))
|
||||||
|
company = db.Column(db.String(100))
|
||||||
|
sku = db.Column(db.String(50), unique=True)
|
||||||
|
weight = db.Column(db.Float)
|
||||||
|
dimensions = db.Column(db.String(50))
|
||||||
|
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
|
||||||
|
images = db.relationship(
|
||||||
|
'ProductImage', backref='product', lazy=True, cascade="all, delete-orphan")
|
||||||
|
reviews = db.relationship('Review', backref='product',
|
||||||
|
lazy=True, cascade="all, delete-orphan")
|
||||||
|
likes = db.relationship('ProductLike', backref='product',
|
||||||
|
lazy=True, cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def average_rating(self):
|
||||||
|
if not self.reviews:
|
||||||
|
return 0
|
||||||
|
total = sum(review.rating for review in self.reviews)
|
||||||
|
return round(total / len(self.reviews), 1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def like_count(self):
|
||||||
|
return len([like for like in self.likes if like.is_like])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dislike_count(self):
|
||||||
|
return len([like for like in self.likes if not like.is_like])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def review_count(self):
|
||||||
|
return len(self.reviews)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def primary_image(self):
|
||||||
|
if self.images:
|
||||||
|
primary = next((img for img in self.images if img.is_primary), None)
|
||||||
|
if primary:
|
||||||
|
return primary
|
||||||
|
return self.images[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_image_url(self):
|
||||||
|
"""Alias for template compatibility"""
|
||||||
|
return self.get_primary_image_url()
|
||||||
|
|
||||||
|
def in_stock(self):
|
||||||
|
return self.stock > 0
|
||||||
|
|
||||||
|
def get_primary_image_url(self):
|
||||||
|
primary = self.primary_image
|
||||||
|
if primary:
|
||||||
|
return f"/static/uploads/products/{primary.filename}"
|
||||||
|
return "/static/images/default-product.jpg"
|
||||||
|
|
||||||
|
def get_all_image_urls(self):
|
||||||
|
return [f"/static/uploads/products/{img.filename}" for img in self.images]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<Product {self.name}>'
|
||||||
|
|
||||||
|
class ProductImage(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
product_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey('product.id'), nullable=False)
|
||||||
|
filename = db.Column(db.String(200), nullable=False)
|
||||||
|
is_primary = db.Column(db.Boolean, default=False)
|
||||||
|
display_order = db.Column(db.Integer, default=0)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
class ProductLike(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
product_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey('product.id'), nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
is_like = db.Column(db.Boolean, default=True)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
|
__table_args__ = (db.UniqueConstraint(
|
||||||
|
'product_id', 'user_id', name='unique_product_user_like'),)
|
||||||
|
|
||||||
|
class Review(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
product_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey('product.id'), nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
rating = db.Column(db.Integer, nullable=False)
|
||||||
|
title = db.Column(db.String(200))
|
||||||
|
comment = db.Column(db.Text)
|
||||||
|
is_verified_purchase = db.Column(db.Boolean, default=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
|
class Cart(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
|
items = db.relationship('CartItem', backref='cart',
|
||||||
|
cascade="all, delete-orphan", lazy=True)
|
||||||
|
|
||||||
|
class CartItem(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
cart_id = db.Column(db.Integer, db.ForeignKey('cart.id'))
|
||||||
|
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
|
||||||
|
quantity = db.Column(db.Integer, default=1)
|
||||||
|
selected_size = db.Column(db.String(10))
|
||||||
|
selected_color = db.Column(db.String(50))
|
||||||
|
|
||||||
|
product = db.relationship('Product')
|
||||||
|
|
||||||
|
class Order(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
status = db.Column(db.String(50), default='pending')
|
||||||
|
|
||||||
|
items = db.relationship('OrderItem', backref='order',
|
||||||
|
cascade='all, delete-orphan', lazy=True)
|
||||||
|
|
||||||
|
class OrderItem(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
order_id = db.Column(db.Integer, db.ForeignKey('order.id'))
|
||||||
|
product_id = db.Column(db.Integer, db.ForeignKey('product.id'))
|
||||||
|
quantity = db.Column(db.Integer, default=1)
|
||||||
|
price_at_purchase = db.Column(db.Float)
|
||||||
|
selected_size = db.Column(db.String(10))
|
||||||
|
selected_color = db.Column(db.String(50))
|
||||||
|
|
||||||
|
product = db.relationship('Product')
|
||||||
|
|
||||||
26
requirements.txt
Normal file
26
requirements.txt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
alembic==1.17.2
|
||||||
|
autopep8==2.3.2
|
||||||
|
blinker==1.9.0
|
||||||
|
click==8.3.1
|
||||||
|
Faker==40.1.0
|
||||||
|
Flask==3.1.2
|
||||||
|
Flask-Login==0.6.3
|
||||||
|
Flask-Migrate==4.1.0
|
||||||
|
Flask-SQLAlchemy==3.0.5
|
||||||
|
greenlet==3.3.0
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
Jinja2==3.1.6
|
||||||
|
Mako==1.3.10
|
||||||
|
MarkupSafe==3.0.3
|
||||||
|
mypy_extensions==1.1.0
|
||||||
|
packaging==25.0
|
||||||
|
pathspec==0.12.1
|
||||||
|
platformdirs==4.5.1
|
||||||
|
pycodestyle==2.14.0
|
||||||
|
PyMySQL==1.1.2
|
||||||
|
python-dotenv==1.2.1
|
||||||
|
pytokens==0.3.0
|
||||||
|
SQLAlchemy==1.4.22
|
||||||
|
typing_extensions==4.15.0
|
||||||
|
tzdata==2025.3
|
||||||
|
Werkzeug==3.1.4
|
||||||
7
routes/__init__.py
Normal file
7
routes/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from .main import main_bp
|
||||||
|
from .cart import cart_bp
|
||||||
|
from .auth import auth_bp
|
||||||
|
from .admin import admin_bp
|
||||||
|
from .user import user_bp
|
||||||
|
|
||||||
|
__all__ = ['main_bp', 'cart_bp', 'auth_bp', 'admin_bp', 'user_bp']
|
||||||
526
routes/admin.py
Normal file
526
routes/admin.py
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
|
||||||
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, current_app
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from models import db, Product, Category, ProductImage, Review, SizeEnum, User, Order, OrderItem
|
||||||
|
import os
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
import json
|
||||||
|
from functools import wraps
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def admin_required(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
flash("Please login to access admin area", "danger")
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
if not current_user.is_admin:
|
||||||
|
flash("Admin access only!", "danger")
|
||||||
|
return redirect(url_for('main.home'))
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def allowed_file(filename):
|
||||||
|
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||||||
|
return '.' in filename and \
|
||||||
|
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_default_category():
|
||||||
|
default = Category.query.filter_by(name='Uncategorized').first()
|
||||||
|
if not default:
|
||||||
|
default = Category(name='Uncategorized')
|
||||||
|
db.session.add(default)
|
||||||
|
db.session.commit()
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def get_categories():
|
||||||
|
categories = Category.query.all()
|
||||||
|
if not categories:
|
||||||
|
default_cat = get_or_create_default_category()
|
||||||
|
categories = [default_cat]
|
||||||
|
return categories
|
||||||
|
|
||||||
|
|
||||||
|
def get_admin_stats():
|
||||||
|
try:
|
||||||
|
total_revenue_result = db.session.query(
|
||||||
|
func.sum(OrderItem.price_at_purchase * OrderItem.quantity)
|
||||||
|
).join(Order).filter(Order.status.in_(['delivered', 'completed'])).scalar()
|
||||||
|
|
||||||
|
total_revenue = float(
|
||||||
|
total_revenue_result) if total_revenue_result else 0.0
|
||||||
|
|
||||||
|
except:
|
||||||
|
|
||||||
|
total_revenue = 0.0
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
'total_products': Product.query.count(),
|
||||||
|
'total_categories': Category.query.count(),
|
||||||
|
'total_reviews': Review.query.count(),
|
||||||
|
'total_orders': Order.query.count() if hasattr(Order, 'query') else 0,
|
||||||
|
'total_users': User.query.count() if hasattr(User, 'query') else 1,
|
||||||
|
'total_revenue': total_revenue,
|
||||||
|
'low_stock': Product.query.filter(Product.stock > 0, Product.stock <= 5).count(),
|
||||||
|
'out_of_stock': Product.query.filter(Product.stock == 0).count(),
|
||||||
|
'total_value': db.session.query(func.sum(Product.price * Product.stock)).scalar() or 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@admin_bp.route('/')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def dashboard():
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
per_page = 20
|
||||||
|
products = Product.query.order_by(
|
||||||
|
Product.id.desc()).paginate(page=page, per_page=per_page)
|
||||||
|
|
||||||
|
stats = get_admin_stats()
|
||||||
|
|
||||||
|
return render_template('admin/dashboard.html',
|
||||||
|
products=products,
|
||||||
|
stats=stats,
|
||||||
|
SizeEnum=SizeEnum)
|
||||||
|
|
||||||
|
@admin_bp.route('/add', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def add_product():
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
name = request.form['name']
|
||||||
|
description = request.form.get('description', '')
|
||||||
|
price = float(request.form['price'])
|
||||||
|
stock = int(request.form.get('stock', 0))
|
||||||
|
sku = request.form.get('sku', '')
|
||||||
|
color = request.form.get('color', '')
|
||||||
|
size = request.form.get('size', '')
|
||||||
|
product_type = request.form.get('product_type', '')
|
||||||
|
material = request.form.get('material', '')
|
||||||
|
company = request.form.get('company', '')
|
||||||
|
weight = request.form.get('weight')
|
||||||
|
dimensions = request.form.get('dimensions', '')
|
||||||
|
|
||||||
|
if weight:
|
||||||
|
try:
|
||||||
|
weight = float(weight)
|
||||||
|
except ValueError:
|
||||||
|
weight = None
|
||||||
|
else:
|
||||||
|
weight = None
|
||||||
|
|
||||||
|
category_id = request.form.get('category_id')
|
||||||
|
if not category_id:
|
||||||
|
category = get_or_create_default_category()
|
||||||
|
category_id = category.id
|
||||||
|
|
||||||
|
product = Product(
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
price=price,
|
||||||
|
stock=stock,
|
||||||
|
sku=sku,
|
||||||
|
color=color,
|
||||||
|
size=size,
|
||||||
|
material=material,
|
||||||
|
company=company,
|
||||||
|
weight=weight,
|
||||||
|
dimensions=dimensions,
|
||||||
|
category_id=category_id
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(product)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
if 'images[]' in request.files:
|
||||||
|
files = request.files.getlist('images[]')
|
||||||
|
for i, file in enumerate(files):
|
||||||
|
if file and file.filename and allowed_file(file.filename):
|
||||||
|
upload_dir = 'static/uploads/products'
|
||||||
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
|
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
name_part, ext = os.path.splitext(filename)
|
||||||
|
unique_filename = f"{name_part}_{product.id}_{i}{ext}"
|
||||||
|
file_path = os.path.join(upload_dir, unique_filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
product_image = ProductImage(
|
||||||
|
product_id=product.id,
|
||||||
|
filename=unique_filename,
|
||||||
|
is_primary=(i == 0),
|
||||||
|
display_order=i
|
||||||
|
)
|
||||||
|
db.session.add(product_image)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Product '{name}' added successfully!", "success")
|
||||||
|
return redirect(url_for('admin.dashboard'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f"Error adding product: {str(e)}", "danger")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
categories = get_categories()
|
||||||
|
return render_template('admin/add_product.html',
|
||||||
|
categories=categories,
|
||||||
|
sizes=SizeEnum.ALL)
|
||||||
|
|
||||||
|
@admin_bp.route('/edit/<int:id>', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def edit_product(id):
|
||||||
|
product = Product.query.get_or_404(id)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
product.name = request.form['name']
|
||||||
|
product.description = request.form.get('description', '')
|
||||||
|
product.price = float(request.form['price'])
|
||||||
|
product.stock = int(request.form.get('stock', 0))
|
||||||
|
product.sku = request.form.get('sku', '')
|
||||||
|
product.color = request.form.get('color', '')
|
||||||
|
product.size = request.form.get('size', '')
|
||||||
|
product.material = request.form.get('material', '')
|
||||||
|
product.company = request.form.get('company', '')
|
||||||
|
weight = request.form.get('weight')
|
||||||
|
if weight:
|
||||||
|
try:
|
||||||
|
product.weight = float(weight)
|
||||||
|
except ValueError:
|
||||||
|
product.weight = None
|
||||||
|
else:
|
||||||
|
product.weight = None
|
||||||
|
|
||||||
|
product.dimensions = request.form.get('dimensions', '')
|
||||||
|
|
||||||
|
category_id = request.form.get('category_id')
|
||||||
|
if category_id:
|
||||||
|
product.category_id = category_id
|
||||||
|
|
||||||
|
if 'images[]' in request.files:
|
||||||
|
files = request.files.getlist('images[]')
|
||||||
|
for i, file in enumerate(files):
|
||||||
|
if file and file.filename and allowed_file(file.filename):
|
||||||
|
upload_dir = 'static/uploads/products'
|
||||||
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
name_part, ext = os.path.splitext(filename)
|
||||||
|
unique_filename = f"{name_part}_{product.id}_{len(product.images) + i}{ext}"
|
||||||
|
file_path = os.path.join(upload_dir, unique_filename)
|
||||||
|
file.save(file_path)
|
||||||
|
product_image = ProductImage(
|
||||||
|
product_id=product.id,
|
||||||
|
filename=unique_filename,
|
||||||
|
display_order=len(product.images) + i
|
||||||
|
)
|
||||||
|
db.session.add(product_image)
|
||||||
|
|
||||||
|
if 'deleted_images' in request.form:
|
||||||
|
deleted_images_str = request.form['deleted_images']
|
||||||
|
if deleted_images_str:
|
||||||
|
try:
|
||||||
|
deleted_ids = json.loads(deleted_images_str)
|
||||||
|
for img_id in deleted_ids:
|
||||||
|
image = ProductImage.query.get(img_id)
|
||||||
|
if image and image.product_id == product.id:
|
||||||
|
|
||||||
|
file_path = os.path.join(
|
||||||
|
'static/uploads/products', image.filename)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
db.session.delete(image)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if 'primary_image' in request.form:
|
||||||
|
primary_id = request.form['primary_image']
|
||||||
|
if primary_id:
|
||||||
|
try:
|
||||||
|
primary_id = int(primary_id)
|
||||||
|
for img in product.images:
|
||||||
|
img.is_primary = (img.id == primary_id)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash("Product updated successfully!", "success")
|
||||||
|
return redirect(url_for('admin.dashboard'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f"Error updating product: {str(e)}", "danger")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
categories = get_categories()
|
||||||
|
return render_template('admin/edit_product.html',
|
||||||
|
product=product,
|
||||||
|
categories=categories,
|
||||||
|
sizes=SizeEnum.ALL)
|
||||||
|
|
||||||
|
@admin_bp.route('/delete/<int:id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def delete_product(id):
|
||||||
|
product = Product.query.get_or_404(id)
|
||||||
|
try:
|
||||||
|
for image in product.images:
|
||||||
|
file_path = os.path.join('static/uploads/products', image.filename)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
db.session.delete(product)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Product deleted successfully!", "info")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error deleting product: {str(e)}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('admin.dashboard'))
|
||||||
|
|
||||||
|
@admin_bp.route('/image/delete/<int:id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def delete_image(id):
|
||||||
|
try:
|
||||||
|
image = ProductImage.query.get_or_404(id)
|
||||||
|
|
||||||
|
file_path = os.path.join('static/uploads/products', image.filename)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
db.session.delete(image)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({'success': True})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
@admin_bp.route('/reviews')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def manage_reviews():
|
||||||
|
reviews = Review.query.order_by(Review.created_at.desc()).all()
|
||||||
|
return render_template('admin/reviews.html', reviews=reviews)
|
||||||
|
|
||||||
|
@admin_bp.route('/review/delete/<int:id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def delete_review(id):
|
||||||
|
review = Review.query.get_or_404(id)
|
||||||
|
try:
|
||||||
|
db.session.delete(review)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Review deleted successfully!", "info")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error deleting review: {str(e)}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('admin.manage_reviews'))
|
||||||
|
|
||||||
|
@admin_bp.route('/categories')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def manage_categories():
|
||||||
|
categories = Category.query.order_by(Category.name.asc()).all()
|
||||||
|
return render_template('admin/categories.html', categories=categories)
|
||||||
|
|
||||||
|
@admin_bp.route('/category/add', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def add_category():
|
||||||
|
name = request.form.get('name', '').strip()
|
||||||
|
if not name:
|
||||||
|
flash("Category name cannot be empty", "danger")
|
||||||
|
return redirect(url_for('admin.manage_categories'))
|
||||||
|
|
||||||
|
existing = Category.query.filter_by(name=name).first()
|
||||||
|
if existing:
|
||||||
|
flash(f"Category '{name}' already exists", "warning")
|
||||||
|
return redirect(url_for('admin.manage_categories'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
category = Category(name=name)
|
||||||
|
db.session.add(category)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Category '{name}' added successfully!", "success")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error adding category: {str(e)}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('admin.manage_categories'))
|
||||||
|
|
||||||
|
@admin_bp.route('/category/delete/<int:id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def delete_category(id):
|
||||||
|
category = Category.query.get_or_404(id)
|
||||||
|
if category.products:
|
||||||
|
flash(
|
||||||
|
f"Cannot delete category '{category.name}' because it has {len(category.products)} product(s)", "danger")
|
||||||
|
return redirect(url_for('admin.manage_categories'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.delete(category)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Category '{category.name}' deleted successfully!", "info")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error deleting category: {str(e)}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('admin.manage_categories'))
|
||||||
|
|
||||||
|
@admin_bp.route('/stats')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def stats():
|
||||||
|
stats_data = get_admin_stats()
|
||||||
|
return render_template('admin/stats.html', stats=stats_data)
|
||||||
|
|
||||||
|
@admin_bp.route('/users')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def manage_users():
|
||||||
|
users = User.query.order_by(User.id.desc()).all()
|
||||||
|
return render_template('admin/users.html', users=users)
|
||||||
|
|
||||||
|
@admin_bp.route('/user/toggle_admin/<int:id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def toggle_user_admin(id):
|
||||||
|
user = User.query.get_or_404(id)
|
||||||
|
|
||||||
|
if user.id == current_user.id:
|
||||||
|
flash("You cannot remove admin privileges from yourself", "warning")
|
||||||
|
return redirect(url_for('admin.manage_users'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
user.is_admin = not user.is_admin
|
||||||
|
db.session.commit()
|
||||||
|
status = "granted" if user.is_admin else "removed"
|
||||||
|
flash(f"Admin privileges {status} for {user.email}", "success")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error updating user: {str(e)}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('admin.manage_users'))
|
||||||
|
|
||||||
|
@admin_bp.route('/user/delete/<int:id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def delete_user(id):
|
||||||
|
user = User.query.get_or_404(id)
|
||||||
|
|
||||||
|
if user.id == current_user.id:
|
||||||
|
flash("You cannot delete your own account", "warning")
|
||||||
|
return redirect(url_for('admin.manage_users'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"User {user.email} deleted successfully", "info")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error deleting user: {str(e)}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('admin.manage_users'))
|
||||||
|
|
||||||
|
@admin_bp.route('/orders')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def manage_orders():
|
||||||
|
orders = Order.query.order_by(Order.created_at.desc()).all()
|
||||||
|
return render_template('admin/orders.html', orders=orders)
|
||||||
|
|
||||||
|
@admin_bp.route('/order/<int:id>')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def order_detail(id):
|
||||||
|
order = Order.query.get_or_404(id)
|
||||||
|
return render_template('admin/order_detail.html', order=order)
|
||||||
|
|
||||||
|
@admin_bp.route('/order/update_status/<int:id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def update_order_status(id):
|
||||||
|
order = Order.query.get_or_404(id)
|
||||||
|
new_status = request.form.get('status')
|
||||||
|
|
||||||
|
if new_status in ['pending', 'processing', 'shipped', 'delivered', 'cancelled']:
|
||||||
|
try:
|
||||||
|
order.status = new_status
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Order #{order.id} status updated to {new_status}", "success")
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error updating order: {str(e)}", "danger")
|
||||||
|
else:
|
||||||
|
flash("Invalid status", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('admin.order_detail', id=id))
|
||||||
|
|
||||||
|
@admin_bp.route('/quick/restock_low')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def quick_restock_low():
|
||||||
|
"""Quick action to restock low inventory items"""
|
||||||
|
try:
|
||||||
|
low_stock_products = Product.query.filter(
|
||||||
|
Product.stock > 0,
|
||||||
|
Product.stock <= 5
|
||||||
|
).all()
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for product in low_stock_products:
|
||||||
|
product.stock += 10
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if count > 0:
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Restocked {count} low inventory products", "success")
|
||||||
|
else:
|
||||||
|
flash("No low inventory products found", "info")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error restocking products: {str(e)}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('admin.dashboard'))
|
||||||
|
|
||||||
|
@admin_bp.route('/quick/update_prices', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def quick_update_prices():
|
||||||
|
"""Quick action to update prices by percentage"""
|
||||||
|
try:
|
||||||
|
percentage = float(request.form.get('percentage', 0))
|
||||||
|
|
||||||
|
if percentage != 0:
|
||||||
|
|
||||||
|
multiplier = 1 + (percentage / 100)
|
||||||
|
|
||||||
|
products = Product.query.all()
|
||||||
|
for product in products:
|
||||||
|
product.price = round(product.price * multiplier, 2)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
action = "increased" if percentage > 0 else "decreased"
|
||||||
|
flash(
|
||||||
|
f"Prices {action} by {abs(percentage)}% for all products", "success")
|
||||||
|
else:
|
||||||
|
flash("No price change specified", "warning")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error updating prices: {str(e)}", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('admin.dashboard'))
|
||||||
43
routes/auth.py
Normal file
43
routes/auth.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
|
from models import db, User
|
||||||
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
|
|
||||||
|
auth_bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||||
|
def register():
|
||||||
|
if request.method == 'POST':
|
||||||
|
email = request.form['email']
|
||||||
|
password = request.form['password']
|
||||||
|
if User.query.filter_by(email=email).first():
|
||||||
|
flash("Email already registered", "danger")
|
||||||
|
return redirect(url_for('auth.register'))
|
||||||
|
user = User(email=email)
|
||||||
|
user.set_password(password)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Registered successfully!", "success")
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
return render_template('register.html')
|
||||||
|
|
||||||
|
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||||
|
def login():
|
||||||
|
if request.method == 'POST':
|
||||||
|
email = request.form['email']
|
||||||
|
password = request.form['password']
|
||||||
|
user = User.query.filter_by(email=email).first()
|
||||||
|
if user and user.check_password(password):
|
||||||
|
login_user(user)
|
||||||
|
flash("Logged in successfully!", "success")
|
||||||
|
next_page = request.args.get('next')
|
||||||
|
return redirect(next_page or url_for('main.home'))
|
||||||
|
flash("Invalid credentials", "danger")
|
||||||
|
return render_template('login.html')
|
||||||
|
|
||||||
|
@auth_bp.route('/logout')
|
||||||
|
@login_required
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
flash("Logged out successfully!", "info")
|
||||||
|
return redirect(url_for('main.home'))
|
||||||
218
routes/cart.py
Normal file
218
routes/cart.py
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
|
||||||
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from models import db, Cart, CartItem, Product
|
||||||
|
|
||||||
|
cart_bp = Blueprint('cart', __name__)
|
||||||
|
|
||||||
|
@cart_bp.route('/cart')
|
||||||
|
@login_required
|
||||||
|
def view_cart():
|
||||||
|
|
||||||
|
cart = Cart.query.filter_by(user_id=current_user.id).first()
|
||||||
|
if not cart:
|
||||||
|
cart = Cart(user_id=current_user.id)
|
||||||
|
db.session.add(cart)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
items = cart.items
|
||||||
|
total = 0
|
||||||
|
item_details = []
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
product = Product.query.get(item.product_id)
|
||||||
|
if product:
|
||||||
|
subtotal = product.price * item.quantity
|
||||||
|
total += subtotal
|
||||||
|
item_details.append({
|
||||||
|
'id': item.id,
|
||||||
|
'product': product,
|
||||||
|
'quantity': item.quantity,
|
||||||
|
'subtotal': subtotal,
|
||||||
|
'max_quantity': product.stock
|
||||||
|
})
|
||||||
|
|
||||||
|
return render_template('cart.html', items=item_details, total=total)
|
||||||
|
|
||||||
|
@cart_bp.route('/add/<int:product_id>')
|
||||||
|
@login_required
|
||||||
|
def add_to_cart(product_id):
|
||||||
|
|
||||||
|
cart = Cart.query.filter_by(user_id=current_user.id).first()
|
||||||
|
if not cart:
|
||||||
|
cart = Cart(user_id=current_user.id)
|
||||||
|
db.session.add(cart)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
product = Product.query.get(product_id)
|
||||||
|
if not product:
|
||||||
|
flash("Product not found", "danger")
|
||||||
|
return redirect(url_for('main.products'))
|
||||||
|
|
||||||
|
if product.stock <= 0:
|
||||||
|
flash(f"Sorry, {product.name} is out of stock", "danger")
|
||||||
|
return redirect(url_for('main.products'))
|
||||||
|
|
||||||
|
cart_item = CartItem.query.filter_by(
|
||||||
|
cart_id=cart.id, product_id=product_id).first()
|
||||||
|
|
||||||
|
if cart_item:
|
||||||
|
|
||||||
|
if cart_item.quantity + 1 > product.stock:
|
||||||
|
flash(f"Only {product.stock} items available in stock", "warning")
|
||||||
|
return redirect(url_for('main.products'))
|
||||||
|
|
||||||
|
cart_item.quantity += 1
|
||||||
|
else:
|
||||||
|
|
||||||
|
cart_item = CartItem(cart_id=cart.id, product_id=product_id, quantity=1)
|
||||||
|
db.session.add(cart_item)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Added {product.name} to cart!", "success")
|
||||||
|
|
||||||
|
referrer = request.referrer
|
||||||
|
if referrer and ('/product/' in referrer or '/products' in referrer):
|
||||||
|
return redirect(referrer)
|
||||||
|
return redirect(url_for('main.products'))
|
||||||
|
|
||||||
|
@cart_bp.route('/remove/<int:item_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def remove_from_cart(item_id):
|
||||||
|
cart_item = CartItem.query.get_or_404(item_id)
|
||||||
|
|
||||||
|
if cart_item.cart.user_id != current_user.id:
|
||||||
|
flash("You don't have permission to remove this item", "danger")
|
||||||
|
return redirect(url_for('cart.view_cart'))
|
||||||
|
|
||||||
|
product_name = cart_item.product.name
|
||||||
|
db.session.delete(cart_item)
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"{product_name} removed from cart", "info")
|
||||||
|
return redirect(url_for('cart.view_cart'))
|
||||||
|
|
||||||
|
@cart_bp.route('/update/<int:item_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def update_cart(item_id):
|
||||||
|
cart_item = CartItem.query.get_or_404(item_id)
|
||||||
|
|
||||||
|
|
||||||
|
if cart_item.cart.user_id != current_user.id:
|
||||||
|
flash("You don't have permission to update this item", "danger")
|
||||||
|
return redirect(url_for('cart.view_cart'))
|
||||||
|
|
||||||
|
new_quantity = request.form.get('quantity')
|
||||||
|
product = Product.query.get(cart_item.product_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_quantity = int(new_quantity)
|
||||||
|
if new_quantity > 0:
|
||||||
|
if new_quantity > product.stock:
|
||||||
|
flash(
|
||||||
|
f"Only {product.stock} items available in stock. Quantity adjusted.", "warning")
|
||||||
|
new_quantity = product.stock
|
||||||
|
|
||||||
|
cart_item.quantity = new_quantity
|
||||||
|
db.session.commit()
|
||||||
|
flash("Cart updated", "success")
|
||||||
|
elif new_quantity == 0:
|
||||||
|
db.session.delete(cart_item)
|
||||||
|
db.session.commit()
|
||||||
|
flash("Item removed from cart", "info")
|
||||||
|
else:
|
||||||
|
flash("Quantity must be at least 0", "danger")
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
flash("Invalid quantity", "danger")
|
||||||
|
|
||||||
|
return redirect(url_for('cart.view_cart'))
|
||||||
|
|
||||||
|
@cart_bp.route('/clear', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def clear_cart():
|
||||||
|
cart = Cart.query.filter_by(user_id=current_user.id).first()
|
||||||
|
if cart:
|
||||||
|
items = cart.items
|
||||||
|
if items:
|
||||||
|
item_count = len(items)
|
||||||
|
|
||||||
|
CartItem.query.filter_by(cart_id=cart.id).delete()
|
||||||
|
db.session.commit()
|
||||||
|
flash(f"Cart cleared ({item_count} items removed)", "info")
|
||||||
|
else:
|
||||||
|
flash("Your cart is already empty", "info")
|
||||||
|
|
||||||
|
return redirect(url_for('cart.view_cart'))
|
||||||
|
|
||||||
|
@cart_bp.route('/checkout', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def checkout():
|
||||||
|
cart = Cart.query.filter_by(user_id=current_user.id).first()
|
||||||
|
if not cart or not cart.items:
|
||||||
|
flash("Your cart is empty", "warning")
|
||||||
|
return redirect(url_for('cart.view_cart'))
|
||||||
|
|
||||||
|
for item in cart.items:
|
||||||
|
product = Product.query.get(item.product_id)
|
||||||
|
if product.stock < item.quantity:
|
||||||
|
flash(
|
||||||
|
f"Sorry, {product.name} only has {product.stock} items in stock (you have {item.quantity} in cart)", "danger")
|
||||||
|
return redirect(url_for('cart.view_cart'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
total = 0
|
||||||
|
order_items = []
|
||||||
|
|
||||||
|
for item in cart.items:
|
||||||
|
product = Product.query.get(item.product_id)
|
||||||
|
|
||||||
|
product.stock -= item.quantity
|
||||||
|
|
||||||
|
total += product.price * item.quantity
|
||||||
|
|
||||||
|
order_items.append({
|
||||||
|
'product_id': product.id,
|
||||||
|
'quantity': item.quantity,
|
||||||
|
'price': product.price
|
||||||
|
})
|
||||||
|
|
||||||
|
from models import Order, OrderItem
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
order = Order(user_id=current_user.id, created_at=datetime.utcnow())
|
||||||
|
db.session.add(order)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
for item_data in order_items:
|
||||||
|
order_item = OrderItem(
|
||||||
|
order_id=order.id,
|
||||||
|
product_id=item_data['product_id'],
|
||||||
|
quantity=item_data['quantity']
|
||||||
|
)
|
||||||
|
db.session.add(order_item)
|
||||||
|
|
||||||
|
CartItem.query.filter_by(cart_id=cart.id).delete()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
flash(f"Order placed successfully! Total: ${total:.2f}", "success")
|
||||||
|
return redirect(url_for('user.profile'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f"Checkout failed: {str(e)}", "danger")
|
||||||
|
return redirect(url_for('cart.view_cart'))
|
||||||
|
|
||||||
|
total = sum(item.product.price * item.quantity for item in cart.items)
|
||||||
|
return render_template('checkout.html', cart=cart, total=total)
|
||||||
|
|
||||||
|
@cart_bp.route('/count')
|
||||||
|
@login_required
|
||||||
|
def cart_count():
|
||||||
|
cart = Cart.query.filter_by(user_id=current_user.id).first()
|
||||||
|
if cart:
|
||||||
|
count = sum(item.quantity for item in cart.items)
|
||||||
|
else:
|
||||||
|
count = 0
|
||||||
|
return str(count)
|
||||||
149
routes/main.py
Normal file
149
routes/main.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
from flask import Blueprint, render_template, request
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from models import Product, Category, SizeEnum, db
|
||||||
|
from sqlalchemy import or_, and_
|
||||||
|
from extensions import db
|
||||||
|
|
||||||
|
main_bp = Blueprint('main', __name__)
|
||||||
|
|
||||||
|
@main_bp.route('/')
|
||||||
|
def home():
|
||||||
|
featured_products = Product.query.order_by(Product.id.desc()).limit(8).all()
|
||||||
|
|
||||||
|
|
||||||
|
best_sellers = Product.query.order_by(
|
||||||
|
Product.stock.desc(),
|
||||||
|
Product.price.desc()
|
||||||
|
).limit(4).all()
|
||||||
|
|
||||||
|
categories = Category.query.all()
|
||||||
|
|
||||||
|
return render_template('home.html',
|
||||||
|
featured_products=featured_products,
|
||||||
|
best_sellers=best_sellers,
|
||||||
|
categories=categories)
|
||||||
|
|
||||||
|
@main_bp.route('/products')
|
||||||
|
def products():
|
||||||
|
search_query = request.args.get('q', '').strip()
|
||||||
|
category_id = request.args.get('category', '')
|
||||||
|
size = request.args.get('size', '')
|
||||||
|
color = request.args.get('color', '')
|
||||||
|
material = request.args.get('material', '')
|
||||||
|
company = request.args.get('company', '')
|
||||||
|
min_price = request.args.get('min_price', '')
|
||||||
|
max_price = request.args.get('max_price', '')
|
||||||
|
in_stock_only = request.args.get('in_stock') == '1'
|
||||||
|
sort_by = request.args.get('sort', 'newest')
|
||||||
|
|
||||||
|
query = Product.query
|
||||||
|
|
||||||
|
if search_query:
|
||||||
|
search_term = f"%{search_query}%"
|
||||||
|
query = query.filter(
|
||||||
|
or_(
|
||||||
|
Product.name.ilike(search_term),
|
||||||
|
Product.description.ilike(search_term),
|
||||||
|
Product.color.ilike(search_term),
|
||||||
|
Product.material.ilike(search_term),
|
||||||
|
Product.company.ilike(search_term),
|
||||||
|
Product.category.has(Category.name.ilike(search_term))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if category_id:
|
||||||
|
try:
|
||||||
|
query = query.filter(Product.category_id == int(category_id))
|
||||||
|
selected_category_name = Category.query.get(int(category_id)).name
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
selected_category_name = None
|
||||||
|
else:
|
||||||
|
selected_category_name = None
|
||||||
|
|
||||||
|
if size:
|
||||||
|
query = query.filter(Product.size == size)
|
||||||
|
|
||||||
|
if color:
|
||||||
|
query = query.filter(Product.color.ilike(f"%{color}%"))
|
||||||
|
|
||||||
|
if material:
|
||||||
|
query = query.filter(Product.material.ilike(f"%{material}%"))
|
||||||
|
|
||||||
|
if company:
|
||||||
|
query = query.filter(Product.company.ilike(f"%{company}%"))
|
||||||
|
|
||||||
|
if min_price:
|
||||||
|
try:
|
||||||
|
query = query.filter(Product.price >= float(min_price))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if max_price:
|
||||||
|
try:
|
||||||
|
query = query.filter(Product.price <= float(max_price))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if in_stock_only:
|
||||||
|
query = query.filter(Product.stock > 0)
|
||||||
|
|
||||||
|
if sort_by == 'price_low':
|
||||||
|
query = query.order_by(Product.price.asc())
|
||||||
|
elif sort_by == 'price_high':
|
||||||
|
query = query.order_by(Product.price.desc())
|
||||||
|
elif sort_by == 'name':
|
||||||
|
query = query.order_by(Product.name.asc())
|
||||||
|
elif sort_by == 'rating':
|
||||||
|
|
||||||
|
query = query.order_by(Product.id.desc())
|
||||||
|
else:
|
||||||
|
query = query.order_by(Product.id.desc())
|
||||||
|
|
||||||
|
colors = db.session.query(Product.color).distinct().filter(
|
||||||
|
Product.color != None, Product.color != '').all()
|
||||||
|
colors = [c[0] for c in colors if c[0]]
|
||||||
|
|
||||||
|
materials = db.session.query(Product.material).distinct().filter(
|
||||||
|
Product.material != None, Product.material != '').all()
|
||||||
|
materials = [m[0] for m in materials if m[0]]
|
||||||
|
|
||||||
|
companies = db.session.query(Product.company).distinct().filter(
|
||||||
|
Product.company != None, Product.company != '').all()
|
||||||
|
companies = [c[0] for c in companies if c[0]]
|
||||||
|
|
||||||
|
products = query.all()
|
||||||
|
|
||||||
|
categories = Category.query.order_by(Category.name.asc()).all()
|
||||||
|
|
||||||
|
return render_template('products.html',
|
||||||
|
products=products,
|
||||||
|
categories=categories,
|
||||||
|
sizes=SizeEnum.ALL,
|
||||||
|
colors=colors,
|
||||||
|
materials=materials,
|
||||||
|
companies=companies,
|
||||||
|
search_query=search_query,
|
||||||
|
selected_category=category_id,
|
||||||
|
selected_category_name=selected_category_name,
|
||||||
|
selected_size=size,
|
||||||
|
selected_color=color,
|
||||||
|
selected_material=material,
|
||||||
|
selected_company=company,
|
||||||
|
min_price=min_price,
|
||||||
|
max_price=max_price,
|
||||||
|
sort_by=sort_by,
|
||||||
|
in_stock_only=in_stock_only)
|
||||||
|
|
||||||
|
@main_bp.route('/product/<int:id>')
|
||||||
|
def product_detail(id):
|
||||||
|
product = Product.query.get_or_404(id)
|
||||||
|
|
||||||
|
|
||||||
|
related_products = Product.query.filter(
|
||||||
|
Product.category_id == product.category_id,
|
||||||
|
Product.id != product.id
|
||||||
|
).limit(4).all()
|
||||||
|
|
||||||
|
return render_template('product_detail.html',
|
||||||
|
product=product,
|
||||||
|
related_products=related_products)
|
||||||
22
routes/user.py
Normal file
22
routes/user.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from flask import Blueprint, render_template, redirect, url_for, flash
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from models import Order
|
||||||
|
|
||||||
|
user_bp = Blueprint('user', __name__)
|
||||||
|
|
||||||
|
@user_bp.route('/profile')
|
||||||
|
@login_required
|
||||||
|
def profile():
|
||||||
|
|
||||||
|
orders = Order.query.filter_by(user_id=current_user.id).all()
|
||||||
|
return render_template('user_profile.html', orders=orders)
|
||||||
|
|
||||||
|
@user_bp.route('/orders/<int:order_id>')
|
||||||
|
@login_required
|
||||||
|
def order_details(order_id):
|
||||||
|
order = Order.query.get_or_404(order_id)
|
||||||
|
|
||||||
|
if order.user_id != current_user.id:
|
||||||
|
flash("Access denied", "danger")
|
||||||
|
return redirect(url_for('user.profile'))
|
||||||
|
return render_template('order_details.html', order=order)
|
||||||
388
seed_db.py
Normal file
388
seed_db.py
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
import random
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from faker import Faker
|
||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
|
from app import create_app, db
|
||||||
|
from models import (
|
||||||
|
Cart,
|
||||||
|
CartItem,
|
||||||
|
Category,
|
||||||
|
CategoryEnum,
|
||||||
|
Order,
|
||||||
|
OrderItem,
|
||||||
|
Product,
|
||||||
|
ProductImage,
|
||||||
|
ProductLike,
|
||||||
|
Review,
|
||||||
|
User,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_seed_data():
|
||||||
|
"""Create dummy data for testing"""
|
||||||
|
fake = Faker()
|
||||||
|
|
||||||
|
print("🌱 Starting database seeding...")
|
||||||
|
|
||||||
|
|
||||||
|
print("👤 Checking/Creating admin user...")
|
||||||
|
admin = User.query.filter_by(email="admin@wearwell.com").first()
|
||||||
|
if not admin:
|
||||||
|
admin = User(
|
||||||
|
email="admin@wearwell.com",
|
||||||
|
password_hash=generate_password_hash("admin123"),
|
||||||
|
is_admin=True,
|
||||||
|
)
|
||||||
|
db.session.add(admin)
|
||||||
|
print("✅ Created admin user")
|
||||||
|
else:
|
||||||
|
print("⚠️ Admin user already exists, skipping...")
|
||||||
|
|
||||||
|
print("👥 Creating regular users...")
|
||||||
|
users = [admin] if admin else []
|
||||||
|
|
||||||
|
existing_users = User.query.count()
|
||||||
|
users_to_create = max(
|
||||||
|
0, 11 - existing_users
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(users_to_create):
|
||||||
|
email = f"user{i + 1}@example.com"
|
||||||
|
|
||||||
|
|
||||||
|
if not User.query.filter_by(email=email).first():
|
||||||
|
user = User(
|
||||||
|
email=email,
|
||||||
|
password_hash=generate_password_hash(f"password{i + 1}"),
|
||||||
|
is_admin=False,
|
||||||
|
)
|
||||||
|
db.session.add(user)
|
||||||
|
users.append(user)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print("🏷️ Creating categories...")
|
||||||
|
categories = []
|
||||||
|
|
||||||
|
for cat_name in CategoryEnum.ALL:
|
||||||
|
category = Category.query.filter_by(name=cat_name).first()
|
||||||
|
if not category:
|
||||||
|
category = Category(name=cat_name)
|
||||||
|
db.session.add(category)
|
||||||
|
categories.append(category)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print("👕 Creating products...")
|
||||||
|
|
||||||
|
existing_products_count = Product.query.count()
|
||||||
|
products_to_create = max(0, 50 - existing_products_count)
|
||||||
|
|
||||||
|
if products_to_create > 0:
|
||||||
|
clothing_brands = [
|
||||||
|
"Nike",
|
||||||
|
"Adidas",
|
||||||
|
"Levi's",
|
||||||
|
"H&M",
|
||||||
|
"Zara",
|
||||||
|
"Uniqlo",
|
||||||
|
"Gap",
|
||||||
|
"Puma",
|
||||||
|
"Under Armour",
|
||||||
|
"Calvin Klein",
|
||||||
|
]
|
||||||
|
colors = [
|
||||||
|
"Black",
|
||||||
|
"White",
|
||||||
|
"Blue",
|
||||||
|
"Red",
|
||||||
|
"Green",
|
||||||
|
"Gray",
|
||||||
|
"Navy",
|
||||||
|
"Brown",
|
||||||
|
"Beige",
|
||||||
|
"Maroon",
|
||||||
|
]
|
||||||
|
materials = [
|
||||||
|
"Cotton",
|
||||||
|
"Polyester",
|
||||||
|
"Denim",
|
||||||
|
"Wool",
|
||||||
|
"Linen",
|
||||||
|
"Silk",
|
||||||
|
"Leather",
|
||||||
|
"Nylon",
|
||||||
|
"Rayon",
|
||||||
|
"Spandex",
|
||||||
|
]
|
||||||
|
product_types = [
|
||||||
|
"T-Shirt",
|
||||||
|
"Shirt",
|
||||||
|
"Jeans",
|
||||||
|
"Pants",
|
||||||
|
"Jacket",
|
||||||
|
"Hoodie",
|
||||||
|
"Sweater",
|
||||||
|
"Shorts",
|
||||||
|
"Dress",
|
||||||
|
]
|
||||||
|
|
||||||
|
existing_products = Product.query.all()
|
||||||
|
products = existing_products.copy()
|
||||||
|
|
||||||
|
existing_skus = {p.sku for p in existing_products if p.sku}
|
||||||
|
new_skus = set()
|
||||||
|
|
||||||
|
for i in range(products_to_create):
|
||||||
|
while True:
|
||||||
|
sku = f"SKU-{uuid.uuid4().hex[:8].upper()}"
|
||||||
|
if sku not in existing_skus and sku not in new_skus:
|
||||||
|
new_skus.add(sku)
|
||||||
|
break
|
||||||
|
|
||||||
|
category = random.choice(categories)
|
||||||
|
price = round(random.uniform(15, 150), 2)
|
||||||
|
stock = random.randint(0, 100)
|
||||||
|
|
||||||
|
product = Product(
|
||||||
|
name=f"{random.choice(product_types)} - {fake.word().title()}",
|
||||||
|
description=fake.paragraph(nb_sentences=3),
|
||||||
|
price=price,
|
||||||
|
stock=stock,
|
||||||
|
color=random.choice(colors),
|
||||||
|
size=random.choice(["XS", "S", "M", "L", "XL", "XXL"]),
|
||||||
|
material=random.choice(materials),
|
||||||
|
company=random.choice(clothing_brands),
|
||||||
|
sku=sku,
|
||||||
|
weight=round(random.uniform(0.1, 2.0), 2),
|
||||||
|
dimensions=f"{
|
||||||
|
random.randint(
|
||||||
|
30,
|
||||||
|
60)}x{
|
||||||
|
random.randint(
|
||||||
|
20,
|
||||||
|
40)}x{
|
||||||
|
random.randint(
|
||||||
|
2,
|
||||||
|
10)} cm",
|
||||||
|
category_id=category.id,
|
||||||
|
)
|
||||||
|
db.session.add(product)
|
||||||
|
products.append(product)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"✅ Created {products_to_create} new products")
|
||||||
|
else:
|
||||||
|
products = Product.query.all()
|
||||||
|
print(f"⚠️ Already have {existing_products_count} products, skipping...")
|
||||||
|
|
||||||
|
print("🖼️ Creating product images...")
|
||||||
|
new_products = [p for p in products if not p.images]
|
||||||
|
|
||||||
|
for product in new_products:
|
||||||
|
num_images = random.randint(1, 4)
|
||||||
|
for j in range(num_images):
|
||||||
|
image = ProductImage(
|
||||||
|
product_id=product.id,
|
||||||
|
filename=f"product_{product.id}_img_{j + 1}.jpg",
|
||||||
|
is_primary=(j == 0),
|
||||||
|
display_order=j,
|
||||||
|
)
|
||||||
|
db.session.add(image)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
existing_reviews = Review.query.count()
|
||||||
|
if existing_reviews < 100:
|
||||||
|
print("⭐ Creating reviews...")
|
||||||
|
products_without_reviews = [p for p in products if not p.reviews]
|
||||||
|
products_to_review = random.sample(
|
||||||
|
products_without_reviews, min(30, len(products_without_reviews))
|
||||||
|
)
|
||||||
|
|
||||||
|
for product in products_to_review:
|
||||||
|
num_reviews = random.randint(1, 8)
|
||||||
|
for _ in range(num_reviews):
|
||||||
|
user = random.choice(users[1:]) if len(users) > 1 else users[0]
|
||||||
|
review = Review(
|
||||||
|
product_id=product.id,
|
||||||
|
user_id=user.id,
|
||||||
|
rating=random.randint(1, 5),
|
||||||
|
title=fake.sentence(),
|
||||||
|
comment=fake.paragraph(),
|
||||||
|
is_verified_purchase=random.choice([True, False]),
|
||||||
|
created_at=fake.date_time_between(
|
||||||
|
start_date="-1y", end_date="now"),
|
||||||
|
)
|
||||||
|
db.session.add(review)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"✅ Created {Review.query.count() - existing_reviews} new reviews")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Already have {existing_reviews} reviews, skipping...")
|
||||||
|
|
||||||
|
print("🛒 Creating carts...")
|
||||||
|
users_without_carts = [u for u in users if not u.cart]
|
||||||
|
|
||||||
|
for user in users_without_carts[:5]:
|
||||||
|
cart = Cart(user_id=user.id)
|
||||||
|
db.session.add(cart)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
num_items = random.randint(1, 5)
|
||||||
|
for _ in range(num_items):
|
||||||
|
product = random.choice(products)
|
||||||
|
cart_item = CartItem(
|
||||||
|
cart_id=cart.id,
|
||||||
|
product_id=product.id,
|
||||||
|
quantity=random.randint(1, 3),
|
||||||
|
selected_size=product.size,
|
||||||
|
selected_color=product.color,
|
||||||
|
)
|
||||||
|
db.session.add(cart_item)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
existing_orders = Order.query.count()
|
||||||
|
if existing_orders < 15:
|
||||||
|
print("📦 Creating orders...")
|
||||||
|
order_statuses = ["pending", "processing",
|
||||||
|
"shipped", "delivered", "cancelled"]
|
||||||
|
|
||||||
|
orders_to_create = 15 - existing_orders
|
||||||
|
for i in range(orders_to_create):
|
||||||
|
user = random.choice(users)
|
||||||
|
order = Order(
|
||||||
|
user_id=user.id,
|
||||||
|
status=random.choice(order_statuses),
|
||||||
|
created_at=fake.date_time_between(start_date="-6m", end_date="now"),
|
||||||
|
)
|
||||||
|
db.session.add(order)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
num_items = random.randint(1, 5)
|
||||||
|
|
||||||
|
for _ in range(num_items):
|
||||||
|
product = random.choice(products)
|
||||||
|
quantity = random.randint(1, 3)
|
||||||
|
price = product.price
|
||||||
|
|
||||||
|
order_item = OrderItem(
|
||||||
|
order_id=order.id,
|
||||||
|
product_id=product.id,
|
||||||
|
quantity=quantity,
|
||||||
|
price_at_purchase=price,
|
||||||
|
selected_size=product.size,
|
||||||
|
selected_color=product.color,
|
||||||
|
)
|
||||||
|
db.session.add(order_item)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"✅ Created {orders_to_create} new orders")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Already have {existing_orders} orders, skipping...")
|
||||||
|
|
||||||
|
print("❤️ Creating product likes...")
|
||||||
|
existing_likes = ProductLike.query.count()
|
||||||
|
|
||||||
|
if existing_likes < 50:
|
||||||
|
products_to_like = random.sample(products, min(20, len(products)))
|
||||||
|
likes_created = 0
|
||||||
|
|
||||||
|
for product in products_to_like:
|
||||||
|
|
||||||
|
available_users = [u for u in users if u.id != 1]
|
||||||
|
num_users_to_like = min(random.randint(3, 10), len(available_users))
|
||||||
|
users_to_like = random.sample(available_users, num_users_to_like)
|
||||||
|
|
||||||
|
for user in users_to_like:
|
||||||
|
|
||||||
|
existing_like = ProductLike.query.filter_by(
|
||||||
|
product_id=product.id, user_id=user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not existing_like:
|
||||||
|
like = ProductLike(
|
||||||
|
product_id=product.id,
|
||||||
|
user_id=user.id,
|
||||||
|
is_like=random.choice([True, False]),
|
||||||
|
created_at=fake.date_time_between(
|
||||||
|
start_date="-3m", end_date="now"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
db.session.add(like)
|
||||||
|
likes_created += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print(f"✅ Created {likes_created} new likes")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Already have {existing_likes} likes, skipping...")
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("✅ DATABASE SEEDING COMPLETED SUCCESSFULLY!")
|
||||||
|
print("=" * 50)
|
||||||
|
print(f"\n📊 FINAL COUNTS:")
|
||||||
|
print(f" 👥 Users: {User.query.count()}")
|
||||||
|
print(f" 🏷️ Categories: {Category.query.count()}")
|
||||||
|
print(f" 👕 Products: {Product.query.count()}")
|
||||||
|
print(f" 🖼️ Product Images: {ProductImage.query.count()}")
|
||||||
|
print(f" ⭐ Reviews: {Review.query.count()}")
|
||||||
|
print(f" 🛒 Carts: {Cart.query.count()}")
|
||||||
|
print(f" 📦 Orders: {Order.query.count()}")
|
||||||
|
print(f" ❤️ Likes: {ProductLike.query.count()}")
|
||||||
|
|
||||||
|
print("\n🔑 LOGIN CREDENTIALS:")
|
||||||
|
print(" 👑 Admin:")
|
||||||
|
print(" Email: admin@wearwell.com")
|
||||||
|
print(" Password: admin123")
|
||||||
|
print("\n 👤 Test Users (10 available):")
|
||||||
|
print(" Email: user1@example.com")
|
||||||
|
print(" Password: password1")
|
||||||
|
print(" Email: user2@example.com")
|
||||||
|
print(" Password: password2")
|
||||||
|
print(" ... and 8 more users (user3@example.com to user10@example.com)")
|
||||||
|
|
||||||
|
print("\n💡 TIPS:")
|
||||||
|
print(" • Run this script multiple times to add more data")
|
||||||
|
print(" • Data won't duplicate (checks for existing records)")
|
||||||
|
print(" • To start fresh, clear your database first")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_and_seed():
|
||||||
|
"""Clear all data and seed fresh"""
|
||||||
|
print("⚠️ WARNING: This will delete ALL data from the database!")
|
||||||
|
response = input("Are you sure? Type 'YES' to continue: ")
|
||||||
|
|
||||||
|
if response != "YES":
|
||||||
|
print("❌ Operation cancelled")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("🗑️ Clearing database...")
|
||||||
|
|
||||||
|
ProductLike.query.delete()
|
||||||
|
OrderItem.query.delete()
|
||||||
|
Order.query.delete()
|
||||||
|
CartItem.query.delete()
|
||||||
|
Cart.query.delete()
|
||||||
|
Review.query.delete()
|
||||||
|
ProductImage.query.delete()
|
||||||
|
Product.query.delete()
|
||||||
|
Category.query.delete()
|
||||||
|
User.query.delete()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print("✅ Database cleared!")
|
||||||
|
|
||||||
|
create_seed_data()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "--fresh":
|
||||||
|
clear_and_seed()
|
||||||
|
else:
|
||||||
|
create_seed_data()
|
||||||
169
static/css/add_product.css
Normal file
169
static/css/add_product.css
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
.admin-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-area {
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-area:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-prompt {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-prompt svg {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-hint {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-input {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
background: rgba(220, 53, 69, 0.9);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
336
static/css/auth.css
Normal file
336
static/css/auth.css
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
.auth-container {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 40px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1em;
|
||||||
|
transition: all 0.3s;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:hover {
|
||||||
|
border-color: #b0b7c3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.password-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-password {
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-password:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 7px 20px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.auth-links {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 25px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.remember-me {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-me input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background: rgba(220, 53, 69, 0.1);
|
||||||
|
color: #dc3545;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
background: rgba(40, 167, 69, 0.1);
|
||||||
|
color: #28a745;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.flash-messages {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash.success {
|
||||||
|
background: rgba(40, 167, 69, 0.1);
|
||||||
|
color: #28a745;
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash.error,
|
||||||
|
.flash.danger {
|
||||||
|
background: rgba(220, 53, 69, 0.1);
|
||||||
|
color: #dc3545;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash.info {
|
||||||
|
background: rgba(23, 162, 184, 0.1);
|
||||||
|
color: #17a2b8;
|
||||||
|
border-left: 4px solid #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash.warning {
|
||||||
|
background: rgba(255, 193, 7, 0.1);
|
||||||
|
color: #ffc107;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.social-login {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-divider::before,
|
||||||
|
.social-divider::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-divider span {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-social {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-social:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-social.google {
|
||||||
|
color: #db4437;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-social.facebook {
|
||||||
|
color: #4267B2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.auth-container {
|
||||||
|
margin: 20px;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-container h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-social {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.password-strength {
|
||||||
|
margin-top: 5px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #e9ecef;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-bar {
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.3s, background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-weak {
|
||||||
|
background: #dc3545;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-medium {
|
||||||
|
background: #ffc107;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-strong {
|
||||||
|
background: #28a745;
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-very-strong {
|
||||||
|
background: #28a745;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-hints {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-hints ul {
|
||||||
|
margin: 5px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms input[type="checkbox"] {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terms a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
349
static/css/cart.css
Normal file
349
static/css/cart.css
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
.cart-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-container h1 {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.cart-items {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-table thead {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-table th {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-table tbody tr {
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-table tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-table td {
|
||||||
|
padding: 20px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-details {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-details h4 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-description {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-info {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.in-stock {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.low-stock {
|
||||||
|
color: #ffc107;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.out-of-stock {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.quantity-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-input {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1em;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-quantity {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.price,
|
||||||
|
.subtotal {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #007bff;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.cart-summary {
|
||||||
|
padding: 30px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
max-width: 400px;
|
||||||
|
margin-left: auto;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card h3 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-row.total {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: none;
|
||||||
|
padding-top: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.cart-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-continue,
|
||||||
|
.btn-clear,
|
||||||
|
.btn-checkout {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-continue {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-continue:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-checkout {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-checkout:hover {
|
||||||
|
background: #218838;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.empty-cart {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 40px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 4em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-cart h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-cart p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-shop {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 30px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-shop:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cart-table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-checkout {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
static/css/categories.css
Normal file
57
static/css/categories.css
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
.admin-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
}
|
||||||
186
static/css/checkout.css
Normal file
186
static/css/checkout.css
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
.checkout-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-container h1 {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.order-summary {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-summary h3 {
|
||||||
|
margin: 0 0 25px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.3em;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-info h4 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 1em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-info p {
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-total {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #007bff;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-total {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 2px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-total h3 {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.4em;
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.checkout-form {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout-form h3 {
|
||||||
|
margin: 0 0 25px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.3em;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1em;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.form-control {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back,
|
||||||
|
.btn-confirm {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 1em;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.checkout-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back,
|
||||||
|
.btn-confirm {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
343
static/css/dashboard.css
Normal file
343
static/css/dashboard.css
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
.admin-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.admin-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 2em;
|
||||||
|
color: #007bff;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.admin-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-admin {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #218838;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover {
|
||||||
|
background: #e0a800;
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.admin-table-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table thead {
|
||||||
|
background: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table th {
|
||||||
|
padding: 18px 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table tbody tr {
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table td {
|
||||||
|
padding: 15px;
|
||||||
|
vertical-align: middle;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.admin-table td:first-child {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.stock-high {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(40, 167, 69, 0.1);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-low {
|
||||||
|
color: #ffc107;
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(255, 193, 7, 0.1);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-out {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(220, 53, 69, 0.1);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.admin-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
padding: 15px 30px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: #007bff;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: #007bff;
|
||||||
|
border-bottom-color: #007bff;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link {
|
||||||
|
padding: 8px 15px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #007bff;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 0.9em;
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link.active {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
padding: 8px 5px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.badge-active,
|
||||||
|
.badge-inactive {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-active {
|
||||||
|
background: rgba(40, 167, 69, 0.1);
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-inactive {
|
||||||
|
background: rgba(108, 117, 125, 0.1);
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-stats {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-tabs {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
227
static/css/edit_product.css
Normal file
227
static/css/edit_product.css
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
.admin-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-images {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-image {
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete-image:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-images {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px dashed #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-area {
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-area:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-prompt {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-prompt svg {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-input {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
background: rgba(220, 53, 69, 0.9);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 10px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
447
static/css/home.css
Normal file
447
static/css/home.css
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
.hero-section {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 80px 20px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
border-radius: 0 0 20px 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="%23ffffff" fill-opacity="0.1" d="M0,96L48,112C96,128,192,160,288,160C384,160,480,128,576,112C672,96,768,96,864,112C960,128,1056,160,1152,160C1248,160,1344,128,1392,112L1440,96L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path></svg>');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3.5em;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.3em;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
opacity: 0.9;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-search {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 30px auto 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 18px 60px 18px 25px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-search-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-search-button:hover {
|
||||||
|
background: #ff5252;
|
||||||
|
transform: translateY(-50%) scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.categories-section {
|
||||||
|
padding: 40px 20px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 2em;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-all-link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-all-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card:hover {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-image {
|
||||||
|
height: 180px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-count {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.featured-section {
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-home {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card-home:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 15px;
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image-home {
|
||||||
|
height: 200px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image-home img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info-home {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-title-home {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price-home {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-rating-home {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-count {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-meta-home {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.promo-banner {
|
||||||
|
background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 60px 40px;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin: 50px 20px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-content h2 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-content p {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-button {
|
||||||
|
display: inline-block;
|
||||||
|
background: white;
|
||||||
|
color: #4facfe;
|
||||||
|
padding: 15px 40px;
|
||||||
|
border-radius: 50px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1em;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-button:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
color: #4facfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.benefits-section {
|
||||||
|
padding: 60px 20px;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-icon {
|
||||||
|
font-size: 3em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-title {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-description {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.newsletter-section {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 60px 20px;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin: 50px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-content {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-content h2 {
|
||||||
|
font-size: 2.2em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-content p {
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-button {
|
||||||
|
padding: 15px 30px;
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-button:hover {
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-grid {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-image {
|
||||||
|
height: 140px;
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-content h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-form {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-input,
|
||||||
|
.newsletter-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
281
static/css/order_details.css
Normal file
281
static/css/order_details.css
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
.order-details-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-details-container h2 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-meta {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-meta p {
|
||||||
|
margin: 8px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-meta strong {
|
||||||
|
color: #333;
|
||||||
|
min-width: 120px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.order-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.items-section h3 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.4em;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-table thead {
|
||||||
|
background: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-table th {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-table tbody tr {
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-table tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-table td {
|
||||||
|
padding: 15px;
|
||||||
|
color: #495057;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-cell {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-cell,
|
||||||
|
.subtotal-cell {
|
||||||
|
text-align: right;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.order-summary {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 400px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
padding-top: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.order-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-print {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #17a2b8;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-print:hover {
|
||||||
|
background: #138496;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.no-items {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-items p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.product-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-variants {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.order-details-container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-table th,
|
||||||
|
.items-table td {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-cell {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-summary {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back,
|
||||||
|
.btn-print {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
280
static/css/product_detail.css
Normal file
280
static/css/product_detail.css
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
.product-detail-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-detail {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 50px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.product-gallery {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-image-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-gallery {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-container {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-container.active {
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2em;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px 0;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item strong {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.product-rating {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star.filled {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.review-count {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.price-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.description-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-section p {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.specifications {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-value {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.actions-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: white;
|
||||||
|
color: #007bff;
|
||||||
|
border: 2px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.related-products {
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-products h2 {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.product-detail {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-image-container {
|
||||||
|
height: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-meta,
|
||||||
|
.spec-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-section {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
441
static/css/products.css
Normal file
441
static/css/products.css
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
.products-page {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.filters-sidebar {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
height: fit-content;
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 2px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-filters {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-filters:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input,
|
||||||
|
.filter-select {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input:focus,
|
||||||
|
.filter-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-range {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-separator {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-apply-filters {
|
||||||
|
padding: 12px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-apply-filters:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.products-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-info h1 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-count {
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag {
|
||||||
|
background: #e9ecef;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag a {
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tag a:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-controls label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.products-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:hover .image-carousel {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-carousel {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-carousel img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
position: relative;
|
||||||
|
height: 280px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.product-card:hover .product-image img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.out-of-stock,
|
||||||
|
.low-stock {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.out-of-stock {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.low-stock {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-rating {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 12px;
|
||||||
|
left: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-stars {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #666;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-to-cart,
|
||||||
|
.btn-out-of-stock,
|
||||||
|
.btn-login-to-buy {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-to-cart {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-to-cart:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-out-of-stock {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login-to-buy {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login-to-buy:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.no-products {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 40px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-products-icon {
|
||||||
|
font-size: 4em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-products h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-products p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view-all {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-view-all:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
211
static/css/profile.css
Normal file
211
static/css/profile.css
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
.profile-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-container h2 {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.8em;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info strong {
|
||||||
|
color: #333;
|
||||||
|
min-width: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.orders-section h3 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-table thead {
|
||||||
|
background: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-table th {
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-table tbody tr {
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-table tbody tr:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-table td {
|
||||||
|
padding: 15px;
|
||||||
|
color: #495057;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-id {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-date {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-items {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-actions {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.view-details-link {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-details-link:hover {
|
||||||
|
background: #218838;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.no-orders {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-orders p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-now-link {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-now-link:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background: rgba(255, 193, 7, 0.1);
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-processing {
|
||||||
|
background: rgba(0, 123, 255, 0.1);
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-shipped {
|
||||||
|
background: rgba(23, 162, 184, 0.1);
|
||||||
|
color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-delivered {
|
||||||
|
background: rgba(40, 167, 69, 0.1);
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cancelled {
|
||||||
|
background: rgba(220, 53, 69, 0.1);
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.profile-container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-table th,
|
||||||
|
.orders-table td {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
207
static/css/styles.css
Normal file
207
static/css/styles.css
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
min-height: calc(100vh - 200px);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
background: #333;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.flash-messages {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash.danger {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flash.info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ced4da;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-1 {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-2 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-3 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-1 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-3 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-count {
|
||||||
|
background: #ff4444;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
static/images/default-product.jpg
Normal file
BIN
static/images/default-product.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
88
static/js/add_product.js
Normal file
88
static/js/add_product.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const imageInput = document.getElementById('image-input');
|
||||||
|
const imagePreview = document.getElementById('image-preview');
|
||||||
|
const uploadArea = document.getElementById('image-upload-area');
|
||||||
|
|
||||||
|
imageInput.addEventListener('change', function (e) {
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||||
|
uploadArea.addEventListener(eventName, preventDefaults, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
function preventDefaults(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
['dragenter', 'dragover'].forEach(eventName => {
|
||||||
|
uploadArea.addEventListener(eventName, highlight, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
['dragleave', 'drop'].forEach(eventName => {
|
||||||
|
uploadArea.addEventListener(eventName, unhighlight, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
function highlight() {
|
||||||
|
uploadArea.style.borderColor = '#007bff';
|
||||||
|
uploadArea.style.background = '#f8f9fa';
|
||||||
|
}
|
||||||
|
|
||||||
|
function unhighlight() {
|
||||||
|
uploadArea.style.borderColor = '#ccc';
|
||||||
|
uploadArea.style.background = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadArea.addEventListener('drop', handleDrop, false);
|
||||||
|
|
||||||
|
function handleDrop(e) {
|
||||||
|
const dt = e.dataTransfer;
|
||||||
|
const files = dt.files;
|
||||||
|
imageInput.files = files;
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
imagePreview.innerHTML = '';
|
||||||
|
const files = imageInput.files;
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
if (!file.type.match('image.*')) continue;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function (e) {
|
||||||
|
const previewItem = document.createElement('div');
|
||||||
|
previewItem.className = 'preview-item';
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = e.target.result;
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className = 'remove-image';
|
||||||
|
removeBtn.innerHTML = '×';
|
||||||
|
removeBtn.onclick = function () {
|
||||||
|
previewItem.remove();
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
const inputFiles = imageInput.files;
|
||||||
|
|
||||||
|
for (let j = 0; j < inputFiles.length; j++) {
|
||||||
|
if (j !== i) {
|
||||||
|
dt.items.add(inputFiles[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageInput.files = dt.files;
|
||||||
|
};
|
||||||
|
|
||||||
|
previewItem.appendChild(img);
|
||||||
|
previewItem.appendChild(removeBtn);
|
||||||
|
imagePreview.appendChild(previewItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
118
static/js/edit_product.js
Normal file
118
static/js/edit_product.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
let deletedImages = [];
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const imageInput = document.getElementById('image-input');
|
||||||
|
const imagePreview = document.getElementById('image-preview');
|
||||||
|
const uploadArea = document.getElementById('image-upload-area');
|
||||||
|
|
||||||
|
imageInput.addEventListener('change', function (e) {
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||||
|
uploadArea.addEventListener(eventName, preventDefaults, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
function preventDefaults(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
['dragenter', 'dragover'].forEach(eventName => {
|
||||||
|
uploadArea.addEventListener(eventName, highlight, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
['dragleave', 'drop'].forEach(eventName => {
|
||||||
|
uploadArea.addEventListener(eventName, unhighlight, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
function highlight() {
|
||||||
|
uploadArea.style.borderColor = '#007bff';
|
||||||
|
uploadArea.style.background = '#f8f9fa';
|
||||||
|
}
|
||||||
|
|
||||||
|
function unhighlight() {
|
||||||
|
uploadArea.style.borderColor = '#ccc';
|
||||||
|
uploadArea.style.background = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadArea.addEventListener('drop', handleDrop, false);
|
||||||
|
|
||||||
|
function handleDrop(e) {
|
||||||
|
const dt = e.dataTransfer;
|
||||||
|
const files = dt.files;
|
||||||
|
imageInput.files = files;
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
imagePreview.innerHTML = '';
|
||||||
|
const files = imageInput.files;
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
if (!file.type.match('image.*')) continue;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function (e) {
|
||||||
|
const previewItem = document.createElement('div');
|
||||||
|
previewItem.className = 'preview-item';
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = e.target.result;
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className = 'remove-image';
|
||||||
|
removeBtn.innerHTML = '×';
|
||||||
|
removeBtn.onclick = function () {
|
||||||
|
previewItem.remove();
|
||||||
|
const dt = new DataTransfer();
|
||||||
|
const inputFiles = imageInput.files;
|
||||||
|
|
||||||
|
for (let j = 0; j < inputFiles.length; j++) {
|
||||||
|
if (j !== i) {
|
||||||
|
dt.items.add(inputFiles[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageInput.files = dt.files;
|
||||||
|
};
|
||||||
|
|
||||||
|
previewItem.appendChild(img);
|
||||||
|
previewItem.appendChild(removeBtn);
|
||||||
|
imagePreview.appendChild(previewItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function deleteImage(imageId) {
|
||||||
|
if (confirm('Delete this image?')) {
|
||||||
|
deletedImages.push(imageId);
|
||||||
|
document.getElementById('deleted-images').value = JSON.stringify(deletedImages);
|
||||||
|
|
||||||
|
const imageItem = document.querySelector('.image-item[data-id="' + imageId + '"]');
|
||||||
|
if (imageItem) {
|
||||||
|
imageItem.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/admin/image/delete/' + imageId, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({})
|
||||||
|
}).then(function (response) {
|
||||||
|
return response.json();
|
||||||
|
}).then(function (data) {
|
||||||
|
if (!data.success) {
|
||||||
|
alert('Error deleting image: ' + data.error);
|
||||||
|
}
|
||||||
|
}).catch(function (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
24
static/js/home.js
Normal file
24
static/js/home.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
document.querySelector('.newsletter-form').addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const email = this.querySelector('.newsletter-input').value;
|
||||||
|
|
||||||
|
alert(`Thank you for subscribing with: ${email}\nYou'll hear from us soon!`);
|
||||||
|
this.querySelector('.newsletter-input').value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.product-card-home').forEach(card => {
|
||||||
|
card.addEventListener('mouseenter', function () {
|
||||||
|
const img = this.querySelector('img');
|
||||||
|
if (img) {
|
||||||
|
img.style.transform = 'scale(1.05)';
|
||||||
|
img.style.transition = 'transform 0.3s ease';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('mouseleave', function () {
|
||||||
|
const img = this.querySelector('img');
|
||||||
|
if (img) {
|
||||||
|
img.style.transform = 'scale(1)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
47
static/js/login.js
Normal file
47
static/js/login.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
function togglePassword(inputId) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const button = event.target;
|
||||||
|
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text';
|
||||||
|
button.textContent = '🙈';
|
||||||
|
} else {
|
||||||
|
input.type = 'password';
|
||||||
|
button.textContent = '👁️';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const form = document.querySelector('.auth-form');
|
||||||
|
const emailInput = document.getElementById('email');
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
|
||||||
|
[emailInput, passwordInput].forEach(input => {
|
||||||
|
input.addEventListener('focus', function () {
|
||||||
|
this.parentElement.classList.add('focused');
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function () {
|
||||||
|
this.parentElement.classList.remove('focused');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener('submit', function (e) {
|
||||||
|
const email = emailInput.value.trim();
|
||||||
|
const password = passwordInput.value.trim();
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Please fill in all fields');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.includes('@')) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Please enter a valid email address');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
70
static/js/product_detail.js
Normal file
70
static/js/product_detail.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
function changeImage(thumbnailElement) {
|
||||||
|
const imageUrl = thumbnailElement.getAttribute('data-image-url');
|
||||||
|
|
||||||
|
document.getElementById('mainImage').src = imageUrl;
|
||||||
|
|
||||||
|
document.querySelectorAll('.thumbnail-container').forEach(thumb => {
|
||||||
|
thumb.classList.remove('active');
|
||||||
|
});
|
||||||
|
thumbnailElement.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteImage(imageId) {
|
||||||
|
if (confirm('Are you sure you want to delete this image?')) {
|
||||||
|
fetch(`/admin/delete-image/${imageId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const imageElement = document.querySelector(`[data-image-id="${imageId}"]`);
|
||||||
|
if (imageElement) {
|
||||||
|
imageElement.remove();
|
||||||
|
}
|
||||||
|
alert('Image deleted successfully');
|
||||||
|
} else {
|
||||||
|
alert('Failed to delete image');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error deleting image');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const thumbnails = document.querySelectorAll('.thumbnail-container');
|
||||||
|
|
||||||
|
thumbnails.forEach((thumb, index) => {
|
||||||
|
thumb.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
changeImage(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'ArrowRight') {
|
||||||
|
e.preventDefault();
|
||||||
|
const next = thumbnails[index + 1];
|
||||||
|
if (next) {
|
||||||
|
next.focus();
|
||||||
|
changeImage(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'ArrowLeft') {
|
||||||
|
e.preventDefault();
|
||||||
|
const prev = thumbnails[index - 1];
|
||||||
|
if (prev) {
|
||||||
|
prev.focus();
|
||||||
|
changeImage(prev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
thumb.setAttribute('tabindex', '0');
|
||||||
|
});
|
||||||
|
});
|
||||||
3
static/js/products.js
Normal file
3
static/js/products.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
document.getElementById('sort').addEventListener('change', function () {
|
||||||
|
this.closest('form').submit();
|
||||||
|
});
|
||||||
159
static/js/register.js
Normal file
159
static/js/register.js
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
function togglePassword(inputId) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const button = event.target;
|
||||||
|
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text';
|
||||||
|
button.textContent = '🙈';
|
||||||
|
} else {
|
||||||
|
input.type = 'password';
|
||||||
|
button.textContent = '👁️';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPasswordStrength(password) {
|
||||||
|
const strengthBar = document.getElementById('strengthBar');
|
||||||
|
const hints = {
|
||||||
|
lengthHint: document.getElementById('lengthHint'),
|
||||||
|
uppercaseHint: document.getElementById('uppercaseHint'),
|
||||||
|
lowercaseHint: document.getElementById('lowercaseHint'),
|
||||||
|
numberHint: document.getElementById('numberHint'),
|
||||||
|
specialHint: document.getElementById('specialHint')
|
||||||
|
};
|
||||||
|
|
||||||
|
let strength = 0;
|
||||||
|
let messages = [];
|
||||||
|
|
||||||
|
if (password.length >= 8) {
|
||||||
|
strength += 20;
|
||||||
|
hints.lengthHint.style.color = '#28a745';
|
||||||
|
hints.lengthHint.style.textDecoration = 'line-through';
|
||||||
|
} else {
|
||||||
|
hints.lengthHint.style.color = '#666';
|
||||||
|
hints.lengthHint.style.textDecoration = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[A-Z]/.test(password)) {
|
||||||
|
strength += 20;
|
||||||
|
hints.uppercaseHint.style.color = '#28a745';
|
||||||
|
hints.uppercaseHint.style.textDecoration = 'line-through';
|
||||||
|
} else {
|
||||||
|
hints.uppercaseHint.style.color = '#666';
|
||||||
|
hints.uppercaseHint.style.textDecoration = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[a-z]/.test(password)) {
|
||||||
|
strength += 20;
|
||||||
|
hints.lowercaseHint.style.color = '#28a745';
|
||||||
|
hints.lowercaseHint.style.textDecoration = 'line-through';
|
||||||
|
} else {
|
||||||
|
hints.lowercaseHint.style.color = '#666';
|
||||||
|
hints.lowercaseHint.style.textDecoration = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[0-9]/.test(password)) {
|
||||||
|
strength += 20;
|
||||||
|
hints.numberHint.style.color = '#28a745';
|
||||||
|
hints.numberHint.style.textDecoration = 'line-through';
|
||||||
|
} else {
|
||||||
|
hints.numberHint.style.color = '#666';
|
||||||
|
hints.numberHint.style.textDecoration = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[^A-Za-z0-9]/.test(password)) {
|
||||||
|
strength += 20;
|
||||||
|
hints.specialHint.style.color = '#28a745';
|
||||||
|
hints.specialHint.style.textDecoration = 'line-through';
|
||||||
|
} else {
|
||||||
|
hints.specialHint.style.color = '#666';
|
||||||
|
hints.specialHint.style.textDecoration = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
strengthBar.className = 'strength-bar';
|
||||||
|
if (strength <= 25) {
|
||||||
|
strengthBar.classList.add('strength-weak');
|
||||||
|
} else if (strength <= 50) {
|
||||||
|
strengthBar.classList.add('strength-medium');
|
||||||
|
} else if (strength <= 75) {
|
||||||
|
strengthBar.classList.add('strength-strong');
|
||||||
|
} else {
|
||||||
|
strengthBar.classList.add('strength-very-strong');
|
||||||
|
}
|
||||||
|
strengthBar.style.width = strength + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPasswordMatch() {
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const confirmPassword = document.getElementById('confirm_password').value;
|
||||||
|
const matchElement = document.getElementById('passwordMatch');
|
||||||
|
|
||||||
|
if (!confirmPassword) {
|
||||||
|
matchElement.textContent = '';
|
||||||
|
matchElement.style.color = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password === confirmPassword) {
|
||||||
|
matchElement.textContent = '✓ Passwords match';
|
||||||
|
matchElement.style.color = '#28a745';
|
||||||
|
} else {
|
||||||
|
matchElement.textContent = '✗ Passwords do not match';
|
||||||
|
matchElement.style.color = '#dc3545';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const form = document.querySelector('.auth-form');
|
||||||
|
const emailInput = document.getElementById('email');
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
const confirmInput = document.getElementById('confirm_password');
|
||||||
|
const termsCheckbox = document.getElementById('terms');
|
||||||
|
|
||||||
|
[emailInput, passwordInput, confirmInput].forEach(input => {
|
||||||
|
input.addEventListener('focus', function () {
|
||||||
|
this.parentElement.classList.add('focused');
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function () {
|
||||||
|
this.parentElement.classList.remove('focused');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener('submit', function (e) {
|
||||||
|
const email = emailInput.value.trim();
|
||||||
|
const password = passwordInput.value.trim();
|
||||||
|
const confirmPassword = confirmInput.value.trim();
|
||||||
|
|
||||||
|
if (!email || !password || !confirmPassword) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Please fill in all fields');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.includes('@')) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Please enter a valid email address');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Password must be at least 8 characters long');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Passwords do not match');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!termsCheckbox.checked) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('You must agree to the Terms of Service');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
131
templates/admin/add_product.html
Normal file
131
templates/admin/add_product.html
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/add_product.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/add_product.js') }}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="admin-container">
|
||||||
|
<h2>Add New Product</h2>
|
||||||
|
<form method="POST" enctype="multipart/form-data" class="product-form">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Basic Information</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Product Name *</label>
|
||||||
|
<input type="text" id="name" name="name" required class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sku">SKU (Stock Keeping Unit)</label>
|
||||||
|
<input type="text" id="sku" name="sku" class="form-control" placeholder="e.g., TSHIRT-RED-M">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea id="description" name="description" class="form-control" rows="4"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="price">Price ($) *</label>
|
||||||
|
<input type="number" step="0.01" id="price" name="price" required class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="stock">Stock Quantity *</label>
|
||||||
|
<input type="number" id="stock" name="stock" value="0" min="0" required class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Product Details</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category_id">Category *</label>
|
||||||
|
<select id="category_id" name="category_id" class="form-control">
|
||||||
|
<option value="">-- Select Category --</option>
|
||||||
|
{% for c in categories %}
|
||||||
|
<option value="{{ c.id }}">{{ c.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="material">Material</label>
|
||||||
|
<input type="text" id="material" name="material" class="form-control" placeholder="e.g., Cotton, Polyester">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="size">Size</label>
|
||||||
|
<select id="size" name="size" class="form-control">
|
||||||
|
<option value="">-- Select Size --</option>
|
||||||
|
{% for size in sizes %}
|
||||||
|
<option value="{{ size }}">{{ size }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="color">Color</label>
|
||||||
|
<input type="text" id="color" name="color" class="form-control" placeholder="e.g., Red, Blue">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="company">Brand/Company</label>
|
||||||
|
<input type="text" id="company" name="company" class="form-control" placeholder="e.g., Nike, Adidas">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="weight">Weight (kg)</label>
|
||||||
|
<input type="number" step="0.01" id="weight" name="weight" class="form-control" placeholder="0.5">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dimensions">Dimensions</label>
|
||||||
|
<input type="text" id="dimensions" name="dimensions" class="form-control" placeholder="10x5x3 cm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Product Images</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Upload Images *</label>
|
||||||
|
<div class="image-upload-area" id="image-upload-area">
|
||||||
|
<div class="upload-prompt">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="17 8 12 3 7 8"></polyline>
|
||||||
|
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||||
|
</svg>
|
||||||
|
<p>Click or drag images here</p>
|
||||||
|
<p class="upload-hint">First image will be primary</p>
|
||||||
|
</div>
|
||||||
|
<input type="file" name="images[]" multiple accept="image/*" class="image-input" id="image-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="image-preview" id="image-preview"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Save Product</button>
|
||||||
|
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
72
templates/admin/categories.html
Normal file
72
templates/admin/categories.html
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/categories.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="admin-container">
|
||||||
|
<h2>Manage Categories</h2>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Add New Category</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="{{ url_for('admin.add_category') }}" class="row g-3">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<input type="text" name="name" class="form-control" placeholder="Category name" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Add Category</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>All Categories ({{ categories|length }})</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if categories %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Products</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for category in categories %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ category.id }}</td>
|
||||||
|
<td>{{ category.name }}</td>
|
||||||
|
<td>{{ category.products|length }}</td>
|
||||||
|
<td>
|
||||||
|
{% if category.products|length == 0 %}
|
||||||
|
<form method="POST" action="{{ url_for('admin.delete_category', id=category.id) }}"
|
||||||
|
style="display: inline;" onsubmit="return confirm('Delete category {{ category.name }}?')">
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-sm btn-secondary" disabled title="Cannot delete category with products">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">No categories found.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
54
templates/admin/dashboard.html
Normal file
54
templates/admin/dashboard.html
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Admin Dashboard</h2>
|
||||||
|
<a href="{{ url_for('admin.add_product') }}" class="btn btn-success">Add New Product</a>
|
||||||
|
<table class="admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Image</th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Stock</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in products %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<img src="{{ p.get_image_url() }}" alt="{{ p.name }}" style="width: 60px; height: 60px; object-fit: cover;">
|
||||||
|
</td>
|
||||||
|
<td>{{ p.id }}</td>
|
||||||
|
<td>{{ p.name }}</td>
|
||||||
|
<td>{{ p.description[:50] }}{% if p.description|length > 50 %}...{% endif %}</td>
|
||||||
|
<td>${{ "%.2f"|format(p.price) }}</td>
|
||||||
|
<td>
|
||||||
|
{% if p.stock > 10 %}
|
||||||
|
<span class="stock-high">{{ p.stock }}</span>
|
||||||
|
{% elif p.stock > 0 %}
|
||||||
|
<span class="stock-low">{{ p.stock }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="stock-out">Out of Stock</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ p.category.name if p.category else 'N/A' }}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<a href="{{ url_for('admin.edit_product', id=p.id) }}" class="btn btn-sm btn-primary">Edit</a>
|
||||||
|
<form method="POST" action="{{ url_for('admin.delete_product', id=p.id) }}" style="display:inline;"
|
||||||
|
onsubmit="return confirm('Delete this product?')">
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
169
templates/admin/edit_product.html
Normal file
169
templates/admin/edit_product.html
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/edit_product.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/edit_product.js') }}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="admin-container">
|
||||||
|
<h2>Edit Product: {{ product.name }}</h2>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data" class="product-form" id="product-form">
|
||||||
|
<div class="form-grid">
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Basic Information</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Product Name *</label>
|
||||||
|
<input type="text" id="name" name="name" value="{{ product.name }}" required class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sku">SKU (Stock Keeping Unit)</label>
|
||||||
|
<input type="text" id="sku" name="sku" value="{{ product.sku or '' }}" class="form-control"
|
||||||
|
placeholder="e.g., TSHIRT-RED-M">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea id="description" name="description" class="form-control"
|
||||||
|
rows="4">{{ product.description or '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="price">Price ($) *</label>
|
||||||
|
<input type="number" step="0.01" id="price" name="price" value="{{ product.price }}" required
|
||||||
|
class="form-control" min="0.01">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="stock">Stock Quantity *</label>
|
||||||
|
<input type="number" id="stock" name="stock" value="{{ product.stock }}" min="0" required
|
||||||
|
class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Product Details</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category_id">Category *</label>
|
||||||
|
<select id="category_id" name="category_id" class="form-control" required>
|
||||||
|
<option value="">-- Select Category --</option>
|
||||||
|
{% for c in categories %}
|
||||||
|
<option value="{{ c.id }}" {% if product.category_id==c.id %}selected{% endif %}>
|
||||||
|
{{ c.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="material">Material</label>
|
||||||
|
<input type="text" id="material" name="material" value="{{ product.material or '' }}" class="form-control"
|
||||||
|
placeholder="e.g., Cotton, Polyester, Denim">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="size">Size</label>
|
||||||
|
<select id="size" name="size" class="form-control">
|
||||||
|
<option value="">-- Select Size --</option>
|
||||||
|
{% for size in sizes %}
|
||||||
|
<option value="{{ size }}" {% if product.size==size %}selected{% endif %}>
|
||||||
|
{{ size }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="color">Color</label>
|
||||||
|
<input type="text" id="color" name="color" value="{{ product.color or '' }}" class="form-control"
|
||||||
|
placeholder="e.g., Red, Blue, Black">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="company">Brand/Company</label>
|
||||||
|
<input type="text" id="company" name="company" value="{{ product.company or '' }}" class="form-control"
|
||||||
|
placeholder="e.g., Nike, Adidas, Levi's">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="weight">Weight (kg)</label>
|
||||||
|
<input type="number" step="0.01" id="weight" name="weight" value="{{ product.weight or '' }}"
|
||||||
|
class="form-control" placeholder="0.5" min="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="dimensions">Dimensions</label>
|
||||||
|
<input type="text" id="dimensions" name="dimensions" value="{{ product.dimensions or '' }}"
|
||||||
|
class="form-control" placeholder="10x5x3 cm">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Current Images</h3>
|
||||||
|
<div class="current-images">
|
||||||
|
{% if product.images %}
|
||||||
|
<div class="image-grid">
|
||||||
|
{% for image in product.images %}
|
||||||
|
<div class="image-item" data-id="{{ image.id }}">
|
||||||
|
<img src="/static/uploads/products/{{ image.filename }}" alt="Product Image {{ loop.index }}">
|
||||||
|
<div class="image-actions">
|
||||||
|
<label class="primary-checkbox">
|
||||||
|
<input type="radio" name="primary_image" value="{{ image.id }}" {% if image.is_primary %}checked{%
|
||||||
|
endif %}>
|
||||||
|
Primary
|
||||||
|
</label>
|
||||||
|
<button type="button" class="btn-delete-image" onclick="deleteImage('{{ image.id }}')">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="no-images">No images uploaded yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-top: 30px;">Add New Images</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="image-upload-area" id="image-upload-area">
|
||||||
|
<div class="upload-prompt">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
|
<polyline points="17 8 12 3 7 8"></polyline>
|
||||||
|
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||||
|
</svg>
|
||||||
|
<p>Click or drag images here</p>
|
||||||
|
</div>
|
||||||
|
<input type="file" name="images[]" multiple accept="image/*" class="image-input" id="image-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="image-preview" id="image-preview"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="deleted_images" id="deleted-images" value="">
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Update Product</button>
|
||||||
|
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-secondary">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
61
templates/base.html
Normal file
61
templates/base.html
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>WearWell Shop{% block title %}{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" />
|
||||||
|
{% block styles %}{% endblock %}
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<a href="{{ url_for('main.home') }}">Home</a>
|
||||||
|
<a href="{{ url_for('main.products') }}">Products</a>
|
||||||
|
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<a href="{{ url_for('cart.view_cart') }}">
|
||||||
|
Cart {% if current_user.cart and current_user.cart.items|length > 0 %}
|
||||||
|
<span class="cart-count">{{ current_user.cart.items|length }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('user.profile') }}">My Profile</a>
|
||||||
|
|
||||||
|
{% if current_user.is_admin %}
|
||||||
|
<a href="{{ url_for('admin.dashboard') }}">Admin Panel</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="{{ url_for('auth.logout') }}">Logout</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('auth.login') }}">Login</a>
|
||||||
|
<a href="{{ url_for('auth.register') }}">Register</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
|
||||||
|
<div class="flash-messages">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="flash {{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %} {% endwith %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="container text-center">
|
||||||
|
<p>© 2025 WearWell Shop. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
114
templates/cart.html
Normal file
114
templates/cart.html
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/cart.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="cart-container">
|
||||||
|
<h1>Your Shopping Cart</h1>
|
||||||
|
|
||||||
|
{% if items %}
|
||||||
|
<div class="cart-items">
|
||||||
|
<table class="cart-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Product</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Subtotal</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in items %}
|
||||||
|
<tr>
|
||||||
|
<td class="product-info">
|
||||||
|
<div class="product-image">
|
||||||
|
<img src="{{ item.product.get_image_url() }}" alt="{{ item.product.name }}">
|
||||||
|
</div>
|
||||||
|
<div class="product-details">
|
||||||
|
<h4>{{ item.product.name }}</h4>
|
||||||
|
<p class="product-description">{{ item.product.description[:80] }}...</p>
|
||||||
|
<div class="stock-info">
|
||||||
|
{% if item.product.stock > 10 %}
|
||||||
|
<span class="in-stock">✓ In Stock</span>
|
||||||
|
{% elif item.product.stock > 0 %}
|
||||||
|
<span class="low-stock">⚠ Low Stock ({{ item.product.stock }} left)</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="out-of-stock">✗ Out of Stock</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="price">${{ "%.2f"|format(item.product.price) }}</td>
|
||||||
|
<td class="quantity">
|
||||||
|
<form method="POST" action="{{ url_for('cart.update_cart', item_id=item.id) }}" class="quantity-form">
|
||||||
|
<input type="number" name="quantity" value="{{ item.quantity }}" min="1" max="{{ item.max_quantity }}"
|
||||||
|
class="quantity-input">
|
||||||
|
<button type="submit" class="btn-update">Update</button>
|
||||||
|
<div class="max-quantity">
|
||||||
|
Max: {{ item.max_quantity }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td class="subtotal">${{ "%.2f"|format(item.subtotal) }}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<form method="POST" action="{{ url_for('cart.remove_from_cart', item_id=item.id) }}"
|
||||||
|
onsubmit="return confirm('Remove {{ item.product.name }} from cart?')">
|
||||||
|
<button type="submit" class="btn-remove">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="cart-summary">
|
||||||
|
<div class="summary-card">
|
||||||
|
<h3>Order Summary</h3>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>Subtotal:</span>
|
||||||
|
<span>${{ "%.2f"|format(total) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span>Shipping:</span>
|
||||||
|
<span>Free</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row total">
|
||||||
|
<span>Total:</span>
|
||||||
|
<span>${{ "%.2f"|format(total) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cart-actions">
|
||||||
|
<a href="{{ url_for('main.products') }}" class="btn-continue">
|
||||||
|
← Continue Shopping
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('cart.clear_cart') }}" style="display: inline;">
|
||||||
|
<button type="submit" class="btn-clear" onclick="return confirm('Clear entire cart?')">
|
||||||
|
Clear Cart
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<a href="{{ url_for('cart.checkout') }}" class="btn-checkout">
|
||||||
|
Proceed to Checkout →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-cart">
|
||||||
|
<div class="empty-icon">🛒</div>
|
||||||
|
<h3>Your cart is empty</h3>
|
||||||
|
<p>Add some products to your cart and they will appear here.</p>
|
||||||
|
<a href="{{ url_for('main.products') }}" class="btn-shop">
|
||||||
|
Browse Products
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
71
templates/checkout.html
Normal file
71
templates/checkout.html
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/checkout.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="checkout-container">
|
||||||
|
<h1>Checkout</h1>
|
||||||
|
|
||||||
|
<div class="checkout-content">
|
||||||
|
<div class="order-summary">
|
||||||
|
<h3>Order Summary</h3>
|
||||||
|
{% for item in cart.items %}
|
||||||
|
<div class="order-item">
|
||||||
|
<div class="item-info">
|
||||||
|
<h4>{{ item.product.name }}</h4>
|
||||||
|
<p>Quantity: {{ item.quantity }} × ${{ "%.2f"|format(item.product.price) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="item-total">
|
||||||
|
${{ "%.2f"|format(item.product.price * item.quantity) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="order-total">
|
||||||
|
<h3>Total: ${{ "%.2f"|format(total) }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkout-form">
|
||||||
|
<h3>Shipping Information</h3>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Full Name:</label>
|
||||||
|
<input type="text" name="name" required class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Email:</label>
|
||||||
|
<input type="email" name="email" value="{{ current_user.email }}" required class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Address:</label>
|
||||||
|
<textarea name="address" required class="form-control" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>City:</label>
|
||||||
|
<input type="text" name="city" required class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Postal Code:</label>
|
||||||
|
<input type="text" name="postal_code" required class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="{{ url_for('cart.view_cart') }}" class="btn-back">
|
||||||
|
← Back to Cart
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn-confirm">
|
||||||
|
Place Order
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
229
templates/home.html
Normal file
229
templates/home.html
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/home.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/home.js') }}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="hero-content">
|
||||||
|
<h1 class="hero-title">Welcome to WearWell Shop</h1>
|
||||||
|
<p class="hero-subtitle">Discover premium fashion for every occasion. Quality clothing, unbeatable prices, and style
|
||||||
|
that lasts.</p>
|
||||||
|
|
||||||
|
<div class="hero-search">
|
||||||
|
<form action="{{ url_for('main.products') }}" method="GET">
|
||||||
|
<input type="text" name="q" placeholder="Search for products, brands, or categories..."
|
||||||
|
class="hero-search-input">
|
||||||
|
<button type="submit" class="hero-search-button">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="categories-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Shop by Category</h2>
|
||||||
|
<a href="{{ url_for('main.products') }}" class="view-all-link">
|
||||||
|
View All Categories →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="categories-grid">
|
||||||
|
{% for category in categories[:6] %}
|
||||||
|
<a href="{{ url_for('main.products', category=category.id) }}" class="category-card">
|
||||||
|
<div class="category-image">
|
||||||
|
{% if category.name == "T-Shirts" %}👕
|
||||||
|
{% elif category.name == "Jeans" %}👖
|
||||||
|
{% elif category.name == "Jackets" %}🧥
|
||||||
|
{% elif category.name == "Shoes" %}👟
|
||||||
|
{% elif category.name == "Accessories" %}👜
|
||||||
|
{% elif category.name == "Dresses" %}👗
|
||||||
|
{% elif category.name == "Shirts" %}👔
|
||||||
|
{% elif category.name == "Hoodies" %}🧢
|
||||||
|
{% else %}👚{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="category-name">
|
||||||
|
{{ category.name }}
|
||||||
|
<span class="category-count">{{ category.products|length }} items</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="featured-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Featured Products</h2>
|
||||||
|
<a href="{{ url_for('main.products') }}" class="view-all-link">
|
||||||
|
View All Products →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="products-grid">
|
||||||
|
{% for product in featured_products[:8] %}
|
||||||
|
<div class="product-card-home">
|
||||||
|
{% if product.stock <= 5 and product.stock> 0 %}
|
||||||
|
<span class="product-badge">Low Stock</span>
|
||||||
|
{% elif product.stock == 0 %}
|
||||||
|
<span class="product-badge" style="background: #666;">Out of Stock</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="{{ url_for('main.product_detail', id=product.id) }}">
|
||||||
|
<div class="product-image-home">
|
||||||
|
<img src="{{ product.get_image_url() }}" alt="{{ product.name }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-info-home">
|
||||||
|
<h3 class="product-title-home">{{ product.name }}</h3>
|
||||||
|
|
||||||
|
<div class="product-price-home">${{ "%.2f"|format(product.price) }}</div>
|
||||||
|
|
||||||
|
{% if product.average_rating > 0 %}
|
||||||
|
<div class="product-rating-home">
|
||||||
|
<div class="stars">
|
||||||
|
{% for i in range(5) %}
|
||||||
|
<span>{% if i < product.average_rating|int %}★{% else %}☆{% endif %}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<span class="rating-count">({{ product.review_count }})</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="product-meta-home">
|
||||||
|
{% if product.company %}
|
||||||
|
<span>{{ product.company }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if product.color %}
|
||||||
|
<span>{{ product.color }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="promo-banner">
|
||||||
|
<div class="promo-content">
|
||||||
|
<h2>Summer Sale is Here! 🌞</h2>
|
||||||
|
<p>Get up to 50% off on selected items. Limited time offer!</p>
|
||||||
|
<a href="{{ url_for('main.products') }}" class="promo-button">
|
||||||
|
Shop Now →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="benefits-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Why Shop With Us?</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefits-grid">
|
||||||
|
<div class="benefit-card">
|
||||||
|
<span class="benefit-icon">🚚</span>
|
||||||
|
<h3 class="benefit-title">Free Shipping</h3>
|
||||||
|
<p class="benefit-description">Free delivery on all orders over $50. Fast and reliable shipping nationwide.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefit-card">
|
||||||
|
<span class="benefit-icon">🔄</span>
|
||||||
|
<h3 class="benefit-title">Easy Returns</h3>
|
||||||
|
<p class="benefit-description">30-day return policy. If you're not satisfied, we'll make it right.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefit-card">
|
||||||
|
<span class="benefit-icon">🔒</span>
|
||||||
|
<h3 class="benefit-title">Secure Payment</h3>
|
||||||
|
<p class="benefit-description">Your payment information is protected with bank-level security.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefit-card">
|
||||||
|
<span class="benefit-icon">⭐</span>
|
||||||
|
<h3 class="benefit-title">Quality Guarantee</h3>
|
||||||
|
<p class="benefit-description">Premium materials and craftsmanship in every product we sell.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="featured-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2 class="section-title">Best Sellers</h2>
|
||||||
|
<a href="{{ url_for('main.products') }}" class="view-all-link">
|
||||||
|
View More →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="products-grid">
|
||||||
|
{% for product in best_sellers[:4] %}
|
||||||
|
<div class="product-card-home">
|
||||||
|
{% if product.stock <= 5 and product.stock> 0 %}
|
||||||
|
<span class="product-badge">Popular</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="{{ url_for('main.product_detail', id=product.id) }}">
|
||||||
|
<div class="product-image-home">
|
||||||
|
<img src="{{ product.get_image_url() }}" alt="{{ product.name }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-info-home">
|
||||||
|
<h3 class="product-title-home">{{ product.name }}</h3>
|
||||||
|
|
||||||
|
<div class="product-price-home">${{ "%.2f"|format(product.price) }}</div>
|
||||||
|
|
||||||
|
{% if product.average_rating > 0 %}
|
||||||
|
<div class="product-rating-home">
|
||||||
|
<div class="stars">
|
||||||
|
{% for i in range(5) %}
|
||||||
|
<span>{% if i < product.average_rating|int %}★{% else %}☆{% endif %}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<span class="rating-count">({{ product.review_count }})</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="product-meta-home">
|
||||||
|
<span>{{ product.stock }} in stock</span>
|
||||||
|
<span>{{ product.like_count }} likes</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="newsletter-section">
|
||||||
|
<div class="newsletter-content">
|
||||||
|
<h2>Stay in the Loop</h2>
|
||||||
|
<p>Subscribe to our newsletter for exclusive deals, new arrivals, and style tips.</p>
|
||||||
|
|
||||||
|
<form class="newsletter-form">
|
||||||
|
<input type="email" placeholder="Enter your email address" class="newsletter-input" required>
|
||||||
|
<button type="submit" class="newsletter-button">Subscribe</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p style="margin-top: 15px; font-size: 0.9em; opacity: 0.8;">
|
||||||
|
By subscribing, you agree to our Privacy Policy and consent to receive updates.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
91
templates/login.html
Normal file
91
templates/login.html
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="auth-container">
|
||||||
|
<h2>Welcome Back</h2>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="flash-messages">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="flash {{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" class="auth-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email Address</label>
|
||||||
|
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<div class="password-container">
|
||||||
|
<input type="password" id="password" name="password" class="form-control" placeholder="Enter your password"
|
||||||
|
required>
|
||||||
|
<button type="button" class="toggle-password" onclick="togglePassword('password')">
|
||||||
|
👁️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="remember-me">
|
||||||
|
<input type="checkbox" id="remember" name="remember">
|
||||||
|
<label for="remember">Remember me</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-submit">
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ url_for('auth.register') }}" class="auth-link">
|
||||||
|
Don't have an account? Register here
|
||||||
|
</a>
|
||||||
|
<a href="#" class="auth-link">
|
||||||
|
Forgot your password?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="social-login">
|
||||||
|
<div class="social-divider">
|
||||||
|
<span>Or continue with</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="social-buttons">
|
||||||
|
<button type="button" class="btn-social google">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
||||||
|
</svg>
|
||||||
|
Google
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="btn-social facebook">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
|
||||||
|
</svg>
|
||||||
|
Facebook
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
93
templates/order_details.html
Normal file
93
templates/order_details.html
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/order_details.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="order-details-container">
|
||||||
|
<h2>Order #{{ order.id }}</h2>
|
||||||
|
|
||||||
|
<div class="order-meta">
|
||||||
|
<p><strong>Order Date:</strong> {{ order.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
|
||||||
|
<div class="order-status">
|
||||||
|
<span class="status-label">Status:</span>
|
||||||
|
<span class="status-badge status-{{ order.status }}">
|
||||||
|
{{ order.status|title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="items-section">
|
||||||
|
<h3>Order Items</h3>
|
||||||
|
|
||||||
|
{% if order.items %}
|
||||||
|
<table class="items-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Product</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Subtotal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in order.items %}
|
||||||
|
<tr>
|
||||||
|
<td class="product-cell">
|
||||||
|
<div class="product-image">
|
||||||
|
<img src="{{ item.product.get_image_url() }}" alt="{{ item.product.name }}">
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<div class="product-name">{{ item.product.name }}</div>
|
||||||
|
{% if item.selected_size or item.selected_color %}
|
||||||
|
<div class="product-variants">
|
||||||
|
{% if item.selected_size %}Size: {{ item.selected_size }}{% endif %}
|
||||||
|
{% if item.selected_color %} | Color: {{ item.selected_color }}{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="quantity-cell">{{ item.quantity }}</td>
|
||||||
|
<td class="price-cell">${{ "%.2f"|format(item.product.price) }}</td>
|
||||||
|
<td class="subtotal-cell">${{ "%.2f"|format(item.product.price * item.quantity) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="order-summary">
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Subtotal:</span>
|
||||||
|
<span class="summary-value">
|
||||||
|
${{ "%.2f"|format(order.items|sum(attribute='product.price') * order.items|sum(attribute='quantity')) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Shipping:</span>
|
||||||
|
<span class="summary-value">Free</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="summary-label">Total:</span>
|
||||||
|
<span class="summary-value">
|
||||||
|
${{ "%.2f"|format(order.items|sum(attribute='product.price') * order.items|sum(attribute='quantity')) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="no-items">
|
||||||
|
<p>No items in this order.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-actions">
|
||||||
|
<a href="{{ url_for('user.profile') }}" class="btn-back">
|
||||||
|
← Back to My Orders
|
||||||
|
</a>
|
||||||
|
<button onclick="window.print()" class="btn-print">
|
||||||
|
📄 Print Order
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
178
templates/product_detail.html
Normal file
178
templates/product_detail.html
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/product_detail.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/product_detail.js') }}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="product-detail-container">
|
||||||
|
|
||||||
|
<div class="product-detail">
|
||||||
|
|
||||||
|
<div class="product-gallery">
|
||||||
|
|
||||||
|
<div class="main-image-container">
|
||||||
|
<img id="mainImage" src="{{ product.get_image_url() }}" alt="{{ product.name }}" class="main-image">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if product.images|length > 0 %}
|
||||||
|
<div class="thumbnail-gallery">
|
||||||
|
{% for image in product.images|sort(attribute='display_order') %}
|
||||||
|
<div class="thumbnail-container {% if loop.first %}active{% endif %}"
|
||||||
|
data-image-url="{{ url_for('static', filename='uploads/products/' + image.filename) }}"
|
||||||
|
onclick="changeImage(this)">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/products/' + image.filename) }}"
|
||||||
|
alt="{{ product.name }} - Image {{ loop.index }}" class="thumbnail">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-info">
|
||||||
|
<h1>{{ product.name }}</h1>
|
||||||
|
|
||||||
|
<div class="stock-status">
|
||||||
|
{% if product.stock > 10 %}
|
||||||
|
<span class="in-stock">✓ In Stock ({{ product.stock }} available)</span>
|
||||||
|
{% elif product.stock > 0 %}
|
||||||
|
<span class="low-stock">⚠ Low Stock (Only {{ product.stock }} left!)</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="out-of-stock">✗ Out of Stock</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if product.average_rating > 0 %}
|
||||||
|
<div class="product-rating">
|
||||||
|
<div class="stars">
|
||||||
|
{% for i in range(5) %}
|
||||||
|
<span class="star {% if i < product.average_rating|int %}filled{% endif %}">★</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<span class="rating-value">{{ product.average_rating }}/5</span>
|
||||||
|
<span class="review-count">({{ product.review_count }} reviews)</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="price-section">
|
||||||
|
<span class="price">${{ "%.2f"|format(product.price) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-meta">
|
||||||
|
{% if product.sku %}
|
||||||
|
<div class="meta-item">
|
||||||
|
<strong>SKU:</strong> {{ product.sku }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if product.company %}
|
||||||
|
<div class="meta-item">
|
||||||
|
<strong>Brand:</strong> {{ product.company }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if product.color %}
|
||||||
|
<div class="meta-item">
|
||||||
|
<strong>Color:</strong> {{ product.color }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if product.size %}
|
||||||
|
<div class="meta-item">
|
||||||
|
<strong>Size:</strong> {{ product.size }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if product.category %}
|
||||||
|
<div class="meta-item">
|
||||||
|
<strong>Category:</strong> {{ product.category.name }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description-section">
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>{{ product.description or 'No description available.' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if product.material or product.weight or product.dimensions %}
|
||||||
|
<div class="specifications">
|
||||||
|
<h3>Specifications</h3>
|
||||||
|
<div class="spec-grid">
|
||||||
|
{% if product.material %}
|
||||||
|
<div class="spec-item">
|
||||||
|
<span class="spec-label">Material:</span>
|
||||||
|
<span class="spec-value">{{ product.material }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if product.weight %}
|
||||||
|
<div class="spec-item">
|
||||||
|
<span class="spec-label">Weight:</span>
|
||||||
|
<span class="spec-value">{{ product.weight }} kg</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if product.dimensions %}
|
||||||
|
<div class="spec-item">
|
||||||
|
<span class="spec-label">Dimensions:</span>
|
||||||
|
<span class="spec-value">{{ product.dimensions }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="actions-section">
|
||||||
|
{% if current_user.is_authenticated and product.stock > 0 %}
|
||||||
|
<a href="{{ url_for('cart.add_to_cart', product_id=product.id) }}" class="btn btn-primary">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="9" cy="21" r="1"></circle>
|
||||||
|
<circle cx="20" cy="21" r="1"></circle>
|
||||||
|
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||||||
|
</svg>
|
||||||
|
Add to Cart
|
||||||
|
</a>
|
||||||
|
{% elif current_user.is_authenticated %}
|
||||||
|
<button class="btn btn-secondary" disabled>
|
||||||
|
Out of Stock
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('auth.login') }}" class="btn btn-primary">
|
||||||
|
Login to Purchase
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="{{ url_for('main.products') }}" class="btn btn-outline">
|
||||||
|
← Back to Products
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if related_products %}
|
||||||
|
<div class="related-products">
|
||||||
|
<h2>Related Products</h2>
|
||||||
|
<div class="related-grid">
|
||||||
|
{% for related in related_products %}
|
||||||
|
<div class="product-card">
|
||||||
|
<a href="{{ url_for('main.product_detail', id=related.id) }}" class="product-link">
|
||||||
|
<div class="product-image">
|
||||||
|
<img src="{{ related.get_image_url() }}" alt="{{ related.name }}">
|
||||||
|
</div>
|
||||||
|
<div class="product-info">
|
||||||
|
<h3 class="product-name">{{ related.name }}</h3>
|
||||||
|
<div class="product-price">${{ "%.2f"|format(related.price) }}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
255
templates/products.html
Normal file
255
templates/products.html
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/products.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/products.js') }}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="products-page">
|
||||||
|
<aside class="filters-sidebar">
|
||||||
|
<div class="filters-header">
|
||||||
|
<h3>Filters</h3>
|
||||||
|
{% if search_query or selected_category or selected_size or selected_color or selected_type or selected_material
|
||||||
|
or selected_company or min_price or max_price or in_stock_only %}
|
||||||
|
<a href="{{ url_for('main.products') }}" class="clear-filters">Clear All</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="GET" action="{{ url_for('main.products') }}" class="filters-form">
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="search">Search</label>
|
||||||
|
<input type="text" id="search" name="q" value="{{ search_query }}" placeholder="Search products..."
|
||||||
|
class="filter-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="category">Category</label>
|
||||||
|
<select id="category" name="category" class="filter-select">
|
||||||
|
<option value="">All Categories</option>
|
||||||
|
{% for c in categories %}
|
||||||
|
<option value="{{ c.id }}" {% if selected_category==c.id|string %}selected{% endif %}>
|
||||||
|
{{ c.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="size">Size</label>
|
||||||
|
<select id="size" name="size" class="filter-select">
|
||||||
|
<option value="">All Sizes</option>
|
||||||
|
{% for size in sizes %}
|
||||||
|
<option value="{{ size }}" {% if selected_size==size %}selected{% endif %}>
|
||||||
|
{{ size }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="color">Color</label>
|
||||||
|
<select id="color" name="color" class="filter-select">
|
||||||
|
<option value="">All Colors</option>
|
||||||
|
{% for color in colors %}
|
||||||
|
<option value="{{ color }}" {% if selected_color==color %}selected{% endif %}>
|
||||||
|
{{ color }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="material">Material</label>
|
||||||
|
<select id="material" name="material" class="filter-select">
|
||||||
|
<option value="">All Materials</option>
|
||||||
|
{% for material in materials %}
|
||||||
|
<option value="{{ material }}" {% if selected_material==material %}selected{% endif %}>
|
||||||
|
{{ material }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="company">Brand</label>
|
||||||
|
<select id="company" name="company" class="filter-select">
|
||||||
|
<option value="">All Brands</option>
|
||||||
|
{% for company in companies %}
|
||||||
|
<option value="{{ company }}" {% if selected_company==company %}selected{% endif %}>
|
||||||
|
{{ company }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Price Range ($)</label>
|
||||||
|
<div class="price-range">
|
||||||
|
<input type="number" name="min_price" value="{{ min_price }}" placeholder="Min" class="price-input" min="0"
|
||||||
|
step="0.01">
|
||||||
|
<span class="price-separator">-</span>
|
||||||
|
<input type="number" name="max_price" value="{{ max_price }}" placeholder="Max" class="price-input" min="0"
|
||||||
|
step="0.01">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="in_stock" value="1" {% if in_stock_only %}checked{% endif %}>
|
||||||
|
In Stock Only
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-apply-filters">Apply Filters</button>
|
||||||
|
</form>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="products-main">
|
||||||
|
<div class="products-header">
|
||||||
|
<div class="results-info">
|
||||||
|
<h1>Clothing</h1>
|
||||||
|
<p class="results-count">{{ products|length }} products found</p>
|
||||||
|
{% if search_query or selected_category or selected_size or selected_color or selected_type or selected_material
|
||||||
|
or selected_company or min_price or max_price or in_stock_only %}
|
||||||
|
<div class="active-filters">
|
||||||
|
{% if search_query %}
|
||||||
|
<span class="filter-tag">Search: "{{ search_query }}"
|
||||||
|
<a
|
||||||
|
href="{{ url_for('main.products', q='', category=selected_category, size=selected_size, color=selected_color, type=selected_type, material=selected_material, company=selected_company, min_price=min_price, max_price=max_price, in_stock=in_stock_only, sort=sort_by) }}">×</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if selected_category_name %}
|
||||||
|
<span class="filter-tag">Category: {{ selected_category_name }}
|
||||||
|
<a
|
||||||
|
href="{{ url_for('main.products', q=search_query, category='', size=selected_size, color=selected_color, type=selected_type, material=selected_material, company=selected_company, min_price=min_price, max_price=max_price, in_stock=in_stock_only, sort=sort_by) }}">×</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if selected_size %}
|
||||||
|
<span class="filter-tag">Size: {{ selected_size }}
|
||||||
|
<a
|
||||||
|
href="{{ url_for('main.products', q=search_query, category=selected_category, size='', color=selected_color, type=selected_type, material=selected_material, company=selected_company, min_price=min_price, max_price=max_price, in_stock=in_stock_only, sort=sort_by) }}">×</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if selected_color %}
|
||||||
|
<span class="filter-tag">Color: {{ selected_color }}
|
||||||
|
<a
|
||||||
|
href="{{ url_for('main.products', q=search_query, category=selected_category, size=selected_size, color='', type=selected_type, material=selected_material, company=selected_company, min_price=min_price, max_price=max_price, in_stock=in_stock_only, sort=sort_by) }}">×</a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="sort-controls">
|
||||||
|
<label for="sort">Sort by:</label>
|
||||||
|
<select id="sort" name="sort" class="sort-select" onchange="this.form.submit()">
|
||||||
|
<option value="newest" {% if sort_by=='newest' %}selected{% endif %}>Newest</option>
|
||||||
|
<option value="price_low" {% if sort_by=='price_low' %}selected{% endif %}>Price: Low to High</option>
|
||||||
|
<option value="price_high" {% if sort_by=='price_high' %}selected{% endif %}>Price: High to Low</option>
|
||||||
|
<option value="name" {% if sort_by=='name' %}selected{% endif %}>Name: A-Z</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if products %}
|
||||||
|
<div class="products-grid">
|
||||||
|
{% for product in products %}
|
||||||
|
<div class="product-card">
|
||||||
|
<a href="{{ url_for('main.product_detail', id=product.id) }}" class="product-link">
|
||||||
|
<div class="product-image">
|
||||||
|
<img src="{{ product.get_image_url() }}" alt="{{ product.name }}">
|
||||||
|
{% if product.stock <= 0 %} <div class="out-of-stock">Out of Stock
|
||||||
|
</div>
|
||||||
|
{% elif product.stock <= 5 %} <div class="low-stock">Only {{ product.stock }} left
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if product.average_rating > 0 %}
|
||||||
|
<div class="product-rating">
|
||||||
|
<span class="rating-stars">★★★★★</span>
|
||||||
|
<span class="rating-value">{{ product.average_rating }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-info">
|
||||||
|
<h3 class="product-name">{{ product.name }}</h3>
|
||||||
|
|
||||||
|
<div class="product-meta">
|
||||||
|
{% if product.color %}
|
||||||
|
<span class="meta-item">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
</svg>
|
||||||
|
{{ product.color }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if product.size %}
|
||||||
|
<span class="meta-item">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M20 7h-9"></path>
|
||||||
|
<path d="M14 17H5"></path>
|
||||||
|
<circle cx="17" cy="17" r="3"></circle>
|
||||||
|
<circle cx="7" cy="7" r="3"></circle>
|
||||||
|
</svg>
|
||||||
|
{{ product.size }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if product.company %}
|
||||||
|
<span class="meta-item">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
||||||
|
</svg>
|
||||||
|
{{ product.company }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="product-footer">
|
||||||
|
<div class="product-price">${{ "%.2f"|format(product.price) }}</div>
|
||||||
|
|
||||||
|
<div class="product-actions">
|
||||||
|
{% if current_user.is_authenticated and product.stock > 0 %}
|
||||||
|
<a href="{{ url_for('cart.add_to_cart', product_id=product.id) }}" class="btn-add-to-cart"
|
||||||
|
onclick="event.stopPropagation();">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<circle cx="9" cy="21" r="1"></circle>
|
||||||
|
<circle cx="20" cy="21" r="1"></circle>
|
||||||
|
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||||||
|
</svg>
|
||||||
|
Add to Cart
|
||||||
|
</a>
|
||||||
|
{% elif current_user.is_authenticated %}
|
||||||
|
<button class="btn-out-of-stock" disabled>
|
||||||
|
Out of Stock
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('auth.login') }}" class="btn-login-to-buy" onclick="event.stopPropagation();">
|
||||||
|
Login to Buy
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="no-products">
|
||||||
|
<div class="no-products-icon">👕</div>
|
||||||
|
<h3>No products found</h3>
|
||||||
|
<p>Try adjusting your filters or search criteria</p>
|
||||||
|
<a href="{{ url_for('main.products') }}" class="btn-view-all">
|
||||||
|
View All Products
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
115
templates/register.html
Normal file
115
templates/register.html
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/register.js') }}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="auth-container">
|
||||||
|
<h2>Create Account</h2>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="flash-messages">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="flash {{ category }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<form method="POST" class="auth-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email Address</label>
|
||||||
|
<input type="email" id="email" name="email" class="form-control" placeholder="you@example.com" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<div class="password-container">
|
||||||
|
<input type="password" id="password" name="password" class="form-control" placeholder="Create a strong password"
|
||||||
|
required oninput="checkPasswordStrength(this.value)">
|
||||||
|
<button type="button" class="toggle-password" onclick="togglePassword('password')">
|
||||||
|
👁️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="password-strength">
|
||||||
|
<div class="strength-bar" id="strengthBar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="password-hints" id="passwordHints">
|
||||||
|
<strong>Password should contain:</strong>
|
||||||
|
<ul>
|
||||||
|
<li id="lengthHint">At least 8 characters</li>
|
||||||
|
<li id="uppercaseHint">One uppercase letter</li>
|
||||||
|
<li id="lowercaseHint">One lowercase letter</li>
|
||||||
|
<li id="numberHint">One number</li>
|
||||||
|
<li id="specialHint">One special character</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm_password">Confirm Password</label>
|
||||||
|
<div class="password-container">
|
||||||
|
<input type="password" id="confirm_password" name="confirm_password" class="form-control"
|
||||||
|
placeholder="Confirm your password" required oninput="checkPasswordMatch()">
|
||||||
|
<button type="button" class="toggle-password" onclick="togglePassword('confirm_password')">
|
||||||
|
👁️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="passwordMatch" style="margin-top: 5px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="terms">
|
||||||
|
<input type="checkbox" id="terms" name="terms" required>
|
||||||
|
<label for="terms">
|
||||||
|
I agree to the <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-submit">
|
||||||
|
Create Account
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="auth-links">
|
||||||
|
<a href="{{ url_for('auth.login') }}" class="auth-link">
|
||||||
|
Already have an account? Sign in
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="social-login">
|
||||||
|
<div class="social-divider">
|
||||||
|
<span>Or sign up with</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="social-buttons">
|
||||||
|
<button type="button" class="btn-social google">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
||||||
|
</svg>
|
||||||
|
Google
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="btn-social facebook">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
|
||||||
|
</svg>
|
||||||
|
Facebook
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
63
templates/user_profile.html
Normal file
63
templates/user_profile.html
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/profile.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="profile-container">
|
||||||
|
<h2>My Profile</h2>
|
||||||
|
|
||||||
|
<div class="profile-info">
|
||||||
|
<p><strong>Email:</strong> {{ current_user.email }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="orders-section">
|
||||||
|
<h3>My Orders</h3>
|
||||||
|
|
||||||
|
{% if orders %}
|
||||||
|
<table class="orders-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Order ID</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Items</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in orders %}
|
||||||
|
<tr>
|
||||||
|
<td class="order-id">#{{ order.id }}</td>
|
||||||
|
<td class="order-date">{{ order.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge status-{{ order.status }}">
|
||||||
|
{{ order.status|title }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="order-items">{{ order.items|length }}</td>
|
||||||
|
<td class="order-actions">
|
||||||
|
<a href="{{ url_for('user.order_details', order_id=order.id) }}" class="view-details-link">
|
||||||
|
View Details
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="no-orders">
|
||||||
|
<p>You haven't placed any orders yet.</p>
|
||||||
|
<a href="{{ url_for('main.products') }}" class="shop-now-link">
|
||||||
|
Start Shopping
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ url_for('main.home') }}" class="back-link">
|
||||||
|
← Back to Home
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user