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 +
+
+
+
+## ๐ 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'| ID | +Name | +Products | +Actions | +
|---|---|---|---|
| {{ category.id }} | +{{ category.name }} | +{{ category.products|length }} | ++ {% if category.products|length == 0 %} + + {% else %} + + {% endif %} + | +
| Image | +ID | +Name | +Description | +Price | +Stock | +Category | +Actions | +
|---|---|---|---|---|---|---|---|
|
+ |
+ {{ 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 + + | +
| Product | +Price | +Quantity | +Subtotal | +Actions | +
|---|---|---|---|---|
|
+
+
+
+
+ {{ 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) }} | ++ + | +${{ "%.2f"|format(item.subtotal) }} | ++ + | +
Add some products to your cart and they will appear here.
+ + Browse Products + +Quantity: {{ item.quantity }} ร ${{ "%.2f"|format(item.product.price) }}
+Discover premium fashion for every occasion. Quality clothing, unbeatable prices, and style + that lasts.
+ +Free delivery on all orders over $50. Fast and reliable shipping nationwide.
+30-day return policy. If you're not satisfied, we'll make it right.
+Your payment information is protected with bank-level security.
+Premium materials and craftsmanship in every product we sell.
+| Product | +Quantity | +Price | +Subtotal | +
|---|---|---|---|
|
+
+
+
+
+ {{ 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) }} | +
No items in this order.
+{{ product.description or 'No description available.' }}
+{{ 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 %} +Email: {{ current_user.email }}
+| Order ID | +Date | +Status | +Items | +Actions | +
|---|---|---|---|---|
| #{{ order.id }} | +{{ order.created_at.strftime('%Y-%m-%d %H:%M') }} | ++ + {{ order.status|title }} + + | +{{ order.items|length }} | ++ + View Details + + | +
You haven't placed any orders yet.
+ + Start Shopping + +