diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d688c72 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1b577bf --- /dev/null +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1eb6903 --- /dev/null +++ b/.gitignore @@ -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*/ diff --git a/README.md b/README.md index 92068b2..cb68e46 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,228 @@ -# wearwell +# ๐Ÿ›๏ธ Wearwell - Modern E-commerce Clothing Store -WearWell Clothing Website \ No newline at end of file +
+ +![Python](https://img.shields.io/badge/Python-3.12+-yellow.svg) +![Flask](https://img.shields.io/badge/Flask-3.1+-blue.svg) +![MariaDB](https://img.shields.io/badge/MariaDB-10+-orange.svg) + +**WearWell - a simple flask shopping web app for clothes** + + + +
+ +## ๐Ÿ“‹ 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 + + + +## ๐ŸŒ 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 +``` diff --git a/app.py b/app.py new file mode 100644 index 0000000..511d020 --- /dev/null +++ b/app.py @@ -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) diff --git a/diagram_wearwell_db.png b/diagram_wearwell_db.png new file mode 100644 index 0000000..1984831 Binary files /dev/null and b/diagram_wearwell_db.png differ diff --git a/extensions.py b/extensions.py new file mode 100644 index 0000000..236fdde --- /dev/null +++ b/extensions.py @@ -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' diff --git a/homepage.png b/homepage.png new file mode 100644 index 0000000..ff6038f Binary files /dev/null and b/homepage.png differ diff --git a/models.py b/models.py new file mode 100644 index 0000000..2793664 --- /dev/null +++ b/models.py @@ -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'' + +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') + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fc152c9 --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..4303497 --- /dev/null +++ b/routes/__init__.py @@ -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'] diff --git a/routes/admin.py b/routes/admin.py new file mode 100644 index 0000000..b9e9121 --- /dev/null +++ b/routes/admin.py @@ -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/', 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/', 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/', 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/', 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/', 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/', 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/', 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/') +@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/', 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')) diff --git a/routes/auth.py b/routes/auth.py new file mode 100644 index 0000000..e44a8bd --- /dev/null +++ b/routes/auth.py @@ -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')) diff --git a/routes/cart.py b/routes/cart.py new file mode 100644 index 0000000..4b84b6b --- /dev/null +++ b/routes/cart.py @@ -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/') +@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/', 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/', 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) diff --git a/routes/main.py b/routes/main.py new file mode 100644 index 0000000..b3bbb7b --- /dev/null +++ b/routes/main.py @@ -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/') +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) diff --git a/routes/user.py b/routes/user.py new file mode 100644 index 0000000..e9c16e4 --- /dev/null +++ b/routes/user.py @@ -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/') +@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) diff --git a/seed_db.py b/seed_db.py new file mode 100644 index 0000000..7e8bc09 --- /dev/null +++ b/seed_db.py @@ -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() diff --git a/static/css/add_product.css b/static/css/add_product.css new file mode 100644 index 0000000..5836391 --- /dev/null +++ b/static/css/add_product.css @@ -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; +} diff --git a/static/css/auth.css b/static/css/auth.css new file mode 100644 index 0000000..2ffcb06 --- /dev/null +++ b/static/css/auth.css @@ -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; +} diff --git a/static/css/cart.css b/static/css/cart.css new file mode 100644 index 0000000..e23b08c --- /dev/null +++ b/static/css/cart.css @@ -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%; + } +} diff --git a/static/css/categories.css b/static/css/categories.css new file mode 100644 index 0000000..f65ebe1 --- /dev/null +++ b/static/css/categories.css @@ -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; +} diff --git a/static/css/checkout.css b/static/css/checkout.css new file mode 100644 index 0000000..f0f8983 --- /dev/null +++ b/static/css/checkout.css @@ -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%; + } +} diff --git a/static/css/dashboard.css b/static/css/dashboard.css new file mode 100644 index 0000000..84173e5 --- /dev/null +++ b/static/css/dashboard.css @@ -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; + } +} diff --git a/static/css/edit_product.css b/static/css/edit_product.css new file mode 100644 index 0000000..90a2b0b --- /dev/null +++ b/static/css/edit_product.css @@ -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; +} diff --git a/static/css/home.css b/static/css/home.css new file mode 100644 index 0000000..b26031b --- /dev/null +++ b/static/css/home.css @@ -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,'); + 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%; + } +} diff --git a/static/css/order_details.css b/static/css/order_details.css new file mode 100644 index 0000000..9c53b00 --- /dev/null +++ b/static/css/order_details.css @@ -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; + } +} diff --git a/static/css/product_detail.css b/static/css/product_detail.css new file mode 100644 index 0000000..3e239cb --- /dev/null +++ b/static/css/product_detail.css @@ -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; + } +} diff --git a/static/css/products.css b/static/css/products.css new file mode 100644 index 0000000..e5035b6 --- /dev/null +++ b/static/css/products.css @@ -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; +} diff --git a/static/css/profile.css b/static/css/profile.css new file mode 100644 index 0000000..696a3e0 --- /dev/null +++ b/static/css/profile.css @@ -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; + } +} diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..00e8d73 --- /dev/null +++ b/static/css/styles.css @@ -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; +} diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..f910db9 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/images/default-product.jpg b/static/images/default-product.jpg new file mode 100644 index 0000000..624fcbe Binary files /dev/null and b/static/images/default-product.jpg differ diff --git a/static/js/add_product.js b/static/js/add_product.js new file mode 100644 index 0000000..63712bb --- /dev/null +++ b/static/js/add_product.js @@ -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); + } + } +}); diff --git a/static/js/edit_product.js b/static/js/edit_product.js new file mode 100644 index 0000000..ce3aff4 --- /dev/null +++ b/static/js/edit_product.js @@ -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); + }); + } +} diff --git a/static/js/home.js b/static/js/home.js new file mode 100644 index 0000000..6495901 --- /dev/null +++ b/static/js/home.js @@ -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)'; + } + }); +}); diff --git a/static/js/login.js b/static/js/login.js new file mode 100644 index 0000000..dc98530 --- /dev/null +++ b/static/js/login.js @@ -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; + }); +}); diff --git a/static/js/product_detail.js b/static/js/product_detail.js new file mode 100644 index 0000000..fd8d525 --- /dev/null +++ b/static/js/product_detail.js @@ -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'); + }); +}); diff --git a/static/js/products.js b/static/js/products.js new file mode 100644 index 0000000..f01d976 --- /dev/null +++ b/static/js/products.js @@ -0,0 +1,3 @@ +document.getElementById('sort').addEventListener('change', function () { + this.closest('form').submit(); +}); diff --git a/static/js/register.js b/static/js/register.js new file mode 100644 index 0000000..6409570 --- /dev/null +++ b/static/js/register.js @@ -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; + }); +}); diff --git a/templates/admin/add_product.html b/templates/admin/add_product.html new file mode 100644 index 0000000..94e59ef --- /dev/null +++ b/templates/admin/add_product.html @@ -0,0 +1,131 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block scripts %} + +{% endblock %} + +{% block content %} +
+

Add New Product

+
+
+
+

Basic Information

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+
+ +
+

Product Details

+ +
+ + +
+ +
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+
+ +
+

Product Images

+ +
+ +
+
+ + + + + +

Click or drag images here

+

First image will be primary

+
+ +
+ +
+
+
+
+ +
+ + Cancel +
+
+
+{% endblock %} diff --git a/templates/admin/categories.html b/templates/admin/categories.html new file mode 100644 index 0000000..8c65631 --- /dev/null +++ b/templates/admin/categories.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block content %} +
+

Manage Categories

+ +
+
+

Add New Category

+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+

All Categories ({{ categories|length }})

+
+
+ {% if categories %} +
+ + + + + + + + + + + {% for category in categories %} + + + + + + + {% endfor %} + +
IDNameProductsActions
{{ category.id }}{{ category.name }}{{ category.products|length }} + {% if category.products|length == 0 %} +
+ +
+ {% else %} + + {% endif %} +
+
+ {% else %} +
No categories found.
+ {% endif %} +
+
+
+{% endblock %} diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html new file mode 100644 index 0000000..9a88a7f --- /dev/null +++ b/templates/admin/dashboard.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block content %} +

Admin Dashboard

+Add New Product + + + + + + + + + + + + + + + {% for p in products %} + + + + + + + + + + + {% endfor %} + +
ImageIDNameDescriptionPriceStockCategoryActions
+ {{ p.name }} + {{ p.id }}{{ p.name }}{{ p.description[:50] }}{% if p.description|length > 50 %}...{% endif %}${{ "%.2f"|format(p.price) }} + {% if p.stock > 10 %} + {{ p.stock }} + {% elif p.stock > 0 %} + {{ p.stock }} + {% else %} + Out of Stock + {% endif %} + {{ p.category.name if p.category else 'N/A' }} + Edit +
+ +
+
+{% endblock %} diff --git a/templates/admin/edit_product.html b/templates/admin/edit_product.html new file mode 100644 index 0000000..3e3c5f7 --- /dev/null +++ b/templates/admin/edit_product.html @@ -0,0 +1,169 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block scripts %} + +{% endblock %} + +{% block content %} +
+

Edit Product: {{ product.name }}

+ +
+
+ +
+

Basic Information

+ +
+ + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+
+ +
+

Product Details

+
+ + +
+ +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+
+ +
+

Current Images

+
+ {% if product.images %} +
+ {% for image in product.images %} +
+ Product Image {{ loop.index }} +
+ + +
+
+ {% endfor %} +
+ {% else %} +

No images uploaded yet.

+ {% endif %} +
+ +

Add New Images

+
+
+
+ + + + + +

Click or drag images here

+
+ +
+ +
+
+
+
+ + + +
+ + Cancel +
+
+
+{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..b1e5bdf --- /dev/null +++ b/templates/base.html @@ -0,0 +1,61 @@ + + + + + + + WearWell Shop{% block title %}{% endblock %} + + {% block styles %}{% endblock %} + + + + + +
+ +
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} {% endwith %} + + + {% block content %}{% endblock %} +
+ +
+
+

© 2025 WearWell Shop. All rights reserved.

+
+
+ + {% block scripts %}{% endblock %} + + diff --git a/templates/cart.html b/templates/cart.html new file mode 100644 index 0000000..4410833 --- /dev/null +++ b/templates/cart.html @@ -0,0 +1,114 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block content %} +
+

Your Shopping Cart

+ + {% if items %} +
+ + + + + + + + + + + + {% for item in items %} + + + + + + + + {% endfor %} + +
ProductPriceQuantitySubtotalActions
+
+ {{ item.product.name }} +
+
+

{{ item.product.name }}

+

{{ item.product.description[:80] }}...

+
+ {% if item.product.stock > 10 %} + โœ“ In Stock + {% elif item.product.stock > 0 %} + โš  Low Stock ({{ item.product.stock }} left) + {% else %} + โœ— Out of Stock + {% endif %} +
+
+
${{ "%.2f"|format(item.product.price) }} +
+ + +
+ Max: {{ item.max_quantity }} +
+
+
${{ "%.2f"|format(item.subtotal) }} +
+ +
+
+ +
+
+

Order Summary

+
+ Subtotal: + ${{ "%.2f"|format(total) }} +
+
+ Shipping: + Free +
+
+ Total: + ${{ "%.2f"|format(total) }} +
+ + +
+
+
+ {% else %} +
+
๐Ÿ›’
+

Your cart is empty

+

Add some products to your cart and they will appear here.

+ + Browse Products + +
+ {% endif %} +
+{% endblock %} diff --git a/templates/checkout.html b/templates/checkout.html new file mode 100644 index 0000000..d06c583 --- /dev/null +++ b/templates/checkout.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block content %} +
+

Checkout

+ +
+
+

Order Summary

+ {% for item in cart.items %} +
+
+

{{ item.product.name }}

+

Quantity: {{ item.quantity }} ร— ${{ "%.2f"|format(item.product.price) }}

+
+
+ ${{ "%.2f"|format(item.product.price * item.quantity) }} +
+
+ {% endfor %} + +
+

Total: ${{ "%.2f"|format(total) }}

+
+
+ +
+

Shipping Information

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + โ† Back to Cart + + +
+
+
+
+
+{% endblock %} diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..04ad40e --- /dev/null +++ b/templates/home.html @@ -0,0 +1,229 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block scripts %} + +{% endblock %} + +{% block content %} + +
+
+

Welcome to WearWell Shop

+

Discover premium fashion for every occasion. Quality clothing, unbeatable prices, and style + that lasts.

+ + +
+
+ +
+ +
+ + + +
+
+

Summer Sale is Here! ๐ŸŒž

+

Get up to 50% off on selected items. Limited time offer!

+ + Shop Now โ†’ + +
+
+ +
+
+
+

Why Shop With Us?

+
+ +
+
+ ๐Ÿšš +

Free Shipping

+

Free delivery on all orders over $50. Fast and reliable shipping nationwide.

+
+ +
+ ๐Ÿ”„ +

Easy Returns

+

30-day return policy. If you're not satisfied, we'll make it right.

+
+ +
+ ๐Ÿ”’ +

Secure Payment

+

Your payment information is protected with bank-level security.

+
+ +
+ โญ +

Quality Guarantee

+

Premium materials and craftsmanship in every product we sell.

+
+
+
+
+ + + + +{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..c39d8e6 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block scripts %} + +{% endblock %} + +{% block content %} +
+

Welcome Back

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + +
+
+ + +
+ +
+ +
+ + +
+
+ +
+ + +
+ + +
+ + + + +
+{% endblock %} diff --git a/templates/order_details.html b/templates/order_details.html new file mode 100644 index 0000000..f50bd36 --- /dev/null +++ b/templates/order_details.html @@ -0,0 +1,93 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block content %} +
+

Order #{{ order.id }}

+ +
+

Order Date: {{ order.created_at.strftime('%Y-%m-%d %H:%M') }}

+
+ Status: + + {{ order.status|title }} + +
+
+ +
+

Order Items

+ + {% if order.items %} + + + + + + + + + + + {% for item in order.items %} + + + + + + + {% endfor %} + +
ProductQuantityPriceSubtotal
+
+ {{ item.product.name }} +
+
+
{{ item.product.name }}
+ {% if item.selected_size or item.selected_color %} +
+ {% if item.selected_size %}Size: {{ item.selected_size }}{% endif %} + {% if item.selected_color %} | Color: {{ item.selected_color }}{% endif %} +
+ {% endif %} +
+
{{ item.quantity }}${{ "%.2f"|format(item.product.price) }}${{ "%.2f"|format(item.product.price * item.quantity) }}
+ +
+
+ Subtotal: + + ${{ "%.2f"|format(order.items|sum(attribute='product.price') * order.items|sum(attribute='quantity')) }} + +
+
+ Shipping: + Free +
+
+ Total: + + ${{ "%.2f"|format(order.items|sum(attribute='product.price') * order.items|sum(attribute='quantity')) }} + +
+
+ {% else %} +
+

No items in this order.

+
+ {% endif %} +
+ +
+ + โ† Back to My Orders + + +
+
+{% endblock %} diff --git a/templates/product_detail.html b/templates/product_detail.html new file mode 100644 index 0000000..0778a74 --- /dev/null +++ b/templates/product_detail.html @@ -0,0 +1,178 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block scripts %} + +{% endblock %} + +{% block content %} +
+ +
+ + + +
+

{{ product.name }}

+ +
+ {% if product.stock > 10 %} + โœ“ In Stock ({{ product.stock }} available) + {% elif product.stock > 0 %} + โš  Low Stock (Only {{ product.stock }} left!) + {% else %} + โœ— Out of Stock + {% endif %} +
+ + {% if product.average_rating > 0 %} +
+
+ {% for i in range(5) %} + โ˜… + {% endfor %} +
+ {{ product.average_rating }}/5 + ({{ product.review_count }} reviews) +
+ {% endif %} + +
+ ${{ "%.2f"|format(product.price) }} +
+ +
+ {% if product.sku %} +
+ SKU: {{ product.sku }} +
+ {% endif %} + + {% if product.company %} +
+ Brand: {{ product.company }} +
+ {% endif %} + + {% if product.color %} +
+ Color: {{ product.color }} +
+ {% endif %} + + {% if product.size %} +
+ Size: {{ product.size }} +
+ {% endif %} + + {% if product.category %} +
+ Category: {{ product.category.name }} +
+ {% endif %} +
+ +
+

Description

+

{{ product.description or 'No description available.' }}

+
+ + {% if product.material or product.weight or product.dimensions %} +
+

Specifications

+
+ {% if product.material %} +
+ Material: + {{ product.material }} +
+ {% endif %} + + {% if product.weight %} +
+ Weight: + {{ product.weight }} kg +
+ {% endif %} + + {% if product.dimensions %} +
+ Dimensions: + {{ product.dimensions }} +
+ {% endif %} +
+
+ {% endif %} + +
+ {% if current_user.is_authenticated and product.stock > 0 %} + + + + + + + Add to Cart + + {% elif current_user.is_authenticated %} + + {% else %} + + Login to Purchase + + {% endif %} + + + โ† Back to Products + +
+
+
+ + {% if related_products %} + + {% endif %} +
+{% endblock %} diff --git a/templates/products.html b/templates/products.html new file mode 100644 index 0000000..fae35ce --- /dev/null +++ b/templates/products.html @@ -0,0 +1,255 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block scripts %} + +{% endblock %} + +{% block content %} +
+ + +
+
+
+

Clothing

+

{{ products|length }} products found

+ {% 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 %} +
+ {% if search_query %} + Search: "{{ search_query }}" + ร— + + {% endif %} + {% if selected_category_name %} + Category: {{ selected_category_name }} + ร— + + {% endif %} + {% if selected_size %} + Size: {{ selected_size }} + ร— + + {% endif %} + {% if selected_color %} + Color: {{ selected_color }} + ร— + + {% endif %} +
+ {% endif %} +
+
+ + +
+
+ + {% if products %} + +{% else %} +
+
๐Ÿ‘•
+

No products found

+

Try adjusting your filters or search criteria

+ + View All Products + +
+{% endif %} +
+
+{% endblock %} diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..0d93767 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,115 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block scripts %} + +{% endblock %} + +{% block content %} +
+

Create Account

+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + +
+
+ + +
+ +
+ +
+ + +
+
+
+
+
+ Password should contain: +
    +
  • At least 8 characters
  • +
  • One uppercase letter
  • +
  • One lowercase letter
  • +
  • One number
  • +
  • One special character
  • +
+
+
+ +
+ +
+ + +
+
+
+ +
+ + +
+ + +
+ + + + +
+{% endblock %} diff --git a/templates/user_profile.html b/templates/user_profile.html new file mode 100644 index 0000000..6b9a364 --- /dev/null +++ b/templates/user_profile.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} + +{% block styles %} + +{% endblock %} + +{% block content %} +
+

My Profile

+ +
+

Email: {{ current_user.email }}

+
+ +
+

My Orders

+ + {% if orders %} + + + + + + + + + + + + {% for order in orders %} + + + + + + + + {% endfor %} + +
Order IDDateStatusItemsActions
#{{ order.id }}{{ order.created_at.strftime('%Y-%m-%d %H:%M') }} + + {{ order.status|title }} + + {{ order.items|length }} + + View Details + +
+ {% else %} +
+

You haven't placed any orders yet.

+ + Start Shopping + +
+ {% endif %} +
+ + + โ† Back to Home + +
+{% endblock %}