diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..33e0876 --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# Environment Variables Template +# Copy this file to .env and fill in your actual values + +# Google Gemini API Key +# Get your API key from: https://makersuite.google.com/app/apikey +GEMINI_API_KEY=your_gemini_api_key_here + +# Flask Configuration +FLASK_ENV=development +FLASK_APP=app.py + +# Flask secret used for sessions (change for production) +FLASK_SECRET_KEY=change-me + +# MongoDB configuration +# Use a full connection string for MongoDB Atlas or local server +MONGO_URI=mongodb://localhost:27017/ +MONGO_DB_NAME=phonestoredb + +# SMTP (optional) - used for password reset emails. If not set, codes are printed to console. +SMTP_SERVER=smtp.gmail.com +SMTP_PORT=465 +SMTP_FROM_EMAIL=your_email@example.com +SMTP_FROM_PASSWORD=your_smtp_app_password +SMTP_USE_TLS=false + +# Instructions: +# 1. Copy this file: cp .env.example .env +# 2. Replace 'your_gemini_api_key_here' with your actual Gemini API key +# 3. Never commit the .env file to version control + +# Notes: +# - On Windows PowerShell you can copy the file with: +# Copy-Item .env.example .env +# - If SMTP credentials are omitted, the app will print password reset codes to the server +# console (convenient for development). \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91d4065 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.env +*.bin +.vscode +__pycache__/ +*.pyc +*.pyo +*.sqlite +*.db +.DS_Store +Thumbs.db +.ipynb \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 6f3a291..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "liveServer.settings.port": 5501 -} \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..a778907 --- /dev/null +++ b/app.py @@ -0,0 +1,783 @@ +from flask import Flask, jsonify, request, render_template, session, redirect, url_for, flash +from flask_cors import CORS +from dotenv import load_dotenv +import uuid +from chatbot_backend import get_chatbot_response +from functools import wraps +from urllib.parse import quote +import os +import random +import smtplib +from email.mime.text import MIMEText +from pymongo import MongoClient, ASCENDING +from gridfs import GridFS +from bson.objectid import ObjectId +from werkzeug.security import generate_password_hash, check_password_hash +from datetime import datetime, timedelta + + + +load_dotenv() + +app = Flask(__name__) +app.secret_key = os.getenv('FLASK_SECRET_KEY', 'dev-secret-key') +app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # 5 MB upload limit (GridFS backend) +CORS(app) + +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not session.get('user_id'): + # Redirect to login with next param so we can return after auth + next_url = request.path + return redirect(url_for('login') + f"?next={quote(next_url)}") + return f(*args, **kwargs) + return decorated_function + +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + role = session.get('role') + if not session.get('user_id') or role != 'admin': + next_url = request.path + return redirect(url_for('login') + f"?next={quote(next_url)}") + return f(*args, **kwargs) + return decorated_function + +# --- MongoDB setup --- +MONGO_URI = os.getenv('MONGO_URI', 'mongodb://localhost:27017/') +MONGO_DB_NAME = os.getenv('MONGO_DB_NAME', 'phonestoredb') + +try: + # Add connection timeout to prevent hanging + mongo_client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000, connectTimeoutMS=5000) + # Test the connection + mongo_client.admin.command('ping') + mongo_db = mongo_client.get_database(MONGO_DB_NAME) + users_col = mongo_db['users'] + print(f"āœ… Connected to MongoDB: {MONGO_DB_NAME}") +except Exception as e: + print(f"āŒ MongoDB connection failed: {e}") + print("šŸ’” Make sure MongoDB is running on localhost:27017") + # Use a fallback or exit gracefully + mongo_client = None + mongo_db = None + users_col = None +products_col = mongo_db['products'] if mongo_db is not None else None +cart_col = mongo_db['cart'] if mongo_db is not None else None +fs = GridFS(mongo_db) if mongo_db is not None else None + +# Ensure we have a unique index on email +try: + users_col.create_index([('email', ASCENDING)], unique=True) +except Exception: + pass +# Helpful index on role (optional, non-unique) +try: + users_col.create_index([('role', ASCENDING)], unique=False) +except Exception: + pass + +# Ensure unique index on product id +try: + products_col.create_index([('id', ASCENDING)], unique=True) +except Exception: + pass + + +@app.route('/') +def index(): + if 'session_id' not in session: + session['session_id'] = str(uuid.uuid4()) + return render_template('index.html') + + + +@app.route('/login/') +def login(): + return render_template('login.html') + +# --- LOGIN API --- +@app.route('/api/login', methods=['POST']) +def api_login(): + data = request.json + email = data.get('email','').strip().lower() + password = data.get('password','') + user = users_col.find_one({'email': email}) + if user and user.get('password_hash') and check_password_hash(user['password_hash'], password): + session['user_id'] = str(user['_id']) + session['user_email'] = user.get('email') + session['user_name'] = user.get('name', '') + session['role'] = user.get('role', 'user') + return jsonify({'success': True, 'name': user.get('name', ''), 'role': session['role']}) + return jsonify({'success': False, 'message': 'Invalid email or password.'}), 401 + +# --- LOGOUT API --- +@app.route('/api/logout', methods=['POST']) +def api_logout(): + session.clear() + return jsonify({'success': True}) + +# --- REGISTER API --- +@app.route('/api/register', methods=['POST']) +def api_register(): + data = request.json + name = data.get('name') + email = data.get('email','').strip().lower() + password = data.get('password') + if not name or not email or not password: + return jsonify({'success': False, 'message': 'All fields are required.'}), 400 + try: + password_hash = generate_password_hash(password) + users_col.insert_one({ + 'name': name, + 'email': email, + 'password_hash': password_hash, + 'reset_code': None, + 'role': 'user' + }) + return jsonify({'success': True, 'message': 'Registration successful! You can now log in.'}) + except Exception as e: + # Likely duplicate email due to unique index + return jsonify({'success': False, 'message': 'Email already registered.'}), 400 + +# --- FORGOT PASSWORD API --- +def send_email(to_email, subject, body): + """Generic email sender with multiple fallback options. Returns True on success. + If SMTP credentials are not configured, logs to console (dev fallback). + """ + try: + SMTP_SERVER = os.getenv('SMTP_SERVER', 'smtp.gmail.com') + FROM_EMAIL = os.getenv('SMTP_FROM_EMAIL') + FROM_PASSWORD = os.getenv('SMTP_FROM_PASSWORD') + USE_TLS = os.getenv('SMTP_USE_TLS', 'false').lower() == 'true' + + msg = MIMEText(body) + msg["Subject"] = subject + msg["From"] = FROM_EMAIL + msg["To"] = to_email + + # If SMTP creds are not configured, log to console as a fallback + if not FROM_EMAIL or not FROM_PASSWORD: + print(f"[DEV] Email to {to_email} | Subject: {subject} | Body: {body}") + return True + + smtp_configs = [ + {'port': 587, 'use_ssl': False, 'use_tls': True}, + {'port': 465, 'use_ssl': True, 'use_tls': False}, + {'port': 25, 'use_ssl': False, 'use_tls': True}, + ] + + for config in smtp_configs: + try: + port = config['port'] + if config['use_ssl']: + server = smtplib.SMTP_SSL(SMTP_SERVER, port, timeout=5) + else: + server = smtplib.SMTP(SMTP_SERVER, port, timeout=5) + if config['use_tls']: + server.starttls() + + server.login(FROM_EMAIL, FROM_PASSWORD) + server.sendmail(FROM_EMAIL, [to_email], msg.as_string()) + server.quit() + print(f"āœ… Email sent to {to_email} via port {port}") + return True + except Exception as port_error: + print(f"āŒ Port {config['port']} failed: {port_error}") + continue + + print(f"āŒ All SMTP ports failed for {to_email}") + print(f"[DEV] Email to {to_email} | Subject: {subject} | Body: {body}") + return False + + except Exception as e: + print(f"āŒ Email sending failed: {e}") + print(f"[DEV] Email to {to_email} | Subject: {subject} | Body: {body}") + return False + + +def send_reset_email(to_email, code): + subject = "Your Password Reset Code" + body = f"Your verification code is: {code}\n\nThis code will expire in 15 minutes. If you didn't request this, ignore this email." + return send_email(to_email, subject, body) + + +def send_password_changed_email(to_email): + subject = "Your Password Was Updated" + body = "Your account password was recently updated. If you did not perform this action, please contact support immediately." + return send_email(to_email, subject, body) + +@app.route('/api/forgot-password', methods=['POST']) +def api_forgot_password(): + try: + # Check database connection + # Explicit None check (PyMongo Collection objects do not support truth value testing) + if users_col is None: + return jsonify({'success': False, 'message': 'Database not available'}), 500 + + data = request.json + email = data.get('email') + user = users_col.find_one({'email': email}) + if user: + code = str(random.randint(100000, 999999)) + # expiry 15 minutes from now + expiry_ts = int((datetime.utcnow() + timedelta(minutes=15)).timestamp()) + users_col.update_one({'_id': user['_id']}, {'$set': {'reset_code': code, 'reset_code_expires': expiry_ts}}) + send_reset_email(email, code) + return jsonify({'success': True, 'message': 'If your email exists, you will receive a code.'}) + except Exception as e: + import traceback + print('ERROR in /api/forgot-password:', traceback.format_exc()) # This will print detailed error in terminal + return jsonify({'success': False, 'message': str(e)}), 500 + +@app.route('/api/reset-password', methods=['POST']) +def api_reset_password(): + data = request.json + email = data.get('email') + code = data.get('code') + new_password = data.get('newPassword') + # Validate inputs + if not email or not code or not new_password: + return jsonify({'success': False, 'message': 'Missing parameters.'}), 400 + + user = users_col.find_one({'email': email}) + if not user or 'reset_code' not in user or not user.get('reset_code'): + return jsonify({'success': False, 'message': 'Invalid code or email.'}), 400 + + # Check code match + if str(user.get('reset_code')) != str(code): + return jsonify({'success': False, 'message': 'Invalid verification code.'}), 400 + + # Check expiry + expires_ts = user.get('reset_code_expires') + if expires_ts: + if int(datetime.utcnow().timestamp()) > int(expires_ts): + return jsonify({'success': False, 'message': 'Verification code expired.'}), 400 + + # All good, update password + try: + users_col.update_one({'_id': user['_id']}, { + '$set': { + 'password_hash': generate_password_hash(new_password), + }, + '$unset': {'reset_code': '', 'reset_code_expires': ''} + }) + # Send notification email (best-effort) + try: + send_password_changed_email(email) + except Exception as e: + print(f"Warning: failed to send password changed email: {e}") + + return jsonify({'success': True, 'message': 'Password reset successful.'}) + except Exception as e: + print(f"Error updating password: {e}") + return jsonify({'success': False, 'message': 'Failed to update password.'}), 500 + + +@app.route('/api/verify-reset-code', methods=['POST']) +def api_verify_reset_code(): + """Verify OTP code without resetting password yet.""" + data = request.json + email = data.get('email') + code = data.get('code') + if not email or not code: + return jsonify({'success': False, 'message': 'Missing parameters.'}), 400 + user = users_col.find_one({'email': email}) + if not user or not user.get('reset_code'): + return jsonify({'success': False, 'message': 'Invalid code or email.'}), 400 + if str(user.get('reset_code')) != str(code): + return jsonify({'success': False, 'message': 'Invalid verification code.'}), 400 + expires_ts = user.get('reset_code_expires') + if expires_ts and int(datetime.utcnow().timestamp()) > int(expires_ts): + return jsonify({'success': False, 'message': 'Verification code expired.'}), 400 + return jsonify({'success': True, 'message': 'Code verified. You may reset your password now.'}) + +@app.route('/api/check-auth') +def check_auth(): + if session.get('user_id'): + try: + user = users_col.find_one({'_id': ObjectId(session['user_id'])}) + except Exception: + user = None + if user: + return jsonify({ + 'authenticated': True, + 'name': user.get('name', ''), + 'email': user.get('email', ''), + 'role': user.get('role', 'user') + }) + return jsonify({'authenticated': False}) +#end + + +#--CHATBOT API-- +@app.route('/api/chatbot', methods=['POST']) +def chatbot(): + try: + data = request.get_json() + message = data.get('message', '') + + # Get the session ID from the user's session + session_id = session.get('session_id', 'default') + + # Get response from the chatbot + response = get_chatbot_response(message, session_id) + + return jsonify({'response': response}) + except Exception as e: + print(f"Error in chatbot endpoint: {e}") + return jsonify({'response': "Sorry, I'm having trouble processing your request right now."}), 500 + + + +# --- PRODUCTS API --- +@app.route('/api/products', methods=['GET']) +def api_products(): + try: + # Return all products (exclude Mongo _id) + raw_docs = list(products_col.find({}, {'_id': 0})) + docs = [] + for d in raw_docs: + if d.get('image_file_id'): + d['image_url'] = f"/api/products/{d['id']}/image" + docs.append(d) + return jsonify(docs) + except Exception as e: + print('ERROR in /api/products:', e) + return jsonify([]), 500 + +@app.route('/api/products//image') +def api_product_image(pid): + try: + prod = products_col.find_one({'id': pid}) + if not prod or not prod.get('image_file_id'): + return ('', 404) + from bson import ObjectId + try: + file_obj = fs.get(ObjectId(prod['image_file_id'])) + except Exception: + return ('', 404) + data = file_obj.read() + mime = file_obj.content_type or 'application/octet-stream' + return app.response_class(data, mimetype=mime, headers={ + 'Cache-Control': 'public, max-age=86400' + }) + except Exception as e: + print('ERROR serving product image:', e) + return ('', 500) + + +# --- ADMIN PAGE --- +@app.route('/admin/') +@admin_required +def admin(): + return render_template('admin.html') + + +# --- PRODUCTS CRUD (admin) --- +def _require_admin(): + return bool(session.get('user_id')) and session.get('role') == 'admin' + +def _allowed_image(filename: str) -> bool: + allowed = {'.png', '.jpg', '.jpeg', '.gif', '.webp'} + _, ext = os.path.splitext(filename.lower()) + return ext in allowed + +def _save_image_gridfs(file_storage): + if not file_storage or file_storage.filename == '': + return None, 'No file provided' + if not _allowed_image(file_storage.filename): + return None, 'Unsupported file type' + try: + file_id = fs.put(file_storage.stream.read(), filename=file_storage.filename, contentType=file_storage.mimetype) + return str(file_id), None + except Exception as e: + return None, f'Failed saving file: {e}' + +@app.route('/api/products', methods=['POST']) +def api_products_create(): + if not _require_admin(): + return jsonify({'success': False, 'message': 'Unauthorized'}), 401 + + # Support JSON (legacy) or multipart form with file + if request.content_type and 'multipart/form-data' in request.content_type: + form = request.form + try: + pid = int(form.get('id', '').strip()) + except ValueError: + return jsonify({'success': False, 'message': 'Invalid id'}), 400 + title = (form.get('title') or '').strip() + price_raw = form.get('price') + category = (form.get('category') or '').strip() + if not title or not price_raw or not category: + return jsonify({'success': False, 'message': 'Missing required fields'}), 400 + try: + price = float(price_raw) + except ValueError: + return jsonify({'success': False, 'message': 'Invalid price'}), 400 + image_file = request.files.get('imageFile') + image_file_id, err = _save_image_gridfs(image_file) + if err: + return jsonify({'success': False, 'message': err}), 400 + data = { + 'id': pid, + 'title': title, + 'price': price, + 'category': category, + 'image_file_id': image_file_id, + 'description': (form.get('description') or '').strip(), + 'specs': (form.get('specs') or '').strip() + } + else: + data = request.get_json(force=True) or {} + required = ['id', 'title', 'image', 'price', 'category'] + missing = [k for k in required if k not in data] + if missing: + return jsonify({'success': False, 'message': f'Missing fields: {', '.join(missing)}'}), 400 + try: + products_col.update_one({'id': data['id']}, {'$set': data}, upsert=True) + return jsonify({'success': True, 'data': {'image_file_id': data.get('image_file_id')}}) + except Exception as e: + return jsonify({'success': False, 'message': str(e)}), 500 + +@app.route('/api/products/', methods=['PUT']) +def api_products_update(pid): + if not _require_admin(): + return jsonify({'success': False, 'message': 'Unauthorized'}), 401 + + existing = products_col.find_one({'id': pid}) + if not existing: + return jsonify({'success': False, 'message': 'Product not found'}), 404 + + if request.content_type and 'multipart/form-data' in request.content_type: + form = request.form + title = (form.get('title') or existing.get('title', '')).strip() + category = (form.get('category') or existing.get('category', '')).strip() + price_raw = form.get('price', existing.get('price')) + try: + price = float(price_raw) + except ValueError: + return jsonify({'success': False, 'message': 'Invalid price'}), 400 + image_file = request.files.get('imageFile') + image_file_id = existing.get('image_file_id') + if image_file and image_file.filename: + new_file_id, err = _save_image_gridfs(image_file) + if err: + return jsonify({'success': False, 'message': err}), 400 + image_file_id = new_file_id + data = { + 'title': title, + 'price': price, + 'category': category, + 'image_file_id': image_file_id, + 'description': (form.get('description') or existing.get('description','')).strip(), + 'specs': (form.get('specs') or existing.get('specs','')).strip() + } + else: + data = request.get_json(force=True) or {} + try: + products_col.update_one({'id': pid}, {'$set': data}, upsert=False) + return jsonify({'success': True, 'data': {'image_file_id': data.get('image_file_id')}}) + except Exception as e: + return jsonify({'success': False, 'message': str(e)}), 500 + +@app.route('/api/products/', methods=['DELETE']) +def api_products_delete(pid): + if not _require_admin(): + return jsonify({'success': False, 'message': 'Unauthorized'}), 401 + try: + products_col.delete_one({'id': pid}) + return jsonify({'success': True}) + except Exception as e: + return jsonify({'success': False, 'message': str(e)}), 500 + + +# (admin-login removed; unified to /api/login and /login/) + + +@app.route('/product/') +def product_index(): + # Keep backward compatibility: redirect to home (use /product/ for details) + return redirect(url_for('index')) + + +@app.route('/product/') +def product_detail(pid): + try: + prod = products_col.find_one({'id': pid}, {'_id': 0}) + except Exception: + prod = None + if not prod: + return render_template('product.html', product=None), 404 + + # Provide image_url when using GridFS + if prod.get('image_file_id'): + prod['image_url'] = f"/api/products/{pid}/image" + + # Build images list (prefer image_url then image path) + images = [] + if prod.get('image_url'): + images.append(prod['image_url']) + if prod.get('image'): + img = prod.get('image') + if isinstance(img, str) and img.startswith('./images/'): + images.append('/static' + img[1:]) + else: + images.append(img) + if not images: + images.append('/static/images/products/placeholder.png') + + prod['images'] = images + prod['price'] = float(prod.get('price', 0)) + prod['description'] = prod.get('description', '') + # Normalize specs: allow stored dict or string (key:value lines or JSON) + raw_specs = prod.get('specs', '') + specs_obj = {} + if isinstance(raw_specs, dict): + specs_obj = raw_specs + elif isinstance(raw_specs, str) and raw_specs.strip(): + # try JSON + try: + import json + parsed = json.loads(raw_specs) + if isinstance(parsed, dict): + specs_obj = parsed + else: + specs_obj = {} + except Exception: + # parse key:value lines + for line in raw_specs.splitlines(): + if ':' in line: + k, v = line.split(':', 1) + specs_obj[k.strip()] = v.strip() + prod['specs'] = specs_obj + + return render_template('product.html', product=prod) + +@app.route('/cart/') +@login_required +def cart(): + return render_template('cart.html') + + +# --- USER DASHBOARD PAGE --- +@app.route('/user-dashboard/') +@login_required +def user_dashboard(): + return render_template('user_dashboard.html') + + +# --- CART API ENDPOINTS --- +@app.route('/api/cart/add', methods=['POST']) +def api_cart_add(): + try: + # Require authenticated user for cart actions + if not session.get('user_id'): + return jsonify({'success': False, 'message': 'Authentication required'}), 401 + data = request.get_json() + product_id = data.get('product_id') + quantity = int(data.get('quantity', 1)) + + if not product_id: + return jsonify({'success': False, 'message': 'Product ID required'}), 400 + + # Get product details + product = products_col.find_one({'id': product_id}, {'_id': 0}) + if not product: + return jsonify({'success': False, 'message': 'Product not found'}), 404 + + # Get user identifier (session_id for guests, user_id for logged in) + user_identifier = session.get('user_id') if session.get('user_id') else session.get('session_id') + if not user_identifier: + session['session_id'] = str(uuid.uuid4()) + user_identifier = session['session_id'] + + # Check if item already in cart + existing_item = cart_col.find_one({ + 'user_identifier': user_identifier, + 'product_id': product_id + }) + + if existing_item: + # Update quantity + new_quantity = existing_item['quantity'] + quantity + cart_col.update_one( + {'_id': existing_item['_id']}, + {'$set': {'quantity': new_quantity}} + ) + else: + # Add new item + cart_item = { + 'user_identifier': user_identifier, + 'product_id': product_id, + 'product_title': product.get('title', ''), + # Prefer image_url (GridFS) then image field then placeholder + 'product_image': product.get('image_url') or product.get('image') or '/static/images/products/placeholder.png', + 'product_price': float(product.get('price', 0)), + 'quantity': quantity, + 'added_at': ObjectId().generation_time + } + cart_col.insert_one(cart_item) + + # Get updated cart count + cart_count = cart_col.count_documents({'user_identifier': user_identifier}) + + return jsonify({ + 'success': True, + 'message': f'{product["title"]} added to cart!', + 'cart_count': cart_count + }) + + except Exception as e: + print(f"Error adding to cart: {e}") + return jsonify({'success': False, 'message': 'Failed to add item to cart'}), 500 + + +@app.route('/api/cart/get', methods=['GET']) +def api_cart_get(): + try: + # Require authenticated user for cart count/display: unauthenticated users should see 0 + if not session.get('user_id'): + return jsonify({'cart_items': [], 'total': 0, 'count': 0}), 401 + user_identifier = session.get('user_id') if session.get('user_id') else session.get('session_id') + if not user_identifier: + return jsonify({'cart_items': [], 'total': 0, 'count': 0}) + + # Get cart items + cart_items = list(cart_col.find( + {'user_identifier': user_identifier}, + {'_id': 0} + )) + + # Calculate total + total = sum(item['product_price'] * item['quantity'] for item in cart_items) + count = len(cart_items) + + return jsonify({ + 'cart_items': cart_items, + 'total': round(total, 2), + 'count': count + }) + + except Exception as e: + print(f"Error getting cart: {e}") + return jsonify({'cart_items': [], 'total': 0, 'count': 0}), 500 + + +@app.route('/api/cart/update', methods=['POST']) +def api_cart_update(): + try: + # Require authenticated user for cart actions + if not session.get('user_id'): + return jsonify({'success': False, 'message': 'Authentication required'}), 401 + data = request.get_json() + product_id = data.get('product_id') + quantity = int(data.get('quantity', 1)) + + if not product_id or quantity < 1: + return jsonify({'success': False, 'message': 'Invalid data'}), 400 + + user_identifier = session.get('user_id') if session.get('user_id') else session.get('session_id') + if not user_identifier: + return jsonify({'success': False, 'message': 'Session not found'}), 400 + + # Update quantity + result = cart_col.update_one( + {'user_identifier': user_identifier, 'product_id': product_id}, + {'$set': {'quantity': quantity}} + ) + + if result.matched_count == 0: + return jsonify({'success': False, 'message': 'Item not found in cart'}), 404 + + # Get updated cart info + cart_items = list(cart_col.find({'user_identifier': user_identifier}, {'_id': 0})) + total = sum(item['product_price'] * item['quantity'] for item in cart_items) + count = len(cart_items) + + return jsonify({ + 'success': True, + 'total': round(total, 2), + 'count': count + }) + + except Exception as e: + print(f"Error updating cart: {e}") + return jsonify({'success': False, 'message': 'Failed to update cart'}), 500 + + +@app.route('/api/cart/remove', methods=['POST']) +def api_cart_remove(): + try: + # Require authenticated user for cart actions + if not session.get('user_id'): + return jsonify({'success': False, 'message': 'Authentication required'}), 401 + data = request.get_json() + product_id = data.get('product_id') + + if not product_id: + return jsonify({'success': False, 'message': 'Product ID required'}), 400 + + user_identifier = session.get('user_id') if session.get('user_id') else session.get('session_id') + if not user_identifier: + return jsonify({'success': False, 'message': 'Session not found'}), 400 + + # Remove item + result = cart_col.delete_one({ + 'user_identifier': user_identifier, + 'product_id': product_id + }) + + if result.deleted_count == 0: + return jsonify({'success': False, 'message': 'Item not found in cart'}), 404 + + # Get updated cart info + cart_items = list(cart_col.find({'user_identifier': user_identifier}, {'_id': 0})) + total = sum(item['product_price'] * item['quantity'] for item in cart_items) + count = len(cart_items) + + return jsonify({ + 'success': True, + 'message': 'Item removed from cart', + 'total': round(total, 2), + 'count': count + }) + + except Exception as e: + print(f"Error removing from cart: {e}") + return jsonify({'success': False, 'message': 'Failed to remove item'}), 500 + + +@app.route('/api/cart/clear', methods=['POST']) +def api_cart_clear(): + try: + # Require authenticated user for cart actions + if not session.get('user_id'): + return jsonify({'success': False, 'message': 'Authentication required'}), 401 + user_identifier = session.get('user_id') if session.get('user_id') else session.get('session_id') + if not user_identifier: + return jsonify({'success': False, 'message': 'Session not found'}), 400 + + # Clear all cart items + cart_col.delete_many({'user_identifier': user_identifier}) + + return jsonify({ + 'success': True, + 'message': 'Cart cleared', + 'total': 0, + 'count': 0 + }) + + except Exception as e: + print(f"Error clearing cart: {e}") + return jsonify({'success': False, 'message': 'Failed to clear cart'}), 500 + + +if __name__ == '__main__': + print("šŸš€ Starting Flask application...") + try: + app.run(host='0.0.0.0', port=5000, threaded=True) + except Exception as e: + print(f"āŒ Failed to start Flask app: {e}") + print("šŸ’” Try running with: python -m flask run --host=0.0.0.0 --port=5000") diff --git a/cart.html b/cart.html deleted file mode 100644 index f84c69e..0000000 --- a/cart.html +++ /dev/null @@ -1,483 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - Phone Shop - - - - - -
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PRODUCTNAMEUNIT PRICEQUANTITYTOTAL
- - - - - Apple iPhone 11 -

- White/6.25 -
-
- $250.99 -
-
-
-
- - - - - - - - - - - -
-
-
-
- $250.99 -
- - - - - -
- - - - - Apple iPhone 11 -

- White/6.25 -
-
- $250.99 -
-
-
-
- - - - - - - - - - - -
-
-
-
- $250.99 -
- - - - - -
- - - - - Apple iPhone 11 -

- White/6.25 -
-
- $250.99 -
-
-
-
- - - - - - - - - - - -
-
-
-
- $250.99 -
- - - - - -
- - - - - Apple iPhone 11 -

- White/6.25 -
-
- $250.99 -
-
-
-
- - - - - - - - - - - -
-
-
-
- $250.99 -
- - - - - -
-
- -
- -
- - Shipping(+7$) -
-
- -
-

Cart Totals

-
    -
  • - Subtotal - $250.99 -
  • -
  • - Shipping - $0 -
  • -
  • - Total - $250.99 -
  • -
- PROCEED TO CHECKOUT -
-
-
-
-
- - -
-
-
-
-
- - - -
-

FREE SHIPPING WORLD WIDE

-
- -
-
- - - -
-

100% MONEY BACK GUARANTEE

-
- -
-
- - - -
-

MANY PAYMENT GATWAYS

-
- -
-
- - - -
-

24/7 ONLINE SUPPORT

-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/chatbot_backend.py b/chatbot_backend.py new file mode 100644 index 0000000..19c9f88 --- /dev/null +++ b/chatbot_backend.py @@ -0,0 +1,156 @@ +import os +from dotenv import load_dotenv +from langchain_google_genai import GoogleGenerativeAI +from langchain_google_genai import GoogleGenerativeAIEmbeddings +from langchain_chroma import Chroma +from langchain.chains import create_retrieval_chain +from langchain.chains.combine_documents import create_stuff_documents_chain +from langchain.chains import create_history_aware_retriever +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from langchain_community.chat_message_histories import ChatMessageHistory +from langchain_core.chat_history import BaseChatMessageHistory +from langchain_core.runnables.history import RunnableWithMessageHistory + + +load_dotenv() + +# Allow either GEMINI_API_KEY or GOOGLE_API_KEY from env/.env +gemini_api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY") + +CHATBOT_READY = bool(gemini_api_key) + +if CHATBOT_READY: + # Propagate to expected env var for the client lib + os.environ["GOOGLE_API_KEY"] = gemini_api_key + + LLModel = GoogleGenerativeAI(model="gemini-2.5-flash") + embedding_model = GoogleGenerativeAIEmbeddings(model="gemini-embedding-001") + vectorstore = Chroma(persist_directory="chroma_db", embedding_function=embedding_model) + retriever = vectorstore.as_retriever( + search_type="similarity", + #search_kwargs={"k": 4} + ) +else: + # Placeholders to avoid NameError if imported when not configured + LLModel = None + embedding_model = None + vectorstore = None + retriever = None + +system_prompt = """ +You are a helpful, customer-friendly mobile phone shop agent. +Use the provided context to answer customer questions about our phones. + +**Behavior Rules:** +1. **Answer from Context:** Only use the information in the `{context}` to answer questions about price, specs, and stock. If the information isn't there, you MUST reply with this exact sentence: `For more details about specific models, please contact us at Phone: 456-456-4512 or Email: company@gmail.com` +2. **Compare Phones:** If the customer's request matches multiple phones, list them with a brief comparison of key specs and price. +3. **Recall Details:** If asked for details about a phone mentioned earlier in the conversation, provide only those details without extra chatter. +4. **Handle Ambiguity:** If a request is unclear (e.g., "the Samsung phone"), ask for clarification (e.g., "Do you mean the Galaxy A54 or the Galaxy S23?"). +5. **Tone:** Be friendly, professional, and concise. Use short paragraphs and bullet points. + +**Purchase Flow:** +- If the customer wants to buy a phone, first confirm which model they want. +- Then, collect the following required information: `full name`, `nic` (national identity card number), and `delivery address`. +- Once you have all three pieces of information, you MUST output a JSON object in the format specified below, and NOTHING ELSE. + +**JSON Checkout Format:** +```json +{{ + "action": "checkout", + "customer": {{ + "name": "Full Name", + "nic": "NIC_NUMBER", + "address": "Full delivery address", + "phone": "optional phone number if provided" + }}, + "items": [ + {{ + "model": "Phone Model Name", + "sku": "SKU or ID from context", + "qty": 1, + "unit_price": 299.99 + }} + ], + "subtotal": 299.99, + "tax": 0.00, + "shipping": 0.00, + "total": 299.99, + "note": "Any note from buyer", + "next_step": "show_checkout_url_or_invoke_payment_api" +}} +``` + +Here is the information about our available phones: +{context} +""" + +prompt = ChatPromptTemplate.from_messages([ + ("system", system_prompt), + ("human", "{input}") +]) + +QAchain = create_stuff_documents_chain(LLModel, prompt) if CHATBOT_READY else None + +# This was the old RAGChain, which was not history-aware. +# RAGChain = create_retrieval_chain(retriever, QAchain) + +context_system_prompt = ( + "Given a chat history and the latest user question " + "which might reference context in the chat history, " + "formulate a standalone question which can be understood without the chat history. " + "Do NOT answer the question, just reformulate it if needed and otherwise return it as is." +) + +context_prompt = ChatPromptTemplate.from_messages([ + ("system", context_system_prompt), + MessagesPlaceholder("chat_history"), + ("human", "{input}"), +]) + +history_aware_retriever = ( + create_history_aware_retriever(LLModel, retriever, context_prompt) + if CHATBOT_READY else None +) + +# Create the new conversational RAG chain +conversational_rag_chain = ( + create_retrieval_chain(history_aware_retriever, QAchain) + if CHATBOT_READY else None +) + + +store = {} + + +def get_session_history(session_id: str) -> BaseChatMessageHistory: + if session_id not in store: + store[session_id] = ChatMessageHistory() + return store[session_id] + + +ConvRAGChain = ( + RunnableWithMessageHistory( + conversational_rag_chain, + get_session_history, + input_messages_key="input", + history_messages_key="chat_history", + output_messages_key="answer", + ) if CHATBOT_READY else None +) + + +def get_chatbot_response(message, session_id="default"): + if not CHATBOT_READY: + return ( + "Chatbot is not configured. Please set GEMINI_API_KEY (or GOOGLE_API_KEY) in your .env file " + "and restart the server." + ) + try: + response = ConvRAGChain.invoke( + {"input": message}, + config={"configurable": {"session_id": session_id}}, + ) + return response["answer"] + except Exception as e: + print(f"Error in chatbot response: {e}") + return "Sorry, I encountered an error processing your request. Please try again." diff --git a/check_users.py b/check_users.py new file mode 100644 index 0000000..d9ec29c --- /dev/null +++ b/check_users.py @@ -0,0 +1,30 @@ +from pymongo import MongoClient +import os + +MONGO_URI = os.getenv('MONGO_URI', 'mongodb://localhost:27017/') +MONGO_DB_NAME = os.getenv('MONGO_DB_NAME', 'phonestoredb') + +try: + client = MongoClient(MONGO_URI, serverSelectionTimeoutMS=5000) + client.admin.command('ping') + db = client.get_database(MONGO_DB_NAME) + users = db['users'] + + user_count = users.count_documents({}) + print(f'āœ… MongoDB connection successful') + print(f'šŸ“Š Total users in database: {user_count}') + + if user_count > 0: + print('\nšŸ‘„ Users in database:') + for i, user in enumerate(users.find(), 1): + name = user.get('name', 'N/A') + email = user.get('email', 'N/A') + role = user.get('role', 'user') + print(f' {i}. {name} ({email}) - Role: {role}') + else: + print('\nāš ļø No users found in database') + print('šŸ’” You can create an admin user by running: python create_admin.py') + +except Exception as e: + print(f'āŒ MongoDB connection failed: {e}') + print('šŸ’” Make sure MongoDB is running on localhost:27017') \ No newline at end of file diff --git a/create_admin.py b/create_admin.py new file mode 100644 index 0000000..78ccc31 --- /dev/null +++ b/create_admin.py @@ -0,0 +1,36 @@ +import os +from getpass import getpass +from pymongo import MongoClient +from werkzeug.security import generate_password_hash + +MONGO_URI = os.getenv('MONGO_URI', 'mongodb://localhost:27017/') +MONGO_DB_NAME = os.getenv('MONGO_DB_NAME', 'phonestoredb') + + +def main(): + print('Create or update an admin user') + email = input('Admin email: ').strip().lower() + name = input('Admin name: ').strip() + password = getpass('Admin password: ') + + client = MongoClient(MONGO_URI) + db = client.get_database(MONGO_DB_NAME) + users = db['users'] + + password_hash = generate_password_hash(password) + users.update_one( + {'email': email}, + {'$set': { + 'name': name, + 'email': email, + 'password_hash': password_hash, + 'role': 'admin', + 'reset_code': None + }}, + upsert=True + ) + print(f'Admin user {email} created/updated successfully.') + + +if __name__ == '__main__': + main() diff --git a/data/mobile_phones.csv b/data/mobile_phones.csv new file mode 100644 index 0000000..a051cb3 --- /dev/null +++ b/data/mobile_phones.csv @@ -0,0 +1,31 @@ +Brand,Model,Release Year,OS,Display Size (inches),RAM (GB),Storage (GB),Battery (mAh),Price (USD) +Samsung,Galaxy S23,2023,Android,6.1,8,128,3900,799 +Apple,iPhone 14,2022,iOS,6.1,6,128,3279,799 +OnePlus,OnePlus 11,2023,Android,6.7,12,256,5000,699 +Xiaomi,Redmi Note 12,2023,Android,6.6,6,128,5000,249 +Google,Pixel 7 Pro,2022,Android,6.7,12,128,5000,899 +Samsung,Galaxy A54,2023,Android,6.4,8,128,5000,449 +Apple,iPhone 13 Mini,2021,iOS,5.4,4,128,2438,699 +Realme,GT Neo 5,2023,Android,6.74,16,256,5000,499 +Oppo,Find X5 Pro,2022,Android,6.7,12,256,5000,1099 +Vivo,X90 Pro,2023,Android,6.78,12,256,4870,899 +Samsung,Galaxy Z Flip 5,2023,Android,6.7,8,256,3700,999 +Apple,iPhone SE (2022),2022,iOS,4.7,4,64,2018,429 +Motorola,Edge 40,2023,Android,6.55,8,128,4400,599 +Nokia,X30,2022,Android,6.43,8,256,4200,529 +Huawei,P50 Pro,2022,HarmonyOS,6.6,8,256,4360,1199 +Sony,Xperia 1 IV,2022,Android,6.5,12,256,5000,1599 +Asus,ROG Phone 7,2023,Android,6.78,16,512,6000,1099 +ZTE,Axon 40 Ultra,2022,Android,6.8,12,256,5000,799 +Honor,Magic5 Pro,2023,Android,6.81,12,512,5100,1199 +Samsung,Galaxy S22 Ultra,2022,Android,6.8,12,256,5000,1199 +Apple,iPhone 12,2020,iOS,6.1,4,128,2815,699 +OnePlus,Nord 2T,2022,Android,6.43,8,128,4500,399 +Xiaomi,Mi 11 Ultra,2021,Android,6.81,12,256,5000,1199 +Google,Pixel 6,2021,Android,6.4,8,128,4614,599 +Realme,Narzo 50,2022,Android,6.6,6,128,5000,199 +Oppo,Reno8 Pro,2022,Android,6.7,8,256,4500,549 +Vivo,V27 Pro,2023,Android,6.78,8,256,4600,459 +Motorola,G82,2022,Android,6.6,6,128,5000,299 +Huawei,Mate 40 Pro,2021,HarmonyOS,6.76,8,256,4400,1099 +Sony,Xperia 10 IV,2022,Android,6.0,6,128,5000,449 diff --git a/data/products.json b/data/products.json index 86a71ed..36b1cb6 100644 --- a/data/products.json +++ b/data/products.json @@ -2,184 +2,238 @@ "products": [{ "id": 6, "title": "Apple iPhone 11", - "image": "./images/products/iphone/iphone3.jpeg", + "image": "./static/images/products/iphone/iphone3.jpeg", "price": 760, - "category": "Featured Products" + "category": "Mobile phone" }, { "id": 3, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone2.jpeg", + "image": "./static/images/products/headphone/headphone2.jpeg", "price": 265, - "category": "Special Products" + "category": "Headphone" }, { "id": 4, "title": "Apple iPhone 11", - "image": "./images/products/iphone/iphone2.jpeg", + "image": "./static/images/products/iphone/iphone2.jpeg", "price": 850, - "category": "Special Products" + "category": "Mobile phone" }, { "id": 8, "title": "Apple iPhone 11", - "image": "./images/products/iphone/iphone4.jpeg", + "image": "./static/images/products/iphone/iphone4.jpeg", "price": 290, - "category": "Featured Products" + "category": "Mobile phone" }, { "id": 5, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone3.jpeg", + "image": "./static/images/products/headphone/headphone3.jpeg", "price": 250, - "category": "Special Products" + "category": "Headphone" }, { "id": 7, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone4.jpeg", + "image": "./static/images/products/headphone/headphone4.jpeg", "price": 365, - "category": "Featured Products" + "category": "Headphone" }, { "id": 10, "title": "Apple iPhone 11 Pro", - "image": "./images/products/iphone/iphone5.jpeg", + "image": "./static/images/products/iphone/iphone5.jpeg", "price": 385, - "category": "Special Products" + "category": "Mobile phone" }, { "id": 11, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone6.jpeg", + "image": "./static/images/products/headphone/headphone6.jpeg", "price": 475, - "category": "Special Products" + "category": "Headphone" }, { "id": 13, "title": "Apple iPhone 11", - "image": "./images/products/iphone/iphone6.jpeg", + "image": "./static/images/products/iphone/iphone6.jpeg", "price": 800, - "category": "Trending Products" + "category": "Mobile phone" }, { "id": 12, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone7.jpeg", + "image": "./static/images/products/headphone/headphone7.jpeg", "price": 850, - "category": "Special Products" + "category": "Headphone" }, { "id": 14, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone7.jpeg", + "image": "./static/images/products/headphone/headphone7.jpeg", "price": 360, - "category": "Trending Products" + "category": "Headphone" }, { "id": 9, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone5.jpeg", + "image": "./static/images/products/headphone/headphone5.jpeg", "price": 320, - "category": "Special Products" + "category": "Headphone" }, + { + "id": 37, + "title": "Samsung 16ā€ Galaxy Book5 Pro", + "image": "./static/images/products/laptop/laptop30.jpg", + "price": 1349, + "category": "Laptops" + }, { "id": 15, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone8.jpeg", + "image": "./static/images/products/headphone/headphone8.jpeg", "price": 305, - "category": "Trending Products" + "category": "Headphone" }, { "id": 16, "title": "Samsung Galaxy", - "image": "./images/products/sumsung/samsung6.jpeg", + "image": "./static/images/products/sumsung/samsung6.jpeg", "price": 400, - "category": "Special Products" + "category": "Mobile phone" }, { "id": 17, "title": "Samsung Galaxy", - "image": "./images/products/sumsung/samsung5.jpeg", + "image": "./static/images/products/sumsung/samsung5.jpeg", "price": 550, - "category": "Trending Products" + "category": "Mobile phone" }, + { + "id": 34, + "title": "MSI Katana 15", + "image": "./static/images/products/laptop/laptop27.jpeg", + "price": 1499, + "category": "Laptops" + }, { "id": 2, "title": "Apple iPhone 11", - "image": "./images/products/iphone/iphone1.jpeg", + "image": "./static/images/products/iphone/iphone1.jpeg", "price": 300, - "category": "Special Products" + "category": "Mobile phone" }, { "id": 18, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone9.jpeg", + "image": "./static/images/products/headphone/headphone9.jpeg", "price": 630, - "category": "Trending Products" + "category": "Headphone" }, + { + "id": 31, + "title": "Apple MacBook Air (MGN63PA/A)", + "image": "./static/images/products/laptop/laptop24.jpg", + "price": 2000, + "category": "Laptops" +}, { "id": 20, "title": "Samsung Galaxy", - "image": "./images/products/sumsung/samsung4.jpeg", + "image": "./static/images/products/sumsung/samsung4.jpeg", "price": 270, - "category": "Special Products" + "category": "Mobile phone" }, { "id": 19, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone10.jpeg", + "image": "./static/images/products/headphone/headphone10.jpeg", "price": 250, - "category": "Trending Products" + "category": "Headphone" }, + { + "id": 35, + "title": "HP 14 Laptop", + "image": "./static/images/products/laptop/laptop28.jpg", + "price": 172, + "category": "Laptops" + }, { "id": 1, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone1.jpeg", + "image": "./static/images/products/headphone/headphone1.jpeg", "price": 265, - "category": "Special Products" + "category": "Headphone" }, { "id": 24, "title": "Samsung Galaxy", - "image": "./images/products/sumsung/samsung2.jpeg", + "image": "./static/images/products/sumsung/samsung2.jpeg", "price": 500, - "category": "Featured Products" + "category": "Mobile phone" }, + { + "id": 32, + "title": "ASUS ROG Strix G16", + "image": "./static/images/products/laptop/laptop25.jpeg", + "price": 2099, + "category": "Laptops" + }, { "id": 21, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone11.jpeg", + "image": "./static/images/products/headphone/headphone11.jpeg", "price": 700, - "category": "Trending Products" + "category": "Headphone" }, + { + "id": 33, + "title": "Acer Nitro V Gaming Laptop", + "image": "./static/images/products/laptop/laptop26.jpg", + "price": 849, + "category": "Laptops" + }, + + { "id": 25, "title": "Samsung Galaxy", - "image": "./images/products/sumsung/samsung1.jpeg", + "image": "./static/images/products/sumsung/samsung1.jpeg", "price": 450, - "category": "Special Products" + "category": "Mobile phone" }, { "id": 22, "title": "Samsung Galaxy", - "image": "./images/products/sumsung/samsung3.jpeg", + "image": "./static/images/products/sumsung/samsung3.jpeg", "price": 460, - "category": "Trending Products" + "category": "Mobile phone" }, { "id": 23, "title": "Sony WH-CH510", - "image": "./images/products/headphone/headphone12.jpeg", + "image": "./static/images/products/headphone/headphone12.jpeg", "price": 600, - "category": "Featured Products" - } + "category": "Headphone" + }, + +{ + "id": 36, + "title": "Dell Latitude 3190", + "image": "./static/images/products/laptop/laptop29.jpg", + "price": 164, + "category": "Laptops" +} + + ] } \ No newline at end of file diff --git a/images/news1.jpg b/images/news1.jpg deleted file mode 100644 index 568be88..0000000 Binary files a/images/news1.jpg and /dev/null differ diff --git a/images/news2.jpg b/images/news2.jpg deleted file mode 100644 index f78356a..0000000 Binary files a/images/news2.jpg and /dev/null differ diff --git a/images/news3.jpg b/images/news3.jpg deleted file mode 100644 index 82fbc36..0000000 Binary files a/images/news3.jpg and /dev/null differ diff --git a/images/news5.jpg b/images/news5.jpg deleted file mode 100644 index bffb043..0000000 Binary files a/images/news5.jpg and /dev/null differ diff --git a/images/profile1.jpg b/images/profile1.jpg deleted file mode 100644 index e4ade26..0000000 Binary files a/images/profile1.jpg and /dev/null differ diff --git a/images/profile2.jpg b/images/profile2.jpg deleted file mode 100644 index 527c345..0000000 Binary files a/images/profile2.jpg and /dev/null differ diff --git a/images/profile3.jpg b/images/profile3.jpg deleted file mode 100644 index ca6e839..0000000 Binary files a/images/profile3.jpg and /dev/null differ diff --git a/images/profile4.jpg b/images/profile4.jpg deleted file mode 100644 index 43b885d..0000000 Binary files a/images/profile4.jpg and /dev/null differ diff --git a/js/index.js b/js/index.js deleted file mode 100644 index 8fed935..0000000 --- a/js/index.js +++ /dev/null @@ -1,122 +0,0 @@ -/* -============= -Navigation -============= - */ -const navOpen = document.querySelector(".nav__hamburger"); -const navClose = document.querySelector(".close__toggle"); -const menu = document.querySelector(".nav__menu"); -const scrollLink = document.querySelectorAll(".scroll-link"); -const navContainer = document.querySelector(".nav__menu"); - -navOpen.addEventListener("click", () => { - menu.classList.add("open"); - document.body.classList.add("active"); - navContainer.style.left = "0"; - navContainer.style.width = "30rem"; -}); - -navClose.addEventListener("click", () => { - menu.classList.remove("open"); - document.body.classList.remove("active"); - navContainer.style.left = "-30rem"; - navContainer.style.width = "0"; -}); - -/* -============= -PopUp -============= - */ -const popup = document.querySelector(".popup"); -const closePopup = document.querySelector(".popup__close"); - -if (popup) { - closePopup.addEventListener("click", () => { - popup.classList.add("hide__popup"); - }); - - window.addEventListener("load", () => { - setTimeout(() => { - popup.classList.remove("hide__popup"); - }, 500); - }); -} - -/* -============= -Fixed Navigation -============= - */ - -const navBar = document.querySelector(".navigation"); -const gotoTop = document.querySelector(".goto-top"); - -// Smooth Scroll -Array.from(scrollLink).map(link => { - link.addEventListener("click", e => { - // Prevent Default - e.preventDefault(); - - const id = e.currentTarget.getAttribute("href").slice(1); - const element = document.getElementById(id); - const navHeight = navBar.getBoundingClientRect().height; - const fixNav = navBar.classList.contains("fix__nav"); - let position = element.offsetTop - navHeight; - - if (!fixNav) { - position = position - navHeight; - } - - window.scrollTo({ - left: 0, - top: position, - }); - navContainer.style.left = "-30rem"; - document.body.classList.remove("active"); - }); -}); - -// Fix NavBar - -window.addEventListener("scroll", e => { - const scrollHeight = window.pageYOffset; - const navHeight = navBar.getBoundingClientRect().height; - if (scrollHeight > navHeight) { - navBar.classList.add("fix__nav"); - } else { - navBar.classList.remove("fix__nav"); - } - - if (scrollHeight > 300) { - gotoTop.classList.add("show-top"); - } else { - gotoTop.classList.remove("show-top"); - } -}); - -let login=document.querySelector('.login-form'); - -document.querySelector('#login-btn').onclick=()=>{ - login.classList.toggle('active'); - searchForm.classList.remove('active'); - shoppingCart.classList.remove('active'); - -} - -let shoppingCart=document.querySelector('.shopping-cart'); - -document.querySelector('#cart-btn').onclick=()=>{ - shoppingCart.classList.toggle('active'); - searchForm.classList.remove('active'); - login.classList.remove('active'); - -} -let searchForm=document.querySelector('.search-form'); - -document.querySelector('#search-btn').onclick=()=>{ - searchForm.classList.toggle('active'); - shoppingCart.classList.remove('active'); - login.classList.remove('active'); - -} \ No newline at end of file diff --git a/js/products.js b/js/products.js deleted file mode 100644 index 7b5f4c9..0000000 --- a/js/products.js +++ /dev/null @@ -1,203 +0,0 @@ -const getProducts = async () => { - try { - const results = await fetch("./data/products.json"); - const data = await results.json(); - const products = data.products; - return products; - } catch (err) { - console.log(err); - } -}; - -/* -============= -Load Category Products -============= - */ -const categoryCenter = document.querySelector(".category__center"); - -window.addEventListener("DOMContentLoaded", async function () { - const products = await getProducts(); - displayProductItems(products); -}); - -const displayProductItems = items => { - let displayProduct = items.map( - product => ` -
-
- product -
- - -
- ` - ); - - displayProduct = displayProduct.join(""); - if (categoryCenter) { - categoryCenter.innerHTML = displayProduct; - } -}; - -/* -============= -Filtering -============= - */ - -const filterBtn = document.querySelectorAll(".filter-btn"); -const categoryContainer = document.getElementById("category"); - -if (categoryContainer) { - categoryContainer.addEventListener("click", async e => { - const target = e.target.closest(".section__title"); - if (!target) return; - - const id = target.dataset.id; - const products = await getProducts(); - - if (id) { - // remove active from buttons - Array.from(filterBtn).forEach(btn => { - btn.classList.remove("active"); - }); - target.classList.add("active"); - - // Load Products - let menuCategory = products.filter(product => { - if (product.category === id) { - return product; - } - }); - - if (id === "All Products") { - displayProductItems(products); - } else { - displayProductItems(menuCategory); - } - } - }); -} - -/* -============= -Product Details Left -============= - */ -const pic1 = document.getElementById("pic1"); -const pic2 = document.getElementById("pic2"); -const pic3 = document.getElementById("pic3"); -const pic4 = document.getElementById("pic4"); -const pic5 = document.getElementById("pic5"); -const picContainer = document.querySelector(".product__pictures"); -const zoom = document.getElementById("zoom"); -const pic = document.getElementById("pic"); - -// Picture List -const picList = [pic1, pic2, pic3, pic4, pic5]; - -// Active Picture -let picActive = 1; - -["mouseover", "touchstart"].forEach(event => { - if (picContainer) { - picContainer.addEventListener(event, e => { - const target = e.target.closest("img"); - if (!target) return; - const id = target.id.slice(3); - changeImage(`./images/products/iPhone/iphone${id}.jpeg`, id); - }); - } -}); - -// change active image -const changeImage = (imgSrc, n) => { - // change the main image - pic.src = imgSrc; - // change the background-image - zoom.style.backgroundImage = `url(${imgSrc})`; - // remove the border from the previous active side image - picList[picActive - 1].classList.remove("img-active"); - // add to the active image - picList[n - 1].classList.add("img-active"); - // update the active side picture - picActive = n; -}; - -/* -============= -Product Details Bottom -============= - */ - -const btns = document.querySelectorAll(".detail-btn"); -const detail = document.querySelector(".product-detail__bottom"); -const contents = document.querySelectorAll(".content"); - -if (detail) { - detail.addEventListener("click", e => { - const target = e.target.closest(".detail-btn"); - if (!target) return; - - const id = target.dataset.id; - if (id) { - Array.from(btns).forEach(btn => { - // remove active from all btn - btn.classList.remove("active"); - e.target.closest(".detail-btn").classList.add("active"); - }); - // hide other active - Array.from(contents).forEach(content => { - content.classList.remove("active"); - }); - const element = document.getElementById(id); - element.classList.add("active"); - } - }); -} diff --git a/migrate_db.py b/migrate_db.py new file mode 100644 index 0000000..d7c08d0 --- /dev/null +++ b/migrate_db.py @@ -0,0 +1,71 @@ +import os +from pymongo import MongoClient, ASCENDING + +MONGO_URI = os.getenv('MONGO_URI', 'mongodb://localhost:27017/') +MONGO_DB_NAME = os.getenv('MONGO_DB_NAME', 'phonestoredb') + +client = MongoClient(MONGO_URI) +db = client.get_database(MONGO_DB_NAME) +users = db['users'] +products = db['products'] + + +def ensure_indexes(): + print('Ensuring indexes...') + try: + users.create_index([('email', ASCENDING)], unique=True) + users.create_index([('role', ASCENDING)], unique=False) + products.create_index([('id', ASCENDING)], unique=True) + print('Indexes ensured.') + except Exception as e: + print('Index ensure warning:', e) + + +def normalize_user_emails_and_roles(): + print('Normalizing user emails and roles...') + updated = 0 + for u in users.find({}): + updates = {} + # Lowercase and strip emails + email = (u.get('email') or '').strip() + if email and email != email.lower(): + updates['email'] = email.lower() + # Backfill missing roles to 'user' + if not u.get('role'): + updates['role'] = 'user' + if updates: + users.update_one({'_id': u['_id']}, {'$set': updates}) + updated += 1 + print(f'Users normalized: {updated}') + + +def clean_products(): + print('Cleaning product fields...') + cleaned = 0 + for p in products.find({}): + updates = {} + # Ensure id is int + try: + if isinstance(p.get('id'), str) and p['id'].isdigit(): + updates['id'] = int(p['id']) + except Exception: + pass + # Ensure required fields exist + for key in ['title', 'image', 'price', 'category']: + if key not in p: + updates[key] = '' if key in ('title', 'image', 'category') else 0 + if updates: + products.update_one({'_id': p['_id']}, {'$set': updates}) + cleaned += 1 + print(f'Products cleaned: {cleaned}') + + +def main(): + ensure_indexes() + normalize_user_emails_and_roles() + clean_products() + print('Migration complete.') + + +if __name__ == '__main__': + main() diff --git a/readme.md b/readme.md index 2430417..fbe4a64 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,161 @@ -# Responsive Ecommerce Website Using HTML CSS JAVASCRIPT +# Responsive Ecommerce Website šŸ“±šŸ›’ + +A modern, mobile-friendly ecommerce template built with **Flask** (Python), **MongoDB**, and a static frontend (HTML, CSS, JS). Features product catalog, user authentication, admin dashboard, shopping cart, and an advanced **AI-powered chatbot** for product support via Retrieval-Augmented Generation (RAG). + +--- + +## Features + +- **Product Catalog**: Browse phones with images, specs, and pricing +- **User Auth**: Registration, login, logout, and password reset (email or console) +- **Admin Dashboard**: Product CRUD UI and APIs (admin only) +- **Shopping Cart**: Add/update/remove/clear cart (session or user) +- **MongoDB Backend**: Stores users, products, cart; product images via GridFS +- **API Endpoints**: RESTful APIs for frontend and external integrations +- **Conversational AI Chatbot**: Ask about phones, specs, compare models, and automate checkout (powered by LangChain + Google Gemini + ChromaDB) +- **Email Notifications**: Optional SMTP for password reset +- **Testing**: Scripts and test files for API and end-to-end testing + +--- + +## Quick Start + +### 1. Clone & Environment Setup + +```bash +git clone https://github.com/youruser/yourrepo.git +cd yourrepo +python -m venv .venv +source .venv/bin/activate # or .\.venv\Scripts\Activate.ps1 on Windows +pip install -r requirements.txt +``` + +### 2. Configure Environment Variables + +Create a `.env` file in the root: + +```dotenv +FLASK_SECRET_KEY=change-me +MONGO_URI=mongodb://localhost:27017/ +MONGO_DB_NAME=phonestoredb +SMTP_SERVER=smtp.gmail.com # optional for password reset +SMTP_FROM_EMAIL=you@example.com # optional +SMTP_FROM_PASSWORD=your-app-pass # optional +SMTP_USE_TLS=true # optional +GEMINI_API_KEY=your_gemini_key # for chatbot +``` + +If SMTP is not set, password reset codes print to the server console for dev/testing. + +### 3. Seed Products & Create Admin + +```bash +python seed_products.py # Optional: pre-load products +python create_admin.py # Optional: set up admin user +``` + +### 4. Run MongoDB + +- Local: start `mongod` +- Or use [MongoDB Atlas](https://www.mongodb.com/atlas/database) + +### 5. Start the App + +```bash +python app.py +# or: flask run +``` +Visit [http://localhost:5000/](http://localhost:5000/) in your browser. + +--- + +## API Endpoints (Selection) + +- `POST /api/register` `{ name, email, password }` +- `POST /api/login` `{ email, password }` +- `POST /api/logout` +- `POST /api/forgot-password` `{ email }` +- `POST /api/reset-password` `{ email, code, newPassword }` +- `GET /api/products` +- `GET /api/products//image` +- `POST /api/products` *(admin, JSON or multipart)* +- `PUT /api/products/` *(admin)* +- `DELETE /api/products/` *(admin)* +- `POST /api/cart/add` +- `GET /api/cart/get` +- `POST /api/chatbot` *(conversational AI, see below)* + +--- + +## Conversational AI Chatbot (RAG) + +**Supercharge your store with a context-aware AI assistant!** + +- **RAG (Retrieval-Augmented Generation)**: Combines product database (vectorized with ChromaDB) and Google Gemini LLM for accurate, real-time Q&A. +- **Tech Stack**: [LangChain](https://github.com/langchain-ai/langchain), [ChromaDB](https://www.trychroma.com/), [Google Generative AI](https://ai.google.dev/). +- **Capabilities**: + - Ask about phone specs/pricing + - Compare devices (ā€œCompare Galaxy S23 and iPhone 14ā€) + - Get recommendations + - Automate checkout (chatbot collects info, returns checkout JSON) +- **Enable**: + 1. Set `GEMINI_API_KEY` in `.env` + 2. Install Python deps: `pip install -r requirements.txt` + 3. Ensure `chroma_db/chroma.sqlite3` exists (prebuilt with phone data) + 4. Use the chat widget on the main page! + +**Chatbot Architecture**: + +``` +User → [Chat Widget] → [Flask API] → [ChromaDB Retriever] → [Gemini LLM via LangChain] → Response +``` + +--- + +## File Structure Highlights + +- `app.py` – Flask backend & API endpoints +- `chatbot_backend.py` – RAG chatbot logic (LangChain, ChromaDB, Gemini) +- `chroma_db/` – Vector DB for product specs +- `static/`, `templates/` – Frontend assets +- `test_*.py` – Test scripts +- `data/` – Sample product data + +--- + +## Developer Notes & Troubleshooting + +- If MongoDB is unavailable, `app.py` prints an error and disables DB features. +- Product image uploads require working GridFS. +- Unique indexes on `users.email` and `products.id` are created if possible. +- If SMTP is missing, password reset codes are printed to the console. + +**Testing**: +Run `pytest` on test files like `test_api.py`. + +--- + +## Contributing + +Contributions are welcome! Please open an issue or pull request for bug fixes, features, or documentation improvements. + +- Fork the repo and create your branch (`git checkout -b my-feature`) +- Commit your changes +- Push to your fork +- Open a PR describing your change + +--- + +## License + +MIT License. See `LICENSE`. + +--- + +## Credits & Acknowledgments + +- [LangChain](https://github.com/langchain-ai/langchain) +- [Google Generative AI](https://ai.google.dev/) +- [ChromaDB](https://www.trychroma.com/) + +--- diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5acb342 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +flask==2.0.1 +flask-cors==3.0.10 +pymongo==4.8.0 +werkzeug==2.0.3 +langchain-core==0.1.5 +langchain-google-genai==0.0.5 +langchain-community==0.0.9 +langchain-chroma==0.2.6 +chromadb==0.4.15 +pydantic==1.10.8 +python-dotenv==0.21.1 +uuid==1.30 diff --git a/seed_products.py b/seed_products.py new file mode 100644 index 0000000..6924219 --- /dev/null +++ b/seed_products.py @@ -0,0 +1,52 @@ +import json +import os +from pymongo import MongoClient, ASCENDING + + +def main(): + # Resolve data path relative to this file, with fallback to parent directory + here = os.path.dirname(os.path.abspath(__file__)) + candidates = [ + os.path.join(here, 'data', 'products.json'), + os.path.join(os.path.dirname(here), 'data', 'products.json') + ] + data_path = next((p for p in candidates if os.path.exists(p)), None) + if not data_path: + raise FileNotFoundError( + f"Could not find data/products.json. Tried: {candidates}. " + f"Ensure products.json exists under the project's data folder." + ) + + with open(data_path, 'r', encoding='utf-8') as f: + payload = json.load(f) + + products = payload.get('products', []) + if not isinstance(products, list): + raise ValueError('products.json format invalid: "products" should be a list') + + mongo_uri = os.getenv('MONGO_URI', 'mongodb://localhost:27017/') + db_name = os.getenv('MONGO_DB_NAME', 'phonestoredb') + + client = MongoClient(mongo_uri) + db = client.get_database(db_name) + col = db['products'] + + # Ensure unique index on id + try: + col.create_index([('id', ASCENDING)], unique=True) + except Exception: + pass + + # Upsert by id + upserts = 0 + for p in products: + if 'id' not in p: + continue + col.update_one({'id': p['id']}, {'$set': p}, upsert=True) + upserts += 1 + + print(f'Seeded {upserts} products into MongoDB database "{db_name}" collection "products"') + + +if __name__ == '__main__': + main() diff --git a/images/banner_01.png b/static/images/banner_01.png similarity index 100% rename from images/banner_01.png rename to static/images/banner_01.png diff --git a/images/banner_02.png b/static/images/banner_02.png similarity index 100% rename from images/banner_02.png rename to static/images/banner_02.png diff --git a/static/images/bot.png b/static/images/bot.png new file mode 100644 index 0000000..b964a73 Binary files /dev/null and b/static/images/bot.png differ diff --git a/static/images/chat-bot.png b/static/images/chat-bot.png new file mode 100644 index 0000000..626b3ec Binary files /dev/null and b/static/images/chat-bot.png differ diff --git a/images/collection_01.png b/static/images/collection_01.png similarity index 100% rename from images/collection_01.png rename to static/images/collection_01.png diff --git a/images/collection_02.png b/static/images/collection_02.png similarity index 100% rename from images/collection_02.png rename to static/images/collection_02.png diff --git a/static/images/dashbord.png b/static/images/dashbord.png new file mode 100644 index 0000000..60c58db Binary files /dev/null and b/static/images/dashbord.png differ diff --git a/images/favicon.ico b/static/images/favicon.ico similarity index 100% rename from images/favicon.ico rename to static/images/favicon.ico diff --git a/static/images/gemini.png b/static/images/gemini.png new file mode 100644 index 0000000..0512be6 Binary files /dev/null and b/static/images/gemini.png differ diff --git a/static/images/news1.jpg b/static/images/news1.jpg new file mode 100644 index 0000000..d3f2639 Binary files /dev/null and b/static/images/news1.jpg differ diff --git a/static/images/news2.jpg b/static/images/news2.jpg new file mode 100644 index 0000000..3200df8 Binary files /dev/null and b/static/images/news2.jpg differ diff --git a/static/images/news3.jpg b/static/images/news3.jpg new file mode 100644 index 0000000..0c2aef6 Binary files /dev/null and b/static/images/news3.jpg differ diff --git a/images/news4.jpg b/static/images/news4.jpg similarity index 100% rename from images/news4.jpg rename to static/images/news4.jpg diff --git a/static/images/news4.png b/static/images/news4.png new file mode 100644 index 0000000..c392fde Binary files /dev/null and b/static/images/news4.png differ diff --git a/static/images/news5.jpg b/static/images/news5.jpg new file mode 100644 index 0000000..509cc21 Binary files /dev/null and b/static/images/news5.jpg differ diff --git a/images/popup.jpg b/static/images/popup.jpg similarity index 100% rename from images/popup.jpg rename to static/images/popup.jpg diff --git a/images/popup2.jpg b/static/images/popup2.jpg similarity index 100% rename from images/popup2.jpg rename to static/images/popup2.jpg diff --git a/images/products/headphone/headphone1.jpeg b/static/images/products/headphone/headphone1.jpeg similarity index 100% rename from images/products/headphone/headphone1.jpeg rename to static/images/products/headphone/headphone1.jpeg diff --git a/images/products/headphone/headphone10.jpeg b/static/images/products/headphone/headphone10.jpeg similarity index 100% rename from images/products/headphone/headphone10.jpeg rename to static/images/products/headphone/headphone10.jpeg diff --git a/images/products/headphone/headphone11.jpeg b/static/images/products/headphone/headphone11.jpeg similarity index 100% rename from images/products/headphone/headphone11.jpeg rename to static/images/products/headphone/headphone11.jpeg diff --git a/images/products/headphone/headphone12.jpeg b/static/images/products/headphone/headphone12.jpeg similarity index 100% rename from images/products/headphone/headphone12.jpeg rename to static/images/products/headphone/headphone12.jpeg diff --git a/images/products/headphone/headphone2.jpeg b/static/images/products/headphone/headphone2.jpeg similarity index 100% rename from images/products/headphone/headphone2.jpeg rename to static/images/products/headphone/headphone2.jpeg diff --git a/images/products/headphone/headphone3.jpeg b/static/images/products/headphone/headphone3.jpeg similarity index 100% rename from images/products/headphone/headphone3.jpeg rename to static/images/products/headphone/headphone3.jpeg diff --git a/images/products/headphone/headphone4.jpeg b/static/images/products/headphone/headphone4.jpeg similarity index 100% rename from images/products/headphone/headphone4.jpeg rename to static/images/products/headphone/headphone4.jpeg diff --git a/images/products/headphone/headphone5.jpeg b/static/images/products/headphone/headphone5.jpeg similarity index 100% rename from images/products/headphone/headphone5.jpeg rename to static/images/products/headphone/headphone5.jpeg diff --git a/images/products/headphone/headphone6.jpeg b/static/images/products/headphone/headphone6.jpeg similarity index 100% rename from images/products/headphone/headphone6.jpeg rename to static/images/products/headphone/headphone6.jpeg diff --git a/images/products/headphone/headphone7.jpeg b/static/images/products/headphone/headphone7.jpeg similarity index 100% rename from images/products/headphone/headphone7.jpeg rename to static/images/products/headphone/headphone7.jpeg diff --git a/images/products/headphone/headphone8.jpeg b/static/images/products/headphone/headphone8.jpeg similarity index 100% rename from images/products/headphone/headphone8.jpeg rename to static/images/products/headphone/headphone8.jpeg diff --git a/images/products/headphone/headphone9.jpeg b/static/images/products/headphone/headphone9.jpeg similarity index 100% rename from images/products/headphone/headphone9.jpeg rename to static/images/products/headphone/headphone9.jpeg diff --git a/images/products/iPhone/iphone1.jpeg b/static/images/products/iPhone/iphone1.jpeg similarity index 100% rename from images/products/iPhone/iphone1.jpeg rename to static/images/products/iPhone/iphone1.jpeg diff --git a/images/products/iPhone/iphone2.jpeg b/static/images/products/iPhone/iphone2.jpeg similarity index 100% rename from images/products/iPhone/iphone2.jpeg rename to static/images/products/iPhone/iphone2.jpeg diff --git a/images/products/iPhone/iphone3.jpeg b/static/images/products/iPhone/iphone3.jpeg similarity index 100% rename from images/products/iPhone/iphone3.jpeg rename to static/images/products/iPhone/iphone3.jpeg diff --git a/images/products/iPhone/iphone4.jpeg b/static/images/products/iPhone/iphone4.jpeg similarity index 100% rename from images/products/iPhone/iphone4.jpeg rename to static/images/products/iPhone/iphone4.jpeg diff --git a/images/products/iPhone/iphone5.jpeg b/static/images/products/iPhone/iphone5.jpeg similarity index 100% rename from images/products/iPhone/iphone5.jpeg rename to static/images/products/iPhone/iphone5.jpeg diff --git a/images/products/iPhone/iphone6.jpeg b/static/images/products/iPhone/iphone6.jpeg similarity index 100% rename from images/products/iPhone/iphone6.jpeg rename to static/images/products/iPhone/iphone6.jpeg diff --git a/static/images/products/laptop/laptop24.jpg b/static/images/products/laptop/laptop24.jpg new file mode 100644 index 0000000..a5a399e Binary files /dev/null and b/static/images/products/laptop/laptop24.jpg differ diff --git a/static/images/products/laptop/laptop25.jpeg b/static/images/products/laptop/laptop25.jpeg new file mode 100644 index 0000000..040c41c Binary files /dev/null and b/static/images/products/laptop/laptop25.jpeg differ diff --git a/static/images/products/laptop/laptop26.jpg b/static/images/products/laptop/laptop26.jpg new file mode 100644 index 0000000..7ea93b8 Binary files /dev/null and b/static/images/products/laptop/laptop26.jpg differ diff --git a/static/images/products/laptop/laptop27.jpeg b/static/images/products/laptop/laptop27.jpeg new file mode 100644 index 0000000..9db2389 Binary files /dev/null and b/static/images/products/laptop/laptop27.jpeg differ diff --git a/static/images/products/laptop/laptop28.jpg b/static/images/products/laptop/laptop28.jpg new file mode 100644 index 0000000..7715151 Binary files /dev/null and b/static/images/products/laptop/laptop28.jpg differ diff --git a/static/images/products/laptop/laptop29.jpg b/static/images/products/laptop/laptop29.jpg new file mode 100644 index 0000000..43a964a Binary files /dev/null and b/static/images/products/laptop/laptop29.jpg differ diff --git a/static/images/products/laptop/laptop30.jpg b/static/images/products/laptop/laptop30.jpg new file mode 100644 index 0000000..d55da44 Binary files /dev/null and b/static/images/products/laptop/laptop30.jpg differ diff --git a/static/images/products/placeholder.png b/static/images/products/placeholder.png new file mode 100644 index 0000000..f99e43f --- /dev/null +++ b/static/images/products/placeholder.png @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII= \ No newline at end of file diff --git a/images/products/sumsung/samsung1.jpeg b/static/images/products/sumsung/samsung1.jpeg similarity index 100% rename from images/products/sumsung/samsung1.jpeg rename to static/images/products/sumsung/samsung1.jpeg diff --git a/images/products/sumsung/samsung2.jpeg b/static/images/products/sumsung/samsung2.jpeg similarity index 100% rename from images/products/sumsung/samsung2.jpeg rename to static/images/products/sumsung/samsung2.jpeg diff --git a/images/products/sumsung/samsung3.jpeg b/static/images/products/sumsung/samsung3.jpeg similarity index 100% rename from images/products/sumsung/samsung3.jpeg rename to static/images/products/sumsung/samsung3.jpeg diff --git a/images/products/sumsung/samsung4.jpeg b/static/images/products/sumsung/samsung4.jpeg similarity index 100% rename from images/products/sumsung/samsung4.jpeg rename to static/images/products/sumsung/samsung4.jpeg diff --git a/images/products/sumsung/samsung5.jpeg b/static/images/products/sumsung/samsung5.jpeg similarity index 100% rename from images/products/sumsung/samsung5.jpeg rename to static/images/products/sumsung/samsung5.jpeg diff --git a/images/products/sumsung/samsung6.jpeg b/static/images/products/sumsung/samsung6.jpeg similarity index 100% rename from images/products/sumsung/samsung6.jpeg rename to static/images/products/sumsung/samsung6.jpeg diff --git a/static/images/profile1.jpg b/static/images/profile1.jpg new file mode 100644 index 0000000..2230955 Binary files /dev/null and b/static/images/profile1.jpg differ diff --git a/static/images/profile2.jpg b/static/images/profile2.jpg new file mode 100644 index 0000000..79d2252 Binary files /dev/null and b/static/images/profile2.jpg differ diff --git a/static/images/profile3.jpg b/static/images/profile3.jpg new file mode 100644 index 0000000..e194a63 Binary files /dev/null and b/static/images/profile3.jpg differ diff --git a/static/images/profile4.jpg b/static/images/profile4.jpg new file mode 100644 index 0000000..03fd5bf Binary files /dev/null and b/static/images/profile4.jpg differ diff --git a/images/sprite.svg b/static/images/sprite.svg similarity index 100% rename from images/sprite.svg rename to static/images/sprite.svg diff --git a/images/testimonial.jpg b/static/images/testimonial.jpg similarity index 100% rename from images/testimonial.jpg rename to static/images/testimonial.jpg diff --git a/static/js/admin.js b/static/js/admin.js new file mode 100644 index 0000000..ebeb40a --- /dev/null +++ b/static/js/admin.js @@ -0,0 +1,505 @@ +async function fetchProducts() { + const res = await fetch('/api/products'); + return await res.json(); +} + +let allProducts = []; +let currentFiltered = []; +let selectedRowId = null; +let isLoading = false; +let eventsBound = false; + +function renderProducts(rows) { + const tbody = document.getElementById('productsTable'); + if (!tbody) return; + if (!rows.length) { + tbody.innerHTML = ''; + const empty = document.getElementById('emptyState'); + if (empty) empty.style.display = 'block'; + } else { + const empty = document.getElementById('emptyState'); + if (empty) empty.style.display = 'none'; + const newHtml = rows.map(r => { + const stock = r.stock || 0; + const stockClass = stock === 0 ? 'out-of-stock' : (stock < 10 ? 'low-stock' : ''); + const status = r.status || 'active'; + const brand = r.brand || 'Unknown'; + + return ` + + ${r.title} + ${brand} + $${parseFloat(r.price).toFixed(2)} + ${stock} + ${r.category} + ${status} +
+ + +
+ + `; + }).join(''); + + // Only update DOM when HTML actually changes to avoid unnecessary reflows and image reloads + if (tbody.innerHTML !== newHtml) { + tbody.innerHTML = newHtml; + } + } + updateMeta(); + highlightSelected(); +} + +function updateMeta() { + const meta = document.getElementById('productMeta'); + if (meta) { + meta.textContent = `${currentFiltered.length} of ${allProducts.length} products`; + } +} + +function applyFilters() { + const term = document.getElementById('productSearch')?.value.trim().toLowerCase() || ''; + const cat = document.getElementById('categoryFilter')?.value || ''; + currentFiltered = allProducts.filter(p => { + const matchesTerm = !term || p.title.toLowerCase().includes(term) || String(p.id).includes(term) || p.category.toLowerCase().includes(term); + const matchesCat = !cat || p.category === cat; + return matchesTerm && matchesCat; + }); + renderProducts(currentFiltered); +} + +async function load() { + if (isLoading) return; // prevent concurrent loads + isLoading = true; + try { + allProducts = await fetchProducts(); + currentFiltered = [...allProducts]; + renderProducts(currentFiltered); + } finally { + isLoading = false; + } +} + +function setImagePreview(path) { + const wrap = document.getElementById('imagePreview'); + const img = document.getElementById('imagePreviewImg'); + if (!wrap || !img) return; + if (path) { + img.src = path; + wrap.style.display = 'block'; + } else { + img.src = ''; + wrap.style.display = 'none'; + } +} + +// Clear the create/edit product form completely (including file input and preview) +function clearProductForm() { + const form = document.getElementById('productForm'); + if (form) form.reset(); + const p_image = document.getElementById('p_image'); + if (p_image) p_image.value = ''; + const fileInput = document.getElementById('p_image_file'); + if (fileInput) { + // revoke any created object URL + if (fileInput._objectURL) { + try { URL.revokeObjectURL(fileInput._objectURL); } catch (e) {} + fileInput._objectURL = null; + } + try { fileInput.value = ''; } catch (e) {} + } + setImagePreview(''); + selectedRowId = null; + highlightSelected(); +} + +function showNotification(message, type = 'info') { + // Create or reuse notification element + let notification = document.getElementById('admin-notification'); + if (!notification) { + notification = document.createElement('div'); + notification.id = 'admin-notification'; + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + z-index: 10000; + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease; + font-family: 'Archivo', sans-serif; + font-weight: 500; + max-width: 350px; + `; + document.body.appendChild(notification); + } + + // Set colors based on type + const colors = { + success: { bg: '#4CAF50', text: 'white' }, + error: { bg: '#f44336', text: 'white' }, + warning: { bg: '#ff9800', text: 'white' }, + info: { bg: '#2196F3', text: 'white' } + }; + + const color = colors[type] || colors.info; + notification.style.backgroundColor = color.bg; + notification.style.color = color.text; + notification.textContent = message; + + // Show notification + notification.style.opacity = '1'; + notification.style.transform = 'translateX(0)'; + + // Hide after 4 seconds + setTimeout(() => { + notification.style.opacity = '0'; + notification.style.transform = 'translateX(100%)'; + }, 4000); +} + +function buildFormData(data) { + const fd = new FormData(); + Object.entries(data).forEach(([k,v]) => fd.append(k, v)); + const fileInput = document.getElementById('p_image_file'); + if (fileInput && fileInput.files && fileInput.files[0]) { + fd.append('imageFile', fileInput.files[0]); + } + return fd; +} + +async function saveProduct(data) { + const method = 'POST'; + let body; let headers = {}; + const fileInput = document.getElementById('p_image_file'); + if (fileInput && fileInput.files && fileInput.files[0]) { + body = buildFormData(data); + } else { + headers['Content-Type'] = 'application/json'; + body = JSON.stringify(data); + } + const res = await fetch('/api/products', { method, headers, body }); + return await res.json(); +} + +async function updateProduct(id, data) { + let body; let headers = {}; + const fileInput = document.getElementById('p_image_file'); + if (fileInput && fileInput.files && fileInput.files[0]) { + body = buildFormData(data); + } else { + headers['Content-Type'] = 'application/json'; + body = JSON.stringify(data); + } + const res = await fetch(`/api/products/${id}`, { + method: 'PUT', + headers, + body + }); + return await res.json(); +} + +async function deleteProduct(id) { + const res = await fetch(`/api/products/${id}`, { method: 'DELETE' }); + return await res.json(); +} + +function populateForm(r) { + document.getElementById('p_id').value = r.id; + document.getElementById('p_title').value = r.title; + document.getElementById('p_brand').value = r.brand || ''; + // prefer dynamic served image_url if present + const path = r.image_url || r.image || ''; + document.getElementById('p_image').value = path; + setImagePreview(path); + document.getElementById('p_price').value = r.price; + document.getElementById('p_stock').value = r.stock || 0; + document.getElementById('p_category').value = r.category; + document.getElementById('p_status').value = r.status || 'active'; + const fileInput = document.getElementById('p_image_file'); + if (fileInput) fileInput.value = ''; + + // populate description and specs + const descEl = document.getElementById('p_description'); + const specsEl = document.getElementById('p_specs'); + const colorsEl = document.getElementById('p_colors'); + const storageEl = document.getElementById('p_storage'); + + if (descEl) descEl.value = r.description || ''; + if (colorsEl) colorsEl.value = Array.isArray(r.colors) ? r.colors.join(', ') : (r.colors || ''); + if (storageEl) storageEl.value = Array.isArray(r.storage_options) ? r.storage_options.join(', ') : (r.storage_options || ''); + + if (specsEl) { + if (!r.specs) specsEl.value = ''; + else if (typeof r.specs === 'string') specsEl.value = r.specs; + else if (typeof r.specs === 'object') specsEl.value = Object.entries(r.specs).map(([k,v]) => `${k}: ${v}`).join('\n'); + } +} + +function highlightSelected() { + const rows = document.querySelectorAll('#productsTable tr'); + rows.forEach(tr => { + if (Number(tr.dataset.id) === selectedRowId) tr.classList.add('selected'); + else tr.classList.remove('selected'); + }); +} + +function exportCsv() { + const rows = currentFiltered.length ? currentFiltered : allProducts; + if (!rows.length) return alert('No products to export'); + // Export columns: ID, Title, Brand, Price, Stock, Category, Status + const header = ['ID','Title','Brand','Price','Stock','Category','Status']; + const body = rows.map(r => [ + r.id, + String(r.title || '').replace(/"/g,'""'), + String(r.brand || '').replace(/"/g,'""'), + typeof r.price !== 'undefined' ? r.price : '', + typeof r.stock !== 'undefined' ? r.stock : '', + String(r.category || '').replace(/"/g,'""'), + String(r.status || '').replace(/"/g,'""') + ]); + const csv = [header.join(','), ...body.map(r => r.map(x => /[",\n]/.test(String(x)) ? `"${x}"` : x).join(','))].join('\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'products.csv'; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(a.href); +} + +let pdfLibPromise = null; +function ensurePdfLib() { + if (pdfLibPromise) return pdfLibPromise; + pdfLibPromise = new Promise((resolve, reject) => { + // If already loaded + if (window.jsPDF || (window.jspdf && window.jspdf.jsPDF)) return resolve(); + const cdns = [ + 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js', + 'https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js' + ]; + let loaded = false; + let attempts = 0; + function loadNext() { + if (attempts >= cdns.length) { + return reject(new Error('Failed to load jsPDF from all CDNs')); + } + const src = cdns[attempts++]; + const script = document.createElement('script'); + script.src = src; + script.async = true; + script.onload = () => { + if (window.jsPDF || (window.jspdf && window.jspdf.jsPDF)) { + loaded = true; + resolve(); + } else { + loadNext(); + } + }; + script.onerror = () => loadNext(); + document.head.appendChild(script); + } + loadNext(); + }).then(() => new Promise((res) => { + // small defer to ensure globals settle + setTimeout(res, 20); + })); + return pdfLibPromise; +} + +async function exportPdf() { + const rows = (currentFiltered.length ? currentFiltered : allProducts).map(r => ({ + id: r.id, + title: r.title, + brand: r.brand || '', + price: r.price, + stock: r.stock, + category: r.category, + status: r.status || '' + })); + if (!rows.length) return alert('No products to export'); + + const btn = document.getElementById('exportPdf'); + if (btn) { btn.disabled = true; btn.textContent = 'Loading...'; } + try { + await ensurePdfLib(); + const JsPDFCtor = window.jsPDF || (window.jspdf && window.jspdf.jsPDF); + if (!JsPDFCtor) throw new Error('jsPDF not available after load'); + const doc = new JsPDFCtor({ orientation: 'p', unit: 'pt', format: 'a4' }); + + const marginX = 40; + const marginY = 50; + doc.setFont('helvetica',''); + doc.setFontSize(16); + doc.text('Products Export', marginX, marginY - 15); + doc.setFontSize(10); + const generated = new Date().toLocaleString(); + doc.text(`Generated: ${generated}`, marginX, marginY - 2); + + const head = [['ID','Title','Brand','Price','Stock','Category','Status']]; + const body = rows.map(r => [r.id, r.title, r.brand, `$${r.price}`, r.stock, r.category, r.status]); + + if (doc.autoTable) { + doc.autoTable({ + head, + body, + startY: marginY, + styles: { fontSize: 9, cellPadding: 4 }, + headStyles: { fillColor: [255,140,0], textColor: 255, halign: 'left' }, + columnStyles: { 0: { cellWidth: 40 }, 1: { cellWidth: 180 }, 2: { cellWidth: 110 }, 3: { cellWidth: 70, halign: 'right' }, 4: { cellWidth: 60 }, 5: { cellWidth: 110 }, 6: { cellWidth: 80 } }, + didParseCell: (data) => { + if (data.section === 'body' && data.column.index === 3) { + data.cell.styles.halign = 'right'; + } + } + }); + } else { + // Fallback simple list if autotable failed to load + let y = marginY; + doc.setFontSize(11); + doc.text(head[0].join(' | '), marginX, y); + y += 16; + doc.setFontSize(9); + body.forEach(row => { + doc.text(row.join(' | '), marginX, y); + y += 14; + if (y > 780) { // basic page break + doc.addPage(); + y = marginY; + } + }); + } + + doc.save('products.pdf'); + } catch (err) { + console.error(err); + alert('Failed to generate PDF: ' + err.message); + } finally { + if (btn) { btn.disabled = false; btn.textContent = 'Export PDF'; } + } +} + +function bindEvents() { + if (eventsBound) return; // avoid binding multiple times + eventsBound = true; + const form = document.getElementById('productForm'); + const resetBtn = document.getElementById('resetForm'); + const tbody = document.getElementById('productsTable'); + const search = document.getElementById('productSearch'); + const filter = document.getElementById('categoryFilter'); + const exportBtn = document.getElementById('exportCsv'); + const exportPdfBtn = document.getElementById('exportPdf'); + const fileInput = document.getElementById('p_image_file'); + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + + // Parse colors and storage options from comma-separated strings + const colorsValue = document.getElementById('p_colors')?.value || ''; + const storageValue = document.getElementById('p_storage')?.value || ''; + + const data = { + id: Number(document.getElementById('p_id').value), + title: document.getElementById('p_title').value, + brand: document.getElementById('p_brand')?.value || '', + image: document.getElementById('p_image').value, + description: document.getElementById('p_description')?.value || '', + specs: document.getElementById('p_specs')?.value || '', + price: Number(document.getElementById('p_price').value), + stock: Number(document.getElementById('p_stock')?.value) || 0, + category: document.getElementById('p_category').value, + status: document.getElementById('p_status')?.value || 'active', + colors: colorsValue ? colorsValue.split(',').map(c => c.trim()).filter(c => c) : [], + storage_options: storageValue ? storageValue.split(',').map(s => s.trim()).filter(s => s) : [] + }; + + // If product exists (update) else create + const existing = allProducts.find(p => p.id === data.id); + const resp = existing ? await updateProduct(data.id, data) : await saveProduct(data); + + if (resp.success) { + await load(); + // fully reset the form including preview and hidden inputs + clearProductForm(); + selectedRowId = null; + highlightSelected(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + // Show success message + showNotification(existing ? 'Product updated successfully!' : 'Product created successfully!', 'success'); + } else { + alert(resp.message || 'Failed to save'); + } + }); + + resetBtn.addEventListener('click', () => { clearProductForm(); }); + + tbody.addEventListener('click', async (e) => { + const btn = e.target.closest('button'); + const row = e.target.closest('tr'); + if (row && !btn) { // row click selection + selectedRowId = Number(row.dataset.id); + highlightSelected(); + } + if (!btn) return; + const id = Number(btn.dataset.id); + if (btn.classList.contains('edit')) { + const prod = allProducts.find(x => x.id === id); + if (prod) { + // revoke any previous object URL to avoid stale previews + const fileInput = document.getElementById('p_image_file'); + if (fileInput && fileInput._objectURL) { + try { URL.revokeObjectURL(fileInput._objectURL); } catch (e) {} + fileInput._objectURL = null; + } + populateForm(prod); + selectedRowId = id; + highlightSelected(); + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + } else if (btn.classList.contains('delete')) { + if (confirm('Delete this product?')) { + const resp = await deleteProduct(id); + if (resp.success) { + if (selectedRowId === id) selectedRowId = null; + await load(); + // clear form if deleted product was being edited + clearProductForm(); + showNotification('Product deleted successfully!', 'success'); + } else { + alert(resp.message || 'Failed to delete'); + } + } + } + }); + + search?.addEventListener('input', () => applyFilters()); + filter?.addEventListener('change', () => applyFilters()); + exportBtn?.addEventListener('click', exportCsv); + exportPdfBtn?.addEventListener('click', exportPdf); + if (fileInput) { + fileInput.addEventListener('change', () => { + const file = fileInput.files && fileInput.files[0]; + if (file) { + // revoke previous object URL if any + if (fileInput._objectURL) { + try { URL.revokeObjectURL(fileInput._objectURL); } catch (e) {} + fileInput._objectURL = null; + } + const url = URL.createObjectURL(file); + fileInput._objectURL = url; + setImagePreview(url); + } else { + setImagePreview(document.getElementById('p_image').value); + } + }); + } +} + +document.addEventListener('DOMContentLoaded', async () => { + await load(); + bindEvents(); +}); diff --git a/static/js/auth.js b/static/js/auth.js new file mode 100644 index 0000000..d4360c7 --- /dev/null +++ b/static/js/auth.js @@ -0,0 +1,562 @@ +// Helper for API requests +async function api(url, data) { + const res = await fetch(url, { + method: "POST", + headers: {"Content-Type": "application/json"}, + credentials: "same-origin", // ensure cookies (session) are sent/received + body: JSON.stringify(data) + }); + let responseData; + try { + responseData = await res.json(); + } catch { + responseData = {success: false, message: "Invalid server response."}; + } + if (!res.ok) { + responseData.success = false; + responseData.message = responseData.message || "Server error"; + } + return responseData; +} + +// Check authentication status with server +async function checkAuthStatus() { + try { + const response = await fetch('/api/check-auth', { credentials: 'same-origin' }); + const data = await response.json(); + return data; + } catch (error) { + console.error('Error checking auth status:', error); + return { authenticated: false }; + } +} + +// Update UI based on authentication status +async function updateAuthUI() { + const authStatus = await checkAuthStatus(); + const loginRegisterLinks = document.getElementById('loginRegisterLinks'); + const loggedInLinks = document.getElementById('loggedInLinks'); + const welcomeMessage = document.getElementById('welcomeMessage'); + + if (authStatus.authenticated) { + // User is logged in + if (loginRegisterLinks) loginRegisterLinks.style.display = 'none'; + if (loggedInLinks) loggedInLinks.style.display = 'flex'; + if (welcomeMessage) welcomeMessage.textContent = `Welcome, ${authStatus.name}`; + + // Update nav icons + updateNavIcons(true, authStatus.name); + } else { + // User is logged out + if (loginRegisterLinks) loginRegisterLinks.style.display = 'flex'; + if (loggedInLinks) loggedInLinks.style.display = 'none'; + + // Update nav icons + updateNavIcons(false); + } +} + +// Update navigation icons based on login status +function updateNavIcons(isLoggedIn, userName = '') { + const navIcons = document.querySelector('.nav__icons'); + if (!navIcons) return; + + // Remove previous logout button if any + const oldLogout = document.getElementById('logout-btn'); + if (oldLogout) oldLogout.remove(); + + // Find login link + const loginLink = navIcons.querySelector('.icon__item[href="/login/"]'); + + if (isLoggedIn) { + // Hide login button + if (loginLink) loginLink.style.display = 'none'; + + // Add logout button + const logoutBtn = document.createElement('a'); + logoutBtn.href = "#"; + logoutBtn.className = "icon__item logout-btn"; + logoutBtn.id = "logout-btn"; + logoutBtn.innerHTML = ` + + Logout + `; + navIcons.appendChild(logoutBtn); + + logoutBtn.addEventListener('click', async function(e) { + e.preventDefault(); + try { + await fetch('/api/logout', { method: 'POST', credentials: 'same-origin' }); + window.location.href = '/'; + } catch (error) { + console.error('Logout error:', error); + window.location.href = '/'; + } + }); + } else { + // Show login button + if (loginLink) loginLink.style.display = ''; + } +} + +document.addEventListener('DOMContentLoaded', function() { + // Initialize auth UI + updateAuthUI(); + + // Tab Switching Logic + const loginTab = document.getElementById('loginTab'); + const registerTab = document.getElementById('registerTab'); + const loginForm = document.getElementById('loginForm'); + const registerForm = document.getElementById('registerForm'); + const forgotPasswordForm = document.getElementById('forgotPasswordForm'); + const resetPasswordForm = document.getElementById('resetPasswordForm'); + const otpVerifyForm = document.getElementById('otpVerifyForm'); + const forgotPasswordLink = document.getElementById('forgotPasswordLink'); + const backToLoginLink = document.getElementById('backToLoginLink'); + const loginTabLink = document.getElementById('loginTabLink'); + const backToLoginFromResetLink = document.getElementById('backToLoginFromResetLink'); + const otpBackToLoginLink = document.getElementById('otpBackToLoginLink'); + const resendOtpLink = document.getElementById('resendOtpLink'); + const otpCountdownEl = document.getElementById('otpCountdown'); + + let otpTimer = null; + let otpTimeLeft = 60; // seconds (extended from 30) + let currentOtpEmail = null; + + function showForm(formToShow) { + [loginForm, registerForm, forgotPasswordForm, resetPasswordForm, otpVerifyForm].forEach(form => { + if (form) form.classList.remove('active'); + }); + if (formToShow) formToShow.classList.add('active'); + } + + function activateTab(tabToActivate) { + [loginTab, registerTab].forEach(tab => { if (tab) tab.classList.remove('active'); }); + if (tabToActivate) tabToActivate.classList.add('active'); + } + + // Tab click listeners (restored) + if (loginTab) { + loginTab.addEventListener('click', (e) => { + e.preventDefault(); + showForm(loginForm); + activateTab(loginTab); + }); + } + if (registerTab) { + registerTab.addEventListener('click', (e) => { + e.preventDefault(); + showForm(registerForm); + activateTab(registerTab); + }); + } + if (forgotPasswordLink) { + forgotPasswordLink.addEventListener('click', (e) => { + e.preventDefault(); + showForm(forgotPasswordForm); + activateTab(null); // no tab highlighted + }); + } + if (loginTabLink) { + loginTabLink.addEventListener('click', (e) => { + e.preventDefault(); + showForm(loginForm); + activateTab(loginTab); + }); + } + if (backToLoginLink) { + backToLoginLink.addEventListener('click', (e) => { + e.preventDefault(); + showForm(loginForm); + activateTab(loginTab); + }); + } + if (backToLoginFromResetLink) { + backToLoginFromResetLink.addEventListener('click', (e) => { + e.preventDefault(); + showForm(loginForm); + activateTab(loginTab); + }); + } + + function startOtpCountdown() { + if (resendOtpLink) { + resendOtpLink.style.opacity = '0.5'; + resendOtpLink.style.pointerEvents = 'none'; + } + otpTimeLeft = 60; // reset to 60s now + updateOtpCountdownDisplay(); + if (otpTimer) clearInterval(otpTimer); + otpTimer = setInterval(() => { + otpTimeLeft--; + updateOtpCountdownDisplay(); + if (otpTimeLeft <= 0) { + clearInterval(otpTimer); + if (otpCountdownEl) otpCountdownEl.textContent = 'You can resend the code now'; + if (resendOtpLink) { + resendOtpLink.style.opacity = '1'; + resendOtpLink.style.pointerEvents = 'auto'; + } + } + }, 1000); + } + + function updateOtpCountdownDisplay() { + if (otpCountdownEl) { + otpCountdownEl.textContent = `Resend available in ${otpTimeLeft}s`; + } + } + + if (resendOtpLink) { + resendOtpLink.addEventListener('click', async (e) => { + e.preventDefault(); + if (!currentOtpEmail) return; + // Only allow if timer finished + if (otpTimeLeft > 0) return; + try { + await api('/api/forgot-password', { email: currentOtpEmail }); + startOtpCountdown(); + } catch (err) { + console.error('Resend OTP failed', err); + } + }); + } + + if (otpBackToLoginLink) { + otpBackToLoginLink.addEventListener('click', (e) => { + e.preventDefault(); + showForm(loginForm); + activateTab(loginTab); + }); + } + + // Override forgot password submit to go to OTP form first + if (forgotPasswordForm) { + forgotPasswordForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const email = forgotPasswordForm.querySelector('#forgotPasswordEmail').value.trim(); + const errorDiv = document.getElementById('forgotPasswordError'); + const successDiv = document.getElementById('forgotPasswordSuccess'); + const submitBtn = forgotPasswordForm.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + + errorDiv.style.display = 'none'; + successDiv.style.display = 'none'; + submitBtn.textContent = 'Sending Code...'; + submitBtn.disabled = true; + + if (!email) { + errorDiv.textContent = 'Please enter your email.'; + errorDiv.style.display = 'block'; + submitBtn.textContent = originalText; + submitBtn.disabled = false; + return; + } + try { + const response = await api('/api/forgot-password', { email }); + if (response.success) { + currentOtpEmail = email; + if (otpVerifyForm) { + otpVerifyForm.querySelector('#otpEmail').value = email; + const masked = maskEmail(email); + const displayEl = document.getElementById('otpEmailDisplay'); + if (displayEl) displayEl.textContent = masked; + window.lastVerifiedOtpCode = null; // reset previously verified code + showForm(otpVerifyForm); + startOtpCountdown(); + } + } else { + errorDiv.textContent = response.message || 'Request failed.'; + errorDiv.style.display = 'block'; + } + } catch (err) { + errorDiv.textContent = 'Could not connect to server.'; + errorDiv.style.display = 'block'; + } finally { + submitBtn.textContent = originalText; + submitBtn.disabled = false; + } + }); + } + + // OTP verification submit + if (otpVerifyForm) { + otpVerifyForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const email = otpVerifyForm.querySelector('#otpEmail').value.trim(); + const code = otpVerifyForm.querySelector('#otpCode').value.trim(); + const errorDiv = document.getElementById('otpVerifyError'); + const successDiv = document.getElementById('otpVerifySuccess'); + const submitBtn = otpVerifyForm.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + + errorDiv.style.display = 'none'; + successDiv.style.display = 'none'; + submitBtn.textContent = 'Verifying...'; + submitBtn.disabled = true; + + if (!email || !code) { + errorDiv.textContent = 'Please enter the code.'; + errorDiv.style.display = 'block'; + submitBtn.textContent = originalText; + submitBtn.disabled = false; + return; + } + try { + const response = await api('/api/verify-reset-code', { email, code }); + if (response.success) { + successDiv.textContent = 'Code verified. Continue to reset password.'; + successDiv.style.display = 'block'; + window.lastVerifiedOtpCode = code; // store verified code for final reset + // Move to reset form shortly + setTimeout(() => { + if (resetPasswordForm) { + resetPasswordForm.querySelector('#resetEmail').value = email; + showForm(resetPasswordForm); + } + }, 800); + } else { + errorDiv.textContent = response.message || 'Invalid code.'; + errorDiv.style.display = 'block'; + } + } catch (err) { + errorDiv.textContent = 'Could not connect to server.'; + errorDiv.style.display = 'block'; + } finally { + submitBtn.textContent = originalText; + submitBtn.disabled = false; + } + }); + } + + // LOGIN handler (restored) + if (loginForm) { + loginForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const email = loginForm.querySelector('#loginEmail').value.trim(); + const password = loginForm.querySelector('#loginPassword').value; + const errorDiv = document.getElementById('loginError'); + const submitBtn = loginForm.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + errorDiv.style.display = 'none'; + submitBtn.textContent = 'Logging in...'; + submitBtn.disabled = true; + if (!email || !password) { + errorDiv.textContent = 'Please enter email and password.'; + errorDiv.style.display = 'block'; + submitBtn.textContent = originalText; + submitBtn.disabled = false; + return; + } + try { + const response = await api('/api/login', { email, password }); + if (response.success) { + window.location.href = '/'; + } else { + errorDiv.textContent = response.message || 'Login failed.'; + errorDiv.style.display = 'block'; + } + } catch (err) { + errorDiv.textContent = 'Could not connect to server.'; + errorDiv.style.display = 'block'; + } finally { + submitBtn.textContent = originalText; + submitBtn.disabled = false; + } + }); + } + + // REGISTER handler (restored) + if (registerForm) { + registerForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const name = registerForm.querySelector('#registerName').value.trim(); + const email = registerForm.querySelector('#registerEmail').value.trim(); + const password = registerForm.querySelector('#registerPassword').value; + const confirmPassword = registerForm.querySelector('#confirmPassword').value; + const errorDiv = document.getElementById('registerError'); + const successDiv = document.getElementById('registerSuccess'); + const submitBtn = registerForm.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + errorDiv.style.display = 'none'; + successDiv.style.display = 'none'; + submitBtn.textContent = 'Creating Account...'; + submitBtn.disabled = true; + if (!name || !email || !password || !confirmPassword) { + errorDiv.textContent = 'Please fill out all fields.'; + errorDiv.style.display = 'block'; + submitBtn.textContent = originalText; + submitBtn.disabled = false; + return; + } + if (password !== confirmPassword) { + errorDiv.textContent = 'Passwords do not match.'; + errorDiv.style.display = 'block'; + submitBtn.textContent = originalText; + submitBtn.disabled = false; + return; + } + if (password.length < 6) { + errorDiv.textContent = 'Password should be at least 6 characters.'; + errorDiv.style.display = 'block'; + submitBtn.textContent = originalText; + submitBtn.disabled = false; + return; + } + try { + const response = await api('/api/register', { name, email, password }); + if (response.success) { + successDiv.textContent = response.message || 'Registration successful. You can now log in.'; + successDiv.style.display = 'block'; + registerForm.reset(); + setTimeout(() => { showForm(loginForm); activateTab(loginTab); }, 1500); + } else { + errorDiv.textContent = response.message || 'Register failed.'; + errorDiv.style.display = 'block'; + } + } catch (err) { + errorDiv.textContent = 'Could not connect to server.'; + errorDiv.style.display = 'block'; + } finally { + submitBtn.textContent = originalText; + submitBtn.disabled = false; + } + }); + } + + // Adjust reset password submit to not require code now + if (resetPasswordForm) { + resetPasswordForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const email = resetPasswordForm.querySelector('#resetEmail').value.trim(); + // The code has already been verified; pass a placeholder? Better to keep original code? We'll store last entered code. + const newPassword = resetPasswordForm.querySelector('#resetNewPassword').value; + const confirmNewPassword = resetPasswordForm.querySelector('#resetConfirmNewPassword').value; + const errorDiv = document.getElementById('resetPasswordError'); + const successDiv = document.getElementById('resetPasswordSuccess'); + const submitBtn = resetPasswordForm.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + + errorDiv.style.display = 'none'; + successDiv.style.display = 'none'; + submitBtn.textContent = 'Resetting Password...'; + submitBtn.disabled = true; + + if (!email || !newPassword || !confirmNewPassword) { + errorDiv.textContent = 'Please fill out all fields.'; + errorDiv.style.display = 'block'; + submitBtn.textContent = originalText; + submitBtn.disabled = false; + return; + } + if (newPassword !== confirmNewPassword) { + errorDiv.textContent = 'Passwords do not match.'; + errorDiv.style.display = 'block'; + submitBtn.textContent = originalText; + submitBtn.disabled = false; + return; + } + if (newPassword.length < 6) { + errorDiv.textContent = 'Password should be at least 6 characters.'; + errorDiv.style.display = 'block'; + submitBtn.textContent = originalText; + submitBtn.disabled = false; + return; + } + try { + // We still need the verified code for backend; store it after verify + // We'll attach last verified code to a global variable when code verified + const code = window.lastVerifiedOtpCode; + if (!code) { + errorDiv.textContent = 'Session expired. Please request a new code.'; + errorDiv.style.display = 'block'; + submitBtn.textContent = originalText; + submitBtn.disabled = false; + return; + } + const response = await api('/api/reset-password', { email, code, newPassword }); + if (response.success) { + successDiv.textContent = 'Password reset successful! Redirecting to login...'; + successDiv.style.display = 'block'; + // Show popup + showPasswordResetPopup(); + setTimeout(() => { + showForm(loginForm); + activateTab(loginTab); + }, 2500); + } else { + errorDiv.textContent = response.message || 'Reset failed.'; + errorDiv.style.display = 'block'; + } + } catch (err) { + errorDiv.textContent = 'Could not connect to server.'; + errorDiv.style.display = 'block'; + } finally { + submitBtn.textContent = originalText; + submitBtn.disabled = false; + } + }); + } + + // Store verified code globally when success + window.lastVerifiedOtpCode = null; + // Modify otp verify handler to set it (we inserted above, so patch manually if needed) + // We'll redefine the listener if necessary (already done above). + + function showPasswordResetPopup() { + let popup = document.getElementById('passwordResetPopup'); + if (!popup) { + popup = document.createElement('div'); + popup.id = 'passwordResetPopup'; + popup.style.position = 'fixed'; + popup.style.top = '20px'; + popup.style.right = '20px'; + popup.style.padding = '1.5rem 2rem'; + popup.style.background = 'var(--green, #28a745)'; + popup.style.color = '#fff'; + popup.style.borderRadius = '6px'; + popup.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; + popup.style.fontSize = '1.4rem'; + popup.style.zIndex = '9999'; + popup.style.opacity = '0'; + popup.style.transition = 'opacity .3s ease'; + popup.innerText = 'Password successfully reset!'; + document.body.appendChild(popup); + requestAnimationFrame(() => { popup.style.opacity = '1'; }); + setTimeout(() => { + popup.style.opacity = '0'; + setTimeout(() => popup.remove(), 400); + }, 2000); + } + } + + function maskEmail(email) { + if (!email || !email.includes('@')) return email || ''; + const [user, domain] = email.split('@'); + if (user.length <= 2) return user[0] + '*@' + domain; + const visibleStart = user.slice(0, 2); + const masked = '*'.repeat(Math.max(2, user.length - 2)); + return `${visibleStart}${masked}@${domain}`; + } + + if (otpVerifyForm) { + const otpInput = document.getElementById('otpCode'); + if (otpInput) { + otpInput.addEventListener('input', () => { + // Strip non-digits and limit length + let v = otpInput.value.replace(/[^0-9]/g, '').slice(0,6); + otpInput.value = v; + if (v.length === 6) { + otpInput.setCustomValidity(''); + } + }); + otpInput.addEventListener('invalid', () => { + if (otpInput.value.length !== 6) { + otpInput.setCustomValidity('Enter 6 digit code'); + } else { + otpInput.setCustomValidity(''); + } + }); + } + } +}); \ No newline at end of file diff --git a/static/js/cart.js b/static/js/cart.js new file mode 100644 index 0000000..88996fa --- /dev/null +++ b/static/js/cart.js @@ -0,0 +1,616 @@ +/** + * Dynamic Cart Management System + * Handles all cart operations with real-time updates and user feedback + */ + +class CartManager { + constructor() { + this.cartCountElement = document.getElementById('cart__total'); + console.log('Cart Manager initialized'); + console.log('Cart count element:', this.cartCountElement); + // Ensure cart badge shows 0 by default until we fetch the real count + if (this.cartCountElement) this.cartCountElement.textContent = '0'; + this.init(); + } + + init() { + // Initialize cart count on page load + this.updateCartCount(); + + // Add event listeners for cart buttons + this.addEventListeners(); + + // If on cart page, load cart items + if (window.location.pathname === '/cart/') { + this.loadCartPage(); + } + } + + addEventListeners() { + // Add to cart buttons (both static and dynamic) + document.addEventListener('click', (e) => { + // Handle both direct buttons and buttons inside links + const button = e.target.closest('.product__btn'); + if (button) { + e.preventDefault(); + e.stopPropagation(); + this.handleAddToCart(button); + return; + } + + // Cart page quantity controls + if (e.target.matches('.plus-btn, .plus-btn svg, .plus-btn use')) { + e.preventDefault(); + this.handleQuantityChange(e.target, 'increase'); + } + + if (e.target.matches('.minus-btn, .minus-btn svg, .minus-btn use')) { + e.preventDefault(); + this.handleQuantityChange(e.target, 'decrease'); + } + + // Remove from cart + if (e.target.matches('.remove__cart-item, .remove__cart-item svg, .remove__cart-item use')) { + e.preventDefault(); + this.handleRemoveFromCart(e.target); + } + }); + + // Quantity input direct change + document.addEventListener('change', (e) => { + if (e.target.matches('.counter-btn')) { + this.handleQuantityInputChange(e.target); + } + }); + } + + async handleAddToCart(button) { + try { + console.log('Add to cart clicked', button); + + // Extract product data from the product element + const productElement = button.closest('.product'); + console.log('Product element:', productElement); + + if (!productElement) { + this.showNotification('Product information not found', 'error'); + return; + } + + // Get product data + const productData = this.extractProductData(productElement); + console.log('Extracted product data:', productData); + + if (!productData) { + this.showNotification('Could not extract product data', 'error'); + return; + } + + // Show loading state + const originalText = button.textContent; + button.textContent = 'Adding...'; + button.disabled = true; + + // Add to cart via API + const response = await fetch('/api/cart/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + body: JSON.stringify({ + product_id: productData.id, + quantity: 1 + }) + }); + + if (response.status === 401) { + // Not authenticated -> redirect to login + this.redirectToLogin(); + return; + } + + const result = await response.json(); + console.log('API response:', result); + + if (result.success) { + this.showNotification(result.message, 'success'); + this.updateCartCount(result.cart_count); + + // Animate button + button.style.background = '#28a745'; + setTimeout(() => { + button.style.background = ''; + }, 1000); + } else { + this.showNotification(result.message || 'Failed to add to cart', 'error'); + } + + } catch (error) { + console.error('Error adding to cart:', error); + this.showNotification('Network error. Please try again.', 'error'); + } finally { + // Reset button state + setTimeout(() => { + button.textContent = originalText; + button.disabled = false; + }, 1000); + } + } + + extractProductData(productElement) { + try { + // Method 1: From data attributes (preferred for dynamic products) + let productId = productElement.dataset.productId; + + if (productId) { + // Get other data from DOM elements + const titleElement = productElement.querySelector('h3, .product__title'); + const priceElement = productElement.querySelector('h4, .product__price h4, .price span'); + const imgElement = productElement.querySelector('img'); + + return { + id: parseInt(productId), + title: titleElement ? titleElement.textContent.trim() : 'Product', + price: priceElement ? priceElement.textContent.replace('$', '').trim() : '0', + image: imgElement ? imgElement.src : '' + }; + } + + // Method 2: Extract from DOM elements (fallback for static products) + const imgElement = productElement.querySelector('img'); + const titleElement = productElement.querySelector('h3, .product__title'); + const priceElement = productElement.querySelector('h4, .product__price h4, .price span'); + + if (imgElement && titleElement && priceElement) { + // Extract ID from image path and product title + const imgSrc = imgElement.src; + const imageName = imgSrc.split('/').pop(); + const title = titleElement.textContent.trim().toLowerCase(); + + console.log('Extracting from:', { imageName, title, imgSrc }); + + // Enhanced mapping for both image names and titles + let mappedId = null; + + // Map by image name first + const imageToIdMap = { + 'iphone1.jpeg': 1001, 'iphone2.jpeg': 1002, 'iphone3.jpeg': 1003, + 'iphone4.jpeg': 1004, 'iphone5.jpeg': 1005, 'iphone6.jpeg': 1006, + 'samsung1.jpeg': 2001, 'samsung2.jpeg': 2002, 'samsung3.jpeg': 2003, + 'samsung4.jpeg': 2004, 'samsung5.jpeg': 2005, 'samsung6.jpeg': 2006, + 'headphone1.jpeg': 3001, 'headphone2.jpeg': 3002, 'headphone3.jpeg': 3003, + 'headphone4.jpeg': 3004, 'headphone5.jpeg': 3005, 'headphone6.jpeg': 3006, + 'headphone7.jpeg': 3007, 'headphone8.jpeg': 3008, 'headphone9.jpeg': 3009, + 'headphone10.jpeg': 3010, 'headphone11.jpeg': 3011, 'headphone12.jpeg': 3012 + }; + + mappedId = imageToIdMap[imageName]; + + // If no ID from image, try to map by product title + if (!mappedId) { + if (title.includes('iphone') || title.includes('apple')) { + // Default to iPhone 6 for Apple products + mappedId = 1006; + } else if (title.includes('samsung') || title.includes('galaxy')) { + // Default to Samsung Galaxy for Samsung products + mappedId = 2005; + } else if (title.includes('headphone') || title.includes('sony')) { + // Default to Sony headphones + mappedId = 3001; + } else { + // Generic fallback + mappedId = 1001; + } + } + + console.log('Mapped product ID:', mappedId); + + if (mappedId) { + return { + id: mappedId, + title: titleElement.textContent.trim(), + price: priceElement.textContent.replace('$', '').trim(), + image: imgElement.src + }; + } + } + + console.error('Could not extract product data from element:', productElement); + return null; + + } catch (error) { + console.error('Error extracting product data:', error); + return null; + } + } + + async handleQuantityChange(element, action) { + const row = element.closest('tr'); + if (!row) return; + + const quantityInput = row.querySelector('.counter-btn'); + const productId = this.getProductIdFromRow(row); + + if (!quantityInput || !productId) return; + + let currentQuantity = parseInt(quantityInput.value) || 1; + let newQuantity = action === 'increase' ? currentQuantity + 1 : currentQuantity - 1; + + // Ensure minimum quantity is 1 + if (newQuantity < 1) newQuantity = 1; + if (newQuantity > 10) newQuantity = 10; // Maximum limit + + await this.updateCartQuantity(productId, newQuantity, quantityInput, row); + } + + async handleQuantityInputChange(input) { + const row = input.closest('tr'); + if (!row) return; + + const productId = this.getProductIdFromRow(row); + let quantity = parseInt(input.value) || 1; + + // Validate quantity + if (quantity < 1) quantity = 1; + if (quantity > 10) quantity = 10; + + input.value = quantity; // Update input with validated value + + await this.updateCartQuantity(productId, quantity, input, row); + } + + async updateCartQuantity(productId, quantity, quantityInput, row) { + try { + const response = await fetch('/api/cart/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + body: JSON.stringify({ + product_id: productId, + quantity: quantity + }) + }); + + if (response.status === 401) { + this.redirectToLogin(); + return; + } + + const result = await response.json(); + + if (result.success) { + quantityInput.value = quantity; + this.updateRowTotal(row, quantity); + this.updateCartTotals(result.total); + this.updateCartCount(result.count); + this.showNotification('Quantity updated', 'success'); + } else { + this.showNotification(result.message || 'Failed to update quantity', 'error'); + } + + } catch (error) { + console.error('Error updating quantity:', error); + this.showNotification('Network error. Please try again.', 'error'); + } + } + + async handleRemoveFromCart(element) { + const row = element.closest('tr'); + if (!row) return; + + const productId = this.getProductIdFromRow(row); + if (!productId) return; + + if (!confirm('Are you sure you want to remove this item from your cart?')) { + return; + } + + try { + const response = await fetch('/api/cart/remove', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + body: JSON.stringify({ + product_id: productId + }) + }); + + if (response.status === 401) { + this.redirectToLogin(); + return; + } + + const result = await response.json(); + + if (result.success) { + // Animate row removal + row.style.transition = 'opacity 0.3s ease'; + row.style.opacity = '0'; + + setTimeout(() => { + row.remove(); + this.updateCartTotals(result.total); + this.updateCartCount(result.count); + + // Check if cart is empty + if (result.count === 0) { + this.showEmptyCart(); + } + }, 300); + + this.showNotification(result.message, 'success'); + } else { + this.showNotification(result.message || 'Failed to remove item', 'error'); + } + + } catch (error) { + console.error('Error removing from cart:', error); + this.showNotification('Network error. Please try again.', 'error'); + } + } + + getProductIdFromRow(row) { + // Try to get product ID from data attribute + let productId = row.dataset.productId; + + if (!productId) { + // Extract from image source + const img = row.querySelector('img'); + if (img) { + const imageName = img.src.split('/').pop(); + const imageToIdMap = { + 'iphone1.jpeg': 1001, 'iphone2.jpeg': 1002, 'iphone3.jpeg': 1003, + 'iphone4.jpeg': 1004, 'iphone5.jpeg': 1005, 'iphone6.jpeg': 1006, + 'samsung1.jpeg': 2001, 'samsung2.jpeg': 2002, 'samsung3.jpeg': 2003, + 'samsung4.jpeg': 2004, 'samsung5.jpeg': 2005, 'samsung6.jpeg': 2006, + 'headphone1.jpeg': 3001, 'headphone2.jpeg': 3002, 'headphone3.jpeg': 3003, + 'headphone4.jpeg': 3004, 'headphone5.jpeg': 3005, 'headphone6.jpeg': 3006, + 'headphone7.jpeg': 3007, 'headphone8.jpeg': 3008, 'headphone9.jpeg': 3009, + 'headphone10.jpeg': 3010, 'headphone11.jpeg': 3011, 'headphone12.jpeg': 3012 + }; + productId = imageToIdMap[imageName]; + } + } + + return productId; + } + + updateRowTotal(row, quantity) { + const priceElement = row.querySelector('.product__price .new__price'); + const totalElement = row.querySelector('.product__subtotal .new__price'); + + if (priceElement && totalElement) { + const price = parseFloat(priceElement.textContent.replace('$', '')); + const total = price * quantity; + totalElement.textContent = `$${total.toFixed(2)}`; + } + } + + updateCartTotals(newTotal) { + // Update cart totals section + const subtotalElements = document.querySelectorAll('.cart__totals .new__price'); + if (subtotalElements.length >= 2) { + subtotalElements[0].textContent = `$${newTotal.toFixed(2)}`; // Subtotal + subtotalElements[1].textContent = `$${newTotal.toFixed(2)}`; // Total (assuming no shipping) + } + } + + async updateCartCount(count = null) { + try { + if (count === null) { + // Fetch current cart count + const response = await fetch('/api/cart/get', { + credentials: 'same-origin' + }); + if (response.status === 401) { + // If not authenticated, treat count as zero (do not redirect here) + count = 0; + } else { + const result = await response.json(); + count = result.count || 0; + } + } + + if (this.cartCountElement) { + this.cartCountElement.textContent = count; + + // Animate cart count + if (count > 0) { + this.cartCountElement.style.transform = 'scale(1.2)'; + setTimeout(() => { + this.cartCountElement.style.transform = 'scale(1)'; + }, 200); + } + } + + } catch (error) { + console.error('Error updating cart count:', error); + } + } + + async loadCartPage() { + try { + const response = await fetch('/api/cart/get', { + credentials: 'same-origin' + }); + + if (response.status === 401) { + // If user is not logged in, redirect to login page + this.redirectToLogin(); + return; + } + const result = await response.json(); + + const tableBody = document.querySelector('.cart__table tbody'); + if (!tableBody) return; + + if (result.cart_items.length === 0) { + this.showEmptyCart(); + return; + } + + // Clear existing static content + tableBody.innerHTML = ''; + + // Populate with real cart items + result.cart_items.forEach(item => { + const row = this.createCartRow(item); + tableBody.appendChild(row); + }); + + // Update totals + this.updateCartTotals(result.total); + this.updateCartCount(result.count); + + } catch (error) { + console.error('Error loading cart page:', error); + this.showNotification('Failed to load cart items', 'error'); + } + } + + createCartRow(item) { + const row = document.createElement('tr'); + row.dataset.productId = item.product_id; + + const subtotal = (item.product_price * item.quantity).toFixed(2); + + row.innerHTML = ` + + + ${item.product_title} + + + + ${item.product_title} + + +
+ $${item.product_price.toFixed(2)} +
+ + +
+
+ + + + + + + + + + + +
+
+ + +
+ $${subtotal} +
+ + + + + + + `; + + return row; + } + + showEmptyCart() { + const tableBody = document.querySelector('.cart__table tbody'); + if (tableBody) { + tableBody.innerHTML = ` + + +

Your cart is empty

+

Add some products to get started!

+ + Continue Shopping + + + + `; + } + + // Reset totals + this.updateCartTotals(0); + this.updateCartCount(0); + } + + showNotification(message, type = 'info') { + // Create or update notification element + let notification = document.getElementById('cart-notification'); + + if (!notification) { + notification = document.createElement('div'); + notification.id = 'cart-notification'; + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; + border-radius: 5px; + color: white; + font-weight: bold; + z-index: 10000; + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease; + max-width: 300px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + `; + document.body.appendChild(notification); + } + + // Set notification style based on type + const colors = { + success: '#28a745', + error: '#dc3545', + info: '#17a2b8', + warning: '#ffc107' + }; + + notification.style.background = colors[type] || colors.info; + notification.textContent = message; + + // Show notification + setTimeout(() => { + notification.style.opacity = '1'; + notification.style.transform = 'translateX(0)'; + }, 100); + + // Hide notification + setTimeout(() => { + notification.style.opacity = '0'; + notification.style.transform = 'translateX(100%)'; + }, 3000); + } + + redirectToLogin() { + // store current path to return after login + try { + const returnTo = window.location.pathname + window.location.search; + sessionStorage.setItem('post_login_redirect', returnTo); + } catch (e) {} + window.location.href = '/login/'; + } +} + +// Initialize cart manager when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + window.cartManager = new CartManager(); +}); + +// Export for potential external use +if (typeof module !== 'undefined' && module.exports) { + module.exports = CartManager; +} \ No newline at end of file diff --git a/static/js/darkmode.js b/static/js/darkmode.js new file mode 100644 index 0000000..3c14366 --- /dev/null +++ b/static/js/darkmode.js @@ -0,0 +1,108 @@ +// Enhanced Dark mode toggle functionality +document.addEventListener('DOMContentLoaded', function() { + const themeSwitch = document.getElementById('theme-switch'); + const body = document.body; + + console.log('Dark mode script loaded'); // Debug log + + // Check for saved theme preference or default to light mode + const currentTheme = localStorage.getItem('theme'); + console.log('Current stored theme:', currentTheme); // Debug log + + if (currentTheme === 'darkMode') { + body.classList.add('darkMode'); + updateThemeIcon(true); + console.log('Dark mode applied from storage'); // Debug log + } else { + body.classList.remove('darkMode'); + updateThemeIcon(false); + console.log('Light mode applied from storage'); // Debug log + } + + // Theme switch event listener + if (themeSwitch) { + themeSwitch.addEventListener('click', function() { + body.classList.toggle('darkMode'); + + let theme = 'lightMode'; + if (body.classList.contains('darkMode')) { + theme = 'darkMode'; + updateThemeIcon(true); + console.log('Switched to dark mode'); // Debug log + } else { + updateThemeIcon(false); + console.log('Switched to light mode'); // Debug log + } + + localStorage.setItem('theme', theme); + + // Notify listeners (e.g., components that may need to recalc sizes) + try { + document.dispatchEvent(new CustomEvent('theme:changed', { detail: { theme } })); + } catch (e) { + console.warn('Theme change event dispatch failed', e); + } + + // Resume hero slider autoplay if Glide instance exists + if (window.heroGlide && typeof window.heroGlide.play === 'function') { + setTimeout(() => { + try { + console.log('[HeroGlide] Resume attempt after theme toggle'); + window.heroGlide.play(); + } catch (e) { console.warn('Could not resume hero slider', e); } + }, 30); + } + }); + } else { + console.error('Theme switch button not found!'); // Debug log + } + + // Update the theme icon based on current mode + function updateThemeIcon(isDarkMode) { + const themeIcon = document.querySelector('.theme-icon'); + if (themeIcon) { + if (isDarkMode) { + themeIcon.innerHTML = 'šŸŒ™'; // Moon for dark mode + themeIcon.setAttribute('title', 'Switch to light mode'); + } else { + themeIcon.innerHTML = 'ā˜€ļø'; // Sun for light mode + themeIcon.setAttribute('title', 'Switch to dark mode'); + } + } else { + console.error('Theme icon not found!'); // Debug log + } + } + + // Initialize icon on page load + const themeIcon = document.querySelector('.theme-icon'); + if (themeIcon) { + updateThemeIcon(body.classList.contains('darkMode')); + } + + // Add accessibility attributes to theme switch + if (themeSwitch) { + themeSwitch.setAttribute('aria-label', 'Toggle dark mode'); + themeSwitch.setAttribute('role', 'button'); + } + + // Add CSS to ensure dark mode styles are applied immediately + const style = document.createElement('style'); + style.textContent = ` + body.darkMode { + --white: #1a1a1a !important; + --black: #ffffff !important; + --primaryColor: #2d2d2d !important; + } + body.darkMode .header, + body.darkMode .navigation { + background-color: #1a1a1a !important; + } + body.darkMode .nav__logo a, + body.darkMode .nav__link { + color: #ffffff !important; + } + `; + document.head.appendChild(style); + + console.log('Dark mode setup complete'); // Debug log +}); \ No newline at end of file diff --git a/static/js/global.js b/static/js/global.js new file mode 100644 index 0000000..02da090 --- /dev/null +++ b/static/js/global.js @@ -0,0 +1,167 @@ +// Check authentication status with server +async function checkAuthStatus() { + try { + const response = await fetch('/api/check-auth', { credentials: 'same-origin' }); + const data = await response.json(); + return data; + } catch (error) { + console.error('Error checking auth status:', error); + return { authenticated: false }; + } +} + +// Update navigation icons based on login status +function updateNavIcons(isLoggedIn, userName = '', role = 'user') { + const navIcons = document.querySelector('.nav__icons'); + if (!navIcons) return; + + // Remove previous logout button if any + const oldLogout = document.getElementById('logout-btn'); + if (oldLogout) oldLogout.remove(); + + // Handle username label + let userNameEl = document.getElementById('user-name'); + if (!userNameEl) { + userNameEl = document.createElement('span'); + userNameEl.id = 'user-name'; + userNameEl.className = 'user-name'; + // Insert before the first icon so it sits to the left + navIcons.insertBefore(userNameEl, navIcons.firstChild); + } + + // Find login link + const loginLink = document.getElementById('login-btn') + || navIcons.querySelector('.icon__item[href="/login/"]') + || navIcons.querySelector('a.icon__item[href$="/login/"]'); + + // Always set login link with next param to return to the same page after login + if (loginLink) { + const next = encodeURIComponent(window.location.pathname + window.location.search); + loginLink.href = `/login/?next=${next}`; + } + + if (isLoggedIn) { + // Hide login button + if (loginLink) loginLink.style.display = 'none'; + + // Show username + userNameEl.textContent = userName ? `Hi, ${userName}` : 'Hi'; + userNameEl.style.display = 'inline-flex'; + + // Add logout button + const logoutBtn = document.createElement('a'); + logoutBtn.href = "#"; + logoutBtn.className = "logout-btn"; + logoutBtn.id = "logout-btn"; + logoutBtn.innerHTML = ` + + Logout + `; + navIcons.appendChild(logoutBtn); + + logoutBtn.addEventListener('click', async function(e) { + e.preventDefault(); + try { + await fetch('/api/logout', { method: 'POST' }); + window.location.href = '/'; + } catch (error) { + console.error('Logout error:', error); + window.location.href = '/'; + } + }); + + // Add Admin link if role is admin + if (role === 'admin' && !document.getElementById('admin-link')) { + const adminLink = document.createElement('a'); + adminLink.id = 'admin-link'; + adminLink.href = '/admin/'; + adminLink.className = 'icon__item icon__item--admin'; + adminLink.title = 'Admin'; + adminLink.setAttribute('aria-label', 'Admin'); + adminLink.innerHTML = ` + Admin + `; + // Place admin link before logout for convenience + navIcons.insertBefore(adminLink, logoutBtn); + } + } else { + // Show login button + if (loginLink) loginLink.style.display = ''; + // Hide username + if (userNameEl) userNameEl.style.display = 'none'; + // Remove admin link if present + const adminLink = document.getElementById('admin-link'); + if (adminLink) adminLink.remove(); + } +} + +// Update UI based on authentication status +async function updateAuthUI() { + const authStatus = await checkAuthStatus(); + const loginRegisterLinks = document.getElementById('loginRegisterLinks'); + const loggedInLinks = document.getElementById('loggedInLinks'); + const welcomeMessage = document.getElementById('welcomeMessage'); + + if (authStatus.authenticated) { + // User is logged in + if (loginRegisterLinks) loginRegisterLinks.style.display = 'none'; + if (loggedInLinks) loggedInLinks.style.display = 'flex'; + if (welcomeMessage) welcomeMessage.textContent = `Welcome, ${authStatus.name}`; + + // Update nav icons + updateNavIcons(true, authStatus.name, authStatus.role || 'user'); + } else { + // User is logged out + if (loginRegisterLinks) loginRegisterLinks.style.display = 'flex'; + if (loggedInLinks) loggedInLinks.style.display = 'none'; + if (welcomeMessage) welcomeMessage.textContent = ''; + + // Update nav icons + updateNavIcons(false, '', 'user'); + } +} + +// Protect authenticated routes +async function protectAuthenticatedRoutes() { + const authStatus = await checkAuthStatus(); + const protectedRoutes = ['/cart/', '/product/', '/orders/', '/user-dashboard/']; // Add your protected routes here + + const currentPath = window.location.pathname; + + if (protectedRoutes.includes(currentPath) && !authStatus.authenticated) { + window.location.href = '/login/'; + return false; + } + + return true; +} + +document.addEventListener('DOMContentLoaded', async () => { + // Update auth UI on page load + await updateAuthUI(); + + // Protect routes if needed + await protectAuthenticatedRoutes(); + + // Handle logout button if it exists in the template + const logoutButton = document.getElementById('logoutButton'); + if (logoutButton) { + logoutButton.addEventListener('click', async (e) => { + e.preventDefault(); + try { + await fetch('/api/logout', { method: 'POST' }); + window.location.href = '/'; + } catch (error) { + console.error('Logout error:', error); + window.location.href = '/'; + } + }); + } +}); + +// Export functions for use in other modules (if needed) +window.authUtils = { + checkAuthStatus, + updateAuthUI, + protectAuthenticatedRoutes +}; \ No newline at end of file diff --git a/static/js/index.js b/static/js/index.js new file mode 100644 index 0000000..a4f82ae --- /dev/null +++ b/static/js/index.js @@ -0,0 +1,715 @@ +/* +============= +Navigation +============= + */ +const navOpen = document.querySelector(".nav__hamburger"); +const navClose = document.querySelector(".close__toggle"); +const menu = document.querySelector(".nav__menu"); +const scrollLink = document.querySelectorAll(".scroll-link"); +const navContainer = document.querySelector(".nav__menu"); + +if (navOpen && menu && navContainer) { + navOpen.addEventListener("click", () => { + menu.classList.add("open"); + document.body.classList.add("active"); + navContainer.style.left = "0"; + navContainer.style.width = "30rem"; + }); +} + +if (navClose && menu && navContainer) { + navClose.addEventListener("click", () => { + menu.classList.remove("open"); + document.body.classList.remove("active"); + navContainer.style.left = "-30rem"; + navContainer.style.width = "0"; + }); +} + +/* +============= +PopUp +============= + */ +const popup = document.querySelector(".popup"); +const closePopup = document.querySelector(".popup__close"); +const popupForm = document.querySelector(".popup__form"); +const subscribeBtn = document.querySelector(".popup__right a"); + +if (popup && closePopup) { + // Close popup function + const hidePopup = () => { + popup.classList.add("hide__popup"); + localStorage.setItem('popupDismissed', 'true'); + localStorage.setItem('popupDismissedTime', Date.now().toString()); + }; + + // Close popup when clicking the X button + closePopup.addEventListener("click", (e) => { + e.preventDefault(); + hidePopup(); + }); + + // Close popup when clicking outside the content + popup.addEventListener("click", (e) => { + if (e.target === popup) { + hidePopup(); + } + }); + + // Close popup with Escape key + document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && !popup.classList.contains("hide__popup")) { + hidePopup(); + } + }); + + // Handle subscription form + if (subscribeBtn && popupForm) { + subscribeBtn.addEventListener("click", (e) => { + e.preventDefault(); + const email = popupForm.value.trim(); + + if (email && isValidEmail(email)) { + // Show success message + showPopupMessage("Thank you for subscribing! You'll receive 30% off your next purchase.", "success"); + setTimeout(() => { + hidePopup(); + }, 2000); + } else { + showPopupMessage("Please enter a valid email address.", "error"); + } + }); + } + + // Show popup with conditions + window.addEventListener("load", () => { + const lastDismissed = localStorage.getItem('popupDismissedTime'); + const twentyFourHours = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + + // Don't show if dismissed within last 24 hours + if (lastDismissed && (Date.now() - parseInt(lastDismissed)) < twentyFourHours) { + return; + } + + // Show popup after 3 seconds + setTimeout(() => { + popup.classList.remove("hide__popup"); + // Focus trap for accessibility + popup.setAttribute('aria-hidden', 'false'); + closePopup.focus(); + }, 3000); + }); + + // Helper functions + function isValidEmail(email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + function showPopupMessage(message, type) { + const existingMessage = popup.querySelector('.popup-message'); + if (existingMessage) { + existingMessage.remove(); + } + + const messageDiv = document.createElement('div'); + messageDiv.className = `popup-message popup-message--${type}`; + messageDiv.textContent = message; + messageDiv.style.cssText = ` + position: absolute; + top: 20px; + left: 50%; + transform: translateX(-50%); + padding: 10px 20px; + border-radius: 5px; + color: white; + font-weight: bold; + z-index: 10001; + background: ${type === 'success' ? '#28a745' : '#dc3545'}; + box-shadow: 0 2px 10px rgba(0,0,0,0.2); + animation: slideInDown 0.3s ease; + `; + + popup.appendChild(messageDiv); + + // Remove message after 3 seconds + setTimeout(() => { + if (messageDiv.parentNode) { + messageDiv.remove(); + } + }, 3000); + } +} + +/* +============= +Fixed Navigation +============= + */ + +const navBar = document.querySelector(".navigation"); +const gotoTop = document.querySelector(".goto-top"); + +// Smooth Scroll +Array.from(scrollLink).map(link => { + link.addEventListener("click", e => { + // Prevent Default + e.preventDefault(); + + const id = e.currentTarget.getAttribute("href").slice(1); + const element = document.getElementById(id); + const navHeight = navBar.getBoundingClientRect().height; + const fixNav = navBar.classList.contains("fix__nav"); + let position = element.offsetTop - navHeight; + + if (!fixNav) { + position = position - navHeight; + } + + window.scrollTo({ + left: 0, + top: position, + }); + navContainer.style.left = "-30rem"; + document.body.classList.remove("active"); + }); +}); + +// Fix NavBar + +window.addEventListener("scroll", e => { + const scrollHeight = window.pageYOffset; + const navHeight = navBar.getBoundingClientRect().height; + if (scrollHeight > navHeight) { + navBar.classList.add("fix__nav"); + } else { + navBar.classList.remove("fix__nav"); + } + + if (gotoTop) { + if (scrollHeight > 300) { + gotoTop.classList.add("show-top"); + } else { + gotoTop.classList.remove("show-top"); + } + } +}); + +// Safe element queries +let login = document.querySelector('.login-form'); +let shoppingCart = document.querySelector('.shopping-cart'); + +const loginBtn = document.querySelector('#login-btn'); +const cartBtn = document.querySelector('#cart-btn'); + +if (loginBtn) { + loginBtn.onclick = () => { + if (login) login.classList.toggle('active'); + if (shoppingCart) shoppingCart.classList.remove('active'); + }; +} + +if (cartBtn) { + // Remove toggle behavior: clicking the cart button should navigate to /cart/ (its anchor href) + // If you want a small inline cart in the future, re-enable toggle here. +} + +// Updated Chatbot logic +document.addEventListener('DOMContentLoaded', function() { + const toggleBtn = document.getElementById('chatbot-toggle'); + const closeBtn = document.getElementById('chatbot-close'); + const windowEl = document.getElementById('chatbot-window'); + const form = document.getElementById('chatbot-form'); + const input = document.getElementById('chatbot-input'); + const messages = document.getElementById('chatbot-messages'); + + function appendMessage(text, sender) { + const msg = document.createElement('div'); + msg.className = 'chatbot-message ' + sender; + msg.textContent = text; + messages.appendChild(msg); + messages.scrollTop = messages.scrollHeight; + } + + function appendLoading() { + const loading = document.createElement('div'); + loading.className = 'chatbot-message bot'; + loading.id = 'chatbot-loading'; + loading.innerHTML = '...'; + messages.appendChild(loading); + messages.scrollTop = messages.scrollHeight; + } + + function removeLoading() { + const loading = document.getElementById('chatbot-loading'); + if (loading) loading.remove(); + } + + // Function to send message to backend API + async function sendMessageToAPI(text) { + try { + const response = await fetch('/api/chatbot', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ message: text }), + }); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const data = await response.json(); + return data.response; + } catch (error) { + console.error('Error sending message to API:', error); + return "Sorry, I couldn't connect to the server. Please try again later."; + } + } + + if (toggleBtn && closeBtn && windowEl && form && input && messages) { + // Toggle open/close using floating action button + toggleBtn.addEventListener('click', function() { + const wasHidden = windowEl.classList.contains('chatbot-hide'); + windowEl.classList.toggle('chatbot-hide'); + // restore from minimized if necessary + windowEl.classList.remove('chatbot-minimized'); + if (!windowEl.classList.contains('chatbot-hide')) { + setTimeout(() => input.focus(), 200); + } + // update aria-expanded + toggleBtn.setAttribute('aria-expanded', String(!wasHidden)); + }); + + closeBtn.addEventListener('click', function() { + windowEl.classList.add('chatbot-hide'); + try { windowEl.setAttribute('aria-hidden', 'true'); } catch (e) {} + // ensure toggle stays visible + const tb = document.querySelector('.chatbot-toggle-btn'); + if (tb) tb.style.right = '20px'; + toggleBtn.focus(); + toggleBtn.setAttribute('aria-expanded', 'false'); + }); + + // Minimize button (in new markup) + const minimizeBtn = document.getElementById('chatbot-minimize'); + if (minimizeBtn) { + minimizeBtn.addEventListener('click', function() { + // toggle minimized look but keep widget visible + windowEl.classList.toggle('chatbot-minimized'); + // move focus to toggle so keyboard users can restore + toggleBtn.focus(); + // ensure toggle remains visible (reset position) + const tb = document.querySelector('.chatbot-toggle-btn'); + if (tb) tb.style.right = '20px'; + }); + } + + // Attach button/file input handling + const attachBtn = document.getElementById('chatbot-attach-btn'); + const attachInput = document.getElementById('chatbot-attach'); + const attachList = document.getElementById('chatbot-attachments'); + let pendingAttachments = []; + + if (attachBtn && attachInput && attachList) { + attachBtn.addEventListener('click', () => attachInput.click()); + + attachInput.addEventListener('change', (e) => { + const files = Array.from(e.target.files || []); + files.forEach(file => { + // create a small preview chip + const id = Date.now() + Math.random().toString(36).slice(2,7); + pendingAttachments.push({ id, file }); + + const chip = document.createElement('span'); + chip.className = 'attachment'; + chip.dataset.attId = id; + chip.innerHTML = `šŸ“Ž ${file.name} `; + attachList.appendChild(chip); + + const removeBtn = chip.querySelector('button'); + removeBtn.addEventListener('click', () => { + pendingAttachments = pendingAttachments.filter(a => a.id !== id); + chip.remove(); + // clear file input if no pending attachments + if (pendingAttachments.length === 0) attachInput.value = ''; + }); + }); + }); + } + + form.addEventListener('submit', async function(e) { + e.preventDefault(); + const text = input.value.trim(); + if (!text && pendingAttachments.length === 0) return; + + // Show user message (text and attachments summary) + let userText = text || ''; + if (pendingAttachments.length) { + const names = pendingAttachments.map(a => a.file.name).join(', '); + userText = userText ? `${userText} \n(Attachments: ${names})` : `(Attachments: ${names})`; + } + + appendMessage(userText, 'user'); + input.value = ''; + // clear attachment UI + attachList.innerHTML = ''; + attachInput.value = ''; + const attachmentsToSend = pendingAttachments.slice(); + pendingAttachments = []; + + appendLoading(); + + try { + // Basic payload: message + filenames. If you want to actually upload files, + // switch to multipart/form-data and send files to an upload endpoint. + const payload = { message: text, attachments: attachmentsToSend.map(a => a.file.name) }; + const response = await fetch('/api/chatbot', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + + removeLoading(); + if (response.ok) { + const data = await response.json(); + appendMessage(data.response || 'No reply', 'bot'); + } else { + appendMessage("Sorry, I'm having trouble connecting right now.", 'bot'); + } + } catch (error) { + removeLoading(); + appendMessage("Sorry, I'm having trouble connecting right now.", 'bot'); + } + }); + + // Optional: greet on open + let greeted = false; + toggleBtn.addEventListener('click', function() { + if (!greeted && windowEl.classList.contains('chatbot-hide') === false) { + setTimeout(() => { + appendMessage("Hi! I'm your assistant. Ask me anything about our phones!", 'bot'); + }, 400); + greeted = true; + } + }); + } +}); + +// Modern Chatbot UI JavaScript functionality +document.addEventListener('DOMContentLoaded', function() { + const chatBody = document.querySelector('.chat-body'); + const sendButton = document.querySelector('.send-button'); + const inputField = document.querySelector('.input-area input'); + const suggestionButtons = document.querySelectorAll('.suggestion-button'); + const headerIcons = document.querySelectorAll('.header-icons i'); + const attachIcon = document.querySelector('.input-area .fa-paperclip'); + const micIcon = document.querySelector('.input-area .fa-microphone'); + + // Function to add message to chat + function addMessage(text, type) { + const messageDiv = document.createElement('div'); + messageDiv.className = `message ${type}`; + messageDiv.textContent = text; + chatBody.appendChild(messageDiv); + chatBody.scrollTop = chatBody.scrollHeight; + } + + // Function to send message to server + async function sendMessageToServer(text) { + try { + const response = await fetch('/api/chatbot', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ message: text }), + }); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const data = await response.json(); + return data.response; + } catch (error) { + console.error('Error sending message to API:', error); + return "Sorry, I couldn't connect to the server. Please try again later."; + } + } + + // Send button functionality + if (sendButton && inputField) { + sendButton.addEventListener('click', async function() { + const text = inputField.value.trim(); + if (!text) return; + + // Add user message + addMessage(text, 'sent'); + inputField.value = ''; + + // Add loading indicator with wave animation + const loadingDiv = document.createElement('div'); + loadingDiv.className = 'message received loading-animation'; + loadingDiv.id = 'loading-message'; + loadingDiv.innerHTML = ` + + T + y + p + i + n + g + + . + . + . + + + `; + chatBody.appendChild(loadingDiv); + chatBody.scrollTop = chatBody.scrollHeight; + + // Send to server and get response + try { + const response = await sendMessageToServer(text); + const loadingElement = document.getElementById('loading-message'); + if (loadingElement) { + loadingElement.remove(); + } + addMessage(response, 'received'); + } catch (error) { + const loadingElement = document.getElementById('loading-message'); + if (loadingElement) { + loadingElement.remove(); + } + addMessage("Sorry, I'm having trouble connecting right now.", 'received'); + } + }); + } + + // Enter key functionality + if (inputField) { + inputField.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + sendButton.click(); + } + }); + } + + // Suggestion buttons functionality + suggestionButtons.forEach(button => { + button.addEventListener('click', function() { + // Remove active class from all buttons + suggestionButtons.forEach(btn => btn.classList.remove('active')); + // Add active class to clicked button + this.classList.add('active'); + + // Set input value to suggestion text + if (inputField) { + inputField.value = this.textContent.trim(); + } + + // Auto-send the suggestion + setTimeout(() => { + sendButton.click(); + }, 100); + }); + }); + + // Header icons functionality + headerIcons.forEach(icon => { + icon.addEventListener('click', function(e) { + // If chevron clicked -> close/hide the chatbot completely + if (this.classList.contains('fa-chevron-down')) { + const chatContainer = document.querySelector('.chatbot-container'); + if (chatContainer) { + // Add hidden class and ensure aria + inline display updated for compatibility + chatContainer.classList.add('chatbot-hide'); + try { chatContainer.setAttribute('aria-hidden', 'true'); } catch (err) {} + try { chatContainer.style.display = 'none'; } catch (err) {} + + // Reset floating toggle position and aria state so it remains visible + const tb = document.querySelector('.chatbot-toggle-btn'); + if (tb) { + tb.setAttribute('aria-expanded', 'false'); + try { tb.style.right = '20px'; } catch (err) {} + try { tb.focus(); } catch (err) {} + } + } + } else if (this.classList.contains('fa-ellipsis-v')) { + // Show options menu (you can customize this) + alert('Chatbot options menu'); + } + }); + }); + + // Attach icon functionality + if (attachIcon) { + attachIcon.addEventListener('click', function() { + // Create a file input + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/*,.pdf,.txt,.doc,.docx'; + fileInput.style.display = 'none'; + + fileInput.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (file) { + addMessage(`šŸ“Ž Attached: ${file.name}`, 'sent'); + addMessage("I received your file! How can I help you with it?", 'received'); + } + }); + + document.body.appendChild(fileInput); + fileInput.click(); + document.body.removeChild(fileInput); + }); + } + + // Microphone icon functionality + if (micIcon) { + micIcon.addEventListener('click', function() { + // Basic voice recording functionality + if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + const recognition = new SpeechRecognition(); + + recognition.lang = 'en-US'; + recognition.continuous = false; + recognition.interimResults = false; + + recognition.onstart = function() { + micIcon.style.color = '#ff4444'; + }; + + recognition.onresult = function(event) { + const transcript = event.results[0][0].transcript; + if (inputField) { + inputField.value = transcript; + } + micIcon.style.color = '#ff4757'; + }; + + recognition.onerror = function() { + micIcon.style.color = '#ff4757'; + addMessage("Sorry, I couldn't understand the audio.", 'received'); + }; + + recognition.onend = function() { + micIcon.style.color = '#ff4757'; + }; + + recognition.start(); + } else { + alert('Speech recognition not supported in this browser.'); + } + }); + } + + // Toggle is created separately by the standalone initializer below. +}); + +// Standalone toggle button - runs independently +(function() { + function createToggleButton() { + // Remove any existing toggle button first + const existingToggle = document.querySelector('.chatbot-toggle-btn'); + if (existingToggle) { + existingToggle.remove(); + } + + // Create the toggle button + const toggleButton = document.createElement('button'); + toggleButton.className = 'chatbot-toggle-btn'; + // Use the bot image as the button content + toggleButton.innerHTML = 'Chat bot'; + toggleButton.style.cssText = ` + position: fixed !important; + bottom: 20px !important; + right: 20px !important; + width: 60px !important; + height: 60px !important; + border-radius: 50% !important; + background: linear-gradient(to right, #ff4757, #ff3838) !important; + color: white !important; + border: none !important; + font-size: 24px !important; + cursor: pointer !important; + z-index: 99999 !important; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2) !important; + transition: transform 0.3s ease !important; + `; + + toggleButton.addEventListener('click', function() { + console.log('[chatbot] toggle clicked'); + const chatContainer = document.querySelector('.chatbot-container'); + if (!chatContainer) { + console.warn('[chatbot] .chatbot-container not found in DOM'); + return; + } + + // If currently hidden, show it (remove class and clear inline display) + if (chatContainer.classList.contains('chatbot-hide')) { + // OPEN + chatContainer.classList.remove('chatbot-hide'); + try { chatContainer.style.display = ''; } catch (e) {} + chatContainer.setAttribute('aria-hidden', 'false'); + this.setAttribute('aria-expanded', 'true'); + // Shift toggle left so it remains visible outside the opened chat + try { + const rect = chatContainer.getBoundingClientRect(); + const chatWidth = rect.width || 380; + const margin = 12; // gap between chat and toggle + this.style.right = (20 + chatWidth + margin) + 'px'; + } catch (e) { + this.style.right = '420px'; + } + console.log('[chatbot] opened'); + } else { + // CLOSE + chatContainer.classList.add('chatbot-hide'); + try { chatContainer.style.display = 'none'; } catch (e) {} + chatContainer.setAttribute('aria-hidden', 'true'); + this.setAttribute('aria-expanded', 'false'); + // reset toggle position + this.style.right = '20px'; + console.log('[chatbot] closed'); + } + }); + + // Accessibility: make toggle focusable and operable via keyboard + toggleButton.setAttribute('aria-label', 'Open chat'); + toggleButton.setAttribute('role', 'button'); + toggleButton.setAttribute('tabindex', '0'); + toggleButton.setAttribute('aria-expanded', 'false'); + + toggleButton.addEventListener('keydown', function(e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.click(); + } + }); + + // Add hover effect + toggleButton.addEventListener('mouseenter', function() { + this.style.transform = 'scale(1.1)'; + }); + + toggleButton.addEventListener('mouseleave', function() { + this.style.transform = 'scale(1)'; + }); + + document.body.appendChild(toggleButton); + } + + // Create toggle button when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', createToggleButton); + } else { + createToggleButton(); + } +})(); \ No newline at end of file diff --git a/static/js/product-page.js b/static/js/product-page.js new file mode 100644 index 0000000..5100037 --- /dev/null +++ b/static/js/product-page.js @@ -0,0 +1,239 @@ +document.addEventListener('DOMContentLoaded', () => { + // thumbnail clicks change main image + const thumbnails = document.querySelectorAll('.product__pictures .picture'); + const mainImg = document.getElementById('pic'); + const zoom = document.getElementById('zoom'); + + thumbnails.forEach((t) => { + t.addEventListener('click', (e) => { + const src = e.target.src; + if (mainImg) mainImg.src = src; + if (zoom) zoom.style.backgroundImage = `url(${src})`; + // open lightbox on click (desktop behavior) + if (window.innerWidth > 720) { + openLightbox(src); + } + }); + }); + + // Dynamic subtotal calculation + const qtyInput = document.getElementById('product-quantity'); + const subtotalEl = document.getElementById('subtotal-price'); + const plusBtn = document.querySelector('.plus-btn'); + const minusBtn = document.querySelector('.minus-btn'); + + // Get product price from the page + const priceEl = document.querySelector('.new__price'); + let basePrice = 0; + if (priceEl) { + const priceText = priceEl.textContent.replace('$', '').replace(',', ''); + basePrice = parseFloat(priceText) || 0; + } + + function updateSubtotal() { + if (qtyInput && subtotalEl && basePrice > 0) { + const quantity = parseInt(qtyInput.value) || 1; + const subtotal = (basePrice * quantity).toFixed(2); + subtotalEl.textContent = `$${subtotal}`; + } + } + + // Quantity controls + if (plusBtn && qtyInput) { + plusBtn.addEventListener('click', () => { + const current = parseInt(qtyInput.value) || 1; + const max = parseInt(qtyInput.getAttribute('max')) || 999; + if (current < max) { + qtyInput.value = current + 1; + updateSubtotal(); + } + }); + } + + if (minusBtn && qtyInput) { + minusBtn.addEventListener('click', () => { + const current = parseInt(qtyInput.value) || 1; + const min = parseInt(qtyInput.getAttribute('min')) || 1; + if (current > min) { + qtyInput.value = current - 1; + updateSubtotal(); + } + }); + } + + if (qtyInput) { + qtyInput.addEventListener('input', updateSubtotal); + qtyInput.addEventListener('change', () => { + const min = parseInt(qtyInput.getAttribute('min')) || 1; + const max = parseInt(qtyInput.getAttribute('max')) || 999; + let value = parseInt(qtyInput.value) || min; + + if (value < min) value = min; + if (value > max) value = max; + + qtyInput.value = value; + updateSubtotal(); + }); + } + + // Initialize subtotal on page load + updateSubtotal(); + + // Wire Add To Cart and Buy Now + const addBtn = document.querySelector('.product__btn'); + const buyBtn = document.getElementById('buy-now'); + + async function addToCart(productId, qty) { + try { + const res = await fetch('/api/cart/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ product_id: productId, quantity: qty }) + }); + + if (res.status === 401) { + // redirect to login + sessionStorage.setItem('post_login_redirect', window.location.pathname + window.location.search); + window.location.href = '/login/'; + return; + } + + const data = await res.json(); + if (data.success) { + // Show success message with animation + showSuccessMessage(data.message || 'Added to cart'); + // update cart count badge if possible + if (window.cartManager && typeof window.cartManager.updateCartCount === 'function') { + window.cartManager.updateCartCount(data.cart_count); + } + } else { + alert(data.message || 'Failed to add to cart'); + } + } catch (err) { + console.error('Add to cart failed', err); + alert('Network error'); + } + } + + function showSuccessMessage(message) { + // Create or reuse success notification + let notification = document.getElementById('cart-success-notification'); + if (!notification) { + notification = document.createElement('div'); + notification.id = 'cart-success-notification'; + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: #4CAF50; + color: white; + padding: 15px 20px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + z-index: 10000; + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease; + font-family: 'Archivo', sans-serif; + font-weight: 500; + `; + document.body.appendChild(notification); + } + + notification.textContent = message; + notification.style.opacity = '1'; + notification.style.transform = 'translateX(0)'; + + setTimeout(() => { + notification.style.opacity = '0'; + notification.style.transform = 'translateX(100%)'; + }, 3000); + } + + if (addBtn) { + addBtn.addEventListener('click', (e) => { + e.preventDefault(); + const pid = parseInt(addBtn.dataset.productId); + const qty = qtyInput ? parseInt(qtyInput.value) || 1 : 1; + if (pid) addToCart(pid, qty); + }); + } + + if (buyBtn) { + buyBtn.addEventListener('click', async (e) => { + e.preventDefault(); + const pid = parseInt(buyBtn.dataset.productId); + const qty = qtyInput ? parseInt(qtyInput.value) || 1 : 1; + if (!pid) return; + // Add to cart then go to checkout/cart + await addToCart(pid, qty); + window.location.href = '/cart/'; + }); + } + + // Enhanced product tabs functionality + const tabBtns = document.querySelectorAll('.detail-btn'); + const tabContents = document.querySelectorAll('.content'); + + tabBtns.forEach(btn => { + btn.addEventListener('click', () => { + const targetId = btn.dataset.id; + + // Remove active class from all tabs and contents + tabBtns.forEach(b => b.classList.remove('active')); + tabContents.forEach(c => c.classList.remove('active')); + + // Add active class to clicked tab and corresponding content + btn.classList.add('active'); + const targetContent = document.getElementById(targetId); + if (targetContent) { + targetContent.classList.add('active'); + } + }); + }); + + // Lightbox utility + function ensureLightbox() { + let modal = document.getElementById('lightboxModal'); + if (!modal) { + modal = document.createElement('div'); + modal.id = 'lightboxModal'; + modal.className = 'lightbox-modal'; + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 99999; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + `; + modal.innerHTML = ` + + `; + document.body.appendChild(modal); + modal.addEventListener('click', () => { + modal.style.opacity = '0'; + modal.style.visibility = 'hidden'; + }); + } + return modal; + } + + function openLightbox(src) { + const modal = ensureLightbox(); + const img = modal.querySelector('#lightboxImg'); + if (img) img.src = src; + modal.style.opacity = '1'; + modal.style.visibility = 'visible'; + } +}); \ No newline at end of file diff --git a/static/js/products.js b/static/js/products.js new file mode 100644 index 0000000..16b4ff0 --- /dev/null +++ b/static/js/products.js @@ -0,0 +1,316 @@ +const getProducts = async () => { + try { + const res = await fetch('/api/products', { credentials: 'same-origin' }); + // API returns an array of product docs + const products = await res.json(); + return Array.isArray(products) ? products : []; + } catch (err) { + console.log('Failed to load products', err); + return []; + } +}; + +/* +============= +Load Category Products +============= + */ +const categoryCenter = document.querySelector(".category__center"); + +// Convert relative image paths from JSON into paths served by Flask static +function normalizeImage(path) { + if (!path) return ''; + // JSON uses ./images/...; our Flask static is /static/images/... + if (path.startsWith('./images/')) { + return '/static' + path.slice(1); + } + if (path.startsWith('/static/')) return path; + return path; +} + +window.addEventListener("DOMContentLoaded", async function () { + const products = await getProducts(); + displayProductItems(products); +}); + +// Ensure any statically-rendered product cards in templates that have a +// "data-product-id" attribute become clickable links to the product page. +// This fixes the homepage "Latest Products" carousel which is written in +// HTML and not rendered by the API. +function wireStaticProductLinks() { + document.querySelectorAll('.product[data-product-id]').forEach(prod => { + const id = prod.dataset.productId; + if (!id) return; + + // Wrap the header image with an anchor if it's not already wrapped + const header = prod.querySelector('.product__header'); + if (header) { + const img = header.querySelector('img'); + if (img && !img.closest('a')) { + const a = document.createElement('a'); + a.href = `/product/${id}`; + // preserve alt/title if present + img.parentNode.replaceChild(a, img); + a.appendChild(img); + } + } + + // Wrap the title text inside h3 with an anchor if missing + const h3 = prod.querySelector('.product__footer h3'); + if (h3) { + const existing = h3.querySelector('a'); + if (!existing) { + const a2 = document.createElement('a'); + a2.href = `/product/${id}`; + // move inner HTML (keeps any child nodes) + a2.innerHTML = h3.innerHTML; + h3.innerHTML = ''; + h3.appendChild(a2); + } + } + }); +} + +window.addEventListener('DOMContentLoaded', wireStaticProductLinks); + +const displayProductItems = items => { + let displayProduct = items.map( + product => ` +
+
+ + ${product.title} + +
+ + +
+ ` + ); + + displayProduct = displayProduct.join(""); + if (categoryCenter) { + categoryCenter.innerHTML = displayProduct; + } +}; + +/* +============= +Filtering +============= + */ + +const filterBtn = document.querySelectorAll(".filter-btn"); +const categoryContainer = document.getElementById("category"); + +if (categoryContainer) { + categoryContainer.addEventListener("click", async e => { + const target = e.target.closest(".section__title"); + if (!target) return; + + const id = target.dataset.id; + const products = await getProducts(); + + if (id) { + // remove active from buttons + Array.from(filterBtn).forEach(btn => { + btn.classList.remove("active"); + }); + target.classList.add("active"); + + // Load Products + let menuCategory = products.filter(product => { + if (product.category === id) { + return product; + } + }); + + if (id === "All Products") { + displayProductItems(products); + } else { + displayProductItems(menuCategory); + } + } + }); +} + +/* +============= +Product Details Left +============= + */ +const pic1 = document.getElementById("pic1"); +const pic2 = document.getElementById("pic2"); +const pic3 = document.getElementById("pic3"); +const pic4 = document.getElementById("pic4"); +const pic5 = document.getElementById("pic5"); +const picContainer = document.querySelector(".product__pictures"); +const zoom = document.getElementById("zoom"); +const pic = document.getElementById("pic"); + +// Picture List +const picList = [pic1, pic2, pic3, pic4, pic5]; + +// Active Picture +let picActive = 1; + +["mouseover", "touchstart"].forEach(event => { + if (picContainer) { + picContainer.addEventListener(event, e => { + const target = e.target.closest("img"); + if (!target) return; + const id = target.id.slice(3); + changeImage(`./images/products/iPhone/iphone${id}.jpeg`, id); + }); + } +}); + +// change active image +const changeImage = (imgSrc, n) => { + // change the main image + pic.src = imgSrc; + // change the background-image + zoom.style.backgroundImage = `url(${imgSrc})`; + // remove the border from the previous active side image + picList[picActive - 1].classList.remove("img-active"); + // add to the active image + picList[n - 1].classList.add("img-active"); + // update the active side picture + picActive = n; +}; + +/* +============= +Product Details Bottom +============= + */ + +const btns = document.querySelectorAll(".detail-btn"); +const detail = document.querySelector(".product-detail__bottom"); +const contents = document.querySelectorAll(".content"); + +if (detail) { + detail.addEventListener("click", e => { + const target = e.target.closest(".detail-btn"); + if (!target) return; + + const id = target.dataset.id; + if (id) { + Array.from(btns).forEach(btn => { + // remove active from all btn + btn.classList.remove("active"); + e.target.closest(".detail-btn").classList.add("active"); + }); + // hide other active + Array.from(contents).forEach(content => { + content.classList.remove("active"); + }); + const element = document.getElementById(id); + element.classList.add("active"); + } + }); +} + + +//asfaak dev + + +document.addEventListener("DOMContentLoaded", function () { + + function updateCart(productId, quantity) { + fetch(`/update-cart/${productId}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ quantity: quantity }) + }).then(res => res.json()) + .then(data => { + if (data.status !== "success") console.error("Cart update failed"); + }); + } + + document.querySelectorAll(".minus-btn").forEach(minus => { + minus.addEventListener("click", function () { + const container = this.parentElement; + const input = container.querySelector(".counter-btn"); + let value = parseInt(input.value); + const min = parseInt(input.getAttribute("min")) || 1; + if (value > min) { + value -= 1; + input.value = value; + updateCart(input.dataset.productId, value-1); + } + }); + }); + + document.querySelectorAll(".plus-btn").forEach(plus => { + plus.addEventListener("click", function () { + const container = this.parentElement; + const input = container.querySelector(".counter-btn"); + let value = parseInt(input.value); + const max = parseInt(input.getAttribute("max")) || 99; + if (value < max) { + value += 1; + input.value = value; + updateCart(input.dataset.productId, value-1); + } + }); + }); + + // Handle manual typing + document.querySelectorAll(".counter-btn").forEach(input => { + input.addEventListener("change", function () { + let value = parseInt(this.value); + const min = parseInt(this.getAttribute("min")) || 1; + const max = parseInt(this.getAttribute("max")) || 99; + if (value < min) value = min; + if (value > max) value = max; + this.value = value; + updateCart(this.dataset.productId, value-1); + }); + }); +}); diff --git a/static/js/search-filter.js b/static/js/search-filter.js new file mode 100644 index 0000000..9848aa6 --- /dev/null +++ b/static/js/search-filter.js @@ -0,0 +1,113 @@ +const searchBox = document.getElementById("searchBox"); +const searchTrigger = document.getElementById("searchTrigger"); +const filterDropdown = document.getElementById("filterDropdown"); +const iphoneVersion = document.getElementById("iphoneVersion"); +const brandFilters = document.querySelectorAll(".filter-dropdown-custom input[type='checkbox']"); +const priceRange = document.getElementById("priceRange"); +const priceValue = document.getElementById("priceValue"); +const clearFiltersBtn = document.getElementById("clearFilters"); +const dropdownProducts = document.getElementById("dropdownProducts"); +const sortPrice = document.getElementById("sortPrice"); +const loadMoreBtn = document.getElementById("loadMoreBtn"); + +const products = [ + {name: "šŸ“± Apple iPhone 17 Pro", brand: "Apple iPhone 17 Pro", price: 2700}, + {name: "šŸ“± Apple iPhone 11", brand: "Apple iPhone 11", price: 760}, + {name: "šŸ“± Apple iPhone 11", brand: "Apple iPhone 11", price: 850}, + {name: "šŸ“± Apple iPhone 11", brand: "Apple iPhone 11", price: 290}, + {name: "šŸ“± Apple iPhone 11", brand: "Apple iPhone 11", price: 800}, + {name: "šŸ“± Apple iPhone 11", brand: "Apple iPhone 11", price: 300}, + {name: "šŸ“± Apple iPhone 11 Pro", brand: "Apple iPhone 11 Pro", price: 385}, + {name: "šŸ“± Samsung Galaxy", brand: "Samsung Galaxy", price: 400}, + {name: "šŸ“± Samsung Galaxy", brand: "Samsung Galaxy", price: 550}, + {name: "šŸ“± Samsung Galaxy", brand: "Samsung Galaxy", price: 270}, + {name: "šŸ“± Samsung Galaxy", brand: "Samsung Galaxy", price: 500}, + {name: "šŸ“± Samsung Galaxy", brand: "Samsung Galaxy", price: 450}, + {name: "šŸ“± Samsung Galaxy", brand: "Samsung Galaxy", price: 460}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 265}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 250}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 365}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 475}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 850}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 360}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 320}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 305}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 630}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 250}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 700}, + {name: "šŸŽ§ Sony WH-CH510", brand: "Sony WH-CH510", price: 600} +]; + +let productsPerBatch = 6; +let currentIndex = 0; +let filteredProducts = []; + +searchBox.addEventListener("focus", () => filterDropdown.classList.add("show")); +searchTrigger.addEventListener("click", () => filterDropdown.classList.toggle("show")); +document.addEventListener("click", e => { + if(!e.target.closest(".search-wrapper-custom")) filterDropdown.classList.remove("show"); +}); + +function displayProducts(reset = false){ + if(reset){ + currentIndex = 0; + dropdownProducts.innerHTML = ""; + } + + const batch = filteredProducts.slice(currentIndex, currentIndex + productsPerBatch); + batch.forEach((p, i)=>{ + const div = document.createElement("div"); + div.className = "product"; + div.style.animationDelay = `${i*0.05}s`; + div.innerHTML = `${p.name.replace(new RegExp(searchBox.value,"gi"), match=>`${match}`)}$${p.price}`; + dropdownProducts.appendChild(div); + }); + + currentIndex += batch.length; + loadMoreBtn.style.display = (currentIndex < filteredProducts.length) ? "block" : "none"; + + if(filteredProducts.length===0 && reset){ + dropdownProducts.innerHTML = "
šŸ˜ž No products found
"; + loadMoreBtn.style.display = "none"; + } +} + +function updateFilters(){ + let searchText = searchBox.value.toLowerCase(); + let selectedVersion = iphoneVersion.value.toLowerCase(); + let selectedBrands = Array.from(brandFilters).filter(cb=>cb.checked).map(cb=>cb.value.toLowerCase()); + let maxPrice = parseInt(priceRange.value); + + priceValue.textContent = "$"+maxPrice; + + filteredProducts = products.filter(p=>{ + let matchSearch = p.name.toLowerCase().includes(searchText); + let matchVersion = selectedVersion ? p.brand.toLowerCase().includes(selectedVersion) : true; + let matchBrand = selectedBrands.length ? selectedBrands.includes(p.brand.toLowerCase()) : true; + let matchPrice = p.price <= maxPrice; + return matchSearch && matchVersion && matchBrand && matchPrice; + }); + + if(sortPrice.value==="asc") filteredProducts.sort((a,b)=>a.price-b.price); + if(sortPrice.value==="desc") filteredProducts.sort((a,b)=>b.price-a.price); + + displayProducts(true); +} + +loadMoreBtn.addEventListener("click", ()=>displayProducts()); + +searchBox.addEventListener("input", updateFilters); +iphoneVersion.addEventListener("change", updateFilters); +brandFilters.forEach(cb=>cb.addEventListener("change", updateFilters)); +priceRange.addEventListener("input", updateFilters); +sortPrice.addEventListener("change", updateFilters); +clearFiltersBtn.addEventListener("click", ()=>{ + searchBox.value=""; + iphoneVersion.value=""; + brandFilters.forEach(cb=>cb.checked=false); + priceRange.value=3000; + sortPrice.value="none"; + updateFilters(); +}); + +updateFilters(); \ No newline at end of file diff --git a/js/slider.js b/static/js/slider.js similarity index 71% rename from js/slider.js rename to static/js/slider.js index 86b0bb5..52ddf96 100644 --- a/js/slider.js +++ b/static/js/slider.js @@ -16,7 +16,7 @@ Hero ============= */ if (slider1) { - new Glide(slider1, { + window.heroGlide = new Glide(slider1, { type: "carousel", startAt: 0, autoplay: 3000, @@ -24,7 +24,26 @@ if (slider1) { perView: 1, animationDuration: 800, animationTimingFunc: "linear", - }).mount(); + }); + window.heroGlide.mount(); + console.log('[HeroGlide] Mounted'); + + document.addEventListener('theme:changed', () => { + if (!window.heroGlide) return; + try { + const idx = window.heroGlide.index || 0; + console.log('[HeroGlide] Theme changed. Current index:', idx); + // Glide does not expose direct refresh; mimic by forcing go to same index and restarting autoplay + window.heroGlide.go('=' + idx); + // Restart autoplay explicitly + if (typeof window.heroGlide.play === 'function') { + window.heroGlide.play(); + console.log('[HeroGlide] Autoplay resumed after theme change'); + } + } catch (e) { + console.warn('[HeroGlide] Theme change handling failed', e); + } + }); } /* diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..1815907 --- /dev/null +++ b/static/styles.css @@ -0,0 +1,4860 @@ +/* +====================== +Reset +====================== +*/ +:root { + --primaryColor: #f1f1f1; + --black: #222; + --black2: #555; + --black3: #252525; + --black4: #000; + --black5: #212529; + --orange: #eb0028; + --white: #fff; + --grey: #959595; + --grey2: #666; + --grey3: #ccc; + --secondaryColor: #2b1f4d; + --yellow: #ffcc00; + --green: #59b210; + --blue: rgb(56, 10, 223); +} + +* { + margin: 0; + padding: 0; + box-sizing: inherit; +} + +html { + font-size: 62.5%; + box-sizing: border-box; + scroll-behavior: smooth; + overflow-x: hidden; +} + +body{ + overflow-x: hidden; +} + +body, +input { + font-size: 1.6rem; + font-weight: 400; + font-family: "Archivo", sans-serif; + color: var(--black); +} + +a { + text-decoration: none; +} + +ul { + list-style: none; +} + +img { + max-width: 100%; +} + +h3, +h4 { + font-weight: 500; +} + +/* +====================== +Header +====================== +*/ + +.header { + position: relative; + background-color: var(--white); + transition: background-color 0.3s ease; +} + +/* Ensure header has background in all cases */ +body .header { + background-color: #ffffff; +} + +.container { + max-width: 117rem; + margin: 0 auto; + padding: 0 1.6rem; +} + +/* +====================== +Navigation +====================== +*/ + +.navigation { + position: relative; + height: 7rem; + background-color: var(--white); + transition: background-color 0.3s ease; +} + +/* Ensure navigation has background in all cases */ +body .navigation { + background-color: #ffffff; +} + +.nav { + display: flex; + align-items: center; + justify-content: space-between; + height: 100%; + height: 7rem; + padding: 0 1rem; +} + +.fix__nav { + position: fixed; + top: 0; + left: 0; + width: 100%; + background-color: var(--white); + z-index: 1200; +} + +.nav__logo a { + font-size: 2.5rem; + color: var(--black); + padding: 1.6rem; + font-weight: 700; +} + +.nav__hamburger { + display: none; + cursor: pointer; +} + +.nav__hamburger svg { + height: 2.3rem; + width: 2.3rem; +} + +.menu__top { + display: none; +} + +.nav__menu { + width: 50%; +} + +.nav__list { + display: flex; + align-items: center; + height: 100%; + width: 50%; +} + +.nav__item:not(:last-child) { + margin-right: 1.6rem; +} + +.nav__list .nav__link:link, +.nav__list .nav__link:visited { + display: inline-block; + font-size: 1.4rem; + text-transform: uppercase; + color: var(--black); + transition: color 0.3s ease-in-out; +} + +.nav__list .nav__link:hover { + color: var(--orange); +} + +.nav__icons { + display: flex; + position: relative; +} + +.nav__icons .icon__item svg { + width: 1.6rem; + height: 1.6rem; +} + +/* Allow image-based icon items (admin) to align visually */ +.nav__icons .icon__item img { width: 20px; height:20px; display:block; } + +.nav__icons .icon__item { + display: flex; + justify-content: center; + align-items: center; + padding: 0.7rem; + border: 1px solid var(--black); + border-radius: 50%; + transition: background-color 0.3s ease-in-out; +} + +/* Theme switch inherits icon style (works when placed inside nav or reused) */ +#theme-switch.icon__item, +button#theme-switch, +.btn-auth#theme-switch { + border-radius: 50%; + width: 34px; + height: 34px; + padding: 0; + display: inline-flex; + justify-content: center; + align-items: center; + gap: .4rem; + background: transparent; + border: 1px solid var(--black); + color: var(--black); + transition: background-color .3s ease, transform .25s ease, border-color .3s ease; +} + +#theme-switch .theme-icon { font-size: 1.35rem; line-height:1; } + +#theme-switch:focus-visible { outline: 2px solid var(--orange); outline-offset:2px; } +#theme-switch:active { transform: scale(.9); } +#theme-switch:hover { transform: scale(1.05); } + +/* Hover state unified */ +#theme-switch:hover { background: var(--orange); } + +/* Dark mode adjustments */ +body.darkMode #theme-switch { border-color: #fff; color:#fff; } +body.darkMode #theme-switch:hover { background: var(--orange); color:#fff; } + +/* Ensure cart button is positioning context for its badge */ +.nav__icons .icon__item#cart-btn, +.nav__icons .icon__item[href="/cart/"], +.nav__icons .icon__item .icon__cart { + position: relative; +} + +.nav__icons .icon__item:link, +.nav__icons .icon__item:visited { + color: var(--black); +} + +.nav__icons .icon__item:hover { + background-color: var(--orange); + border: 1px solid var(--black); +} + +.nav__icons .icon__item:not(:last-child) { + margin-right: 1rem; +} + +.nav__icons #cart__total { + font-size: 1rem; + position: absolute; + top: -6px; + right: -6px; + background-color: var(--orange); + padding: 0.2rem 0.4rem; + border-radius: 50%; + color: var(--primaryColor); + line-height: 1; +} + +.page__title-area { + background-color: var(--primaryColor); +} + +.page__title-container { + padding: 1rem; +} + +.page__titles { + display: flex; + align-items: center; + font-size: 1.2rem; + color: var(--grey2); +} + +.page__titles a { + margin-right: 2rem; +} + +.page__titles a svg { + width: 1.8rem; + height: 1.8rem; + fill: var(--grey2); +} + +.page__title { + position: relative; +} + +.page__title::before { + position: absolute; + content: "/"; + top: 0; + left: -1rem; +} + +/* +====================== +Navigation Media Query +====================== +*/ +@media only screen and (max-width: 768px) { + .nav__menu { + position: fixed; + top: 0; + left: -30rem; + width: 0; + background-color: var(--white); + z-index: 9990; + height: 100%; + transition: all 0.5s; + } + + .nav__menu.open { + left: 30rem; + width: 30rem; + } + + .nav__logo { + width: 50%; + } + + .nav__hamburger { + display: block; + } + + .menu__top { + display: flex; + align-items: center; + justify-content: space-between; + background-color: var(--orange); + padding: 1.8rem 1rem; + } + + .menu__top svg { + height: 1.6rem; + width: 1.6rem; + fill: var(--primaryColor); + } + + .nav__category { + color: var(--white); + font-size: 2.3rem; + font-weight: 700; + } + + .nav__list { + flex-direction: column; + align-items: start; + padding: 1.6rem 1rem; + } + + .nav__item:not(:last-child) { + margin-right: 0; + } + + .nav__item { + width: 100%; + } + + .nav__list .nav__link:link, + .nav__list .nav__link:visited { + padding: 1.6rem 0; + width: 100%; + color: var(--grey2); + } + + body.active::before { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 110%; + background: var(--black) none no-repeat 0 0; + opacity: 0.7; + z-index: 999; + transition: 0.8s; + } +} + +@media only screen and (max-width: 568px) { + .nav__icons .icon__item svg { + width: 1.4rem; + height: 1.4rem; + } + + .nav__icons .icon__item { + padding: 0.4rem; + } +} + +/*kasunika dev start edit*/ +/* Add logout button styles */ +/* Only for the logout/login button */ +.logout-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1.5rem; /* less vertical padding than horizontal */ + background-color: #ece8e9; + +/* Chatbot styles - elegant floating widget */ +.chatbot-fab { + position: fixed; + right: 1.6rem; + bottom: 1.6rem; + width: 56px; + height: 56px; + border-radius: 50%; + background: linear-gradient(135deg, var(--orange), #ff6b6b); + border: none; + box-shadow: 0 6px 18px rgba(0,0,0,0.18); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 14000; +} +.chatbot-fab img { display:block; width:36px; height:36px; } + +.chatbot-hide { display: none; } +.chatbot-header { display:flex; align-items:center; justify-content:space-between; padding: .8rem; border-bottom: 1px solid rgba(0,0,0,0.06); background: linear-gradient(180deg,#fff,#fafafa); } +.chatbot { font-family: inherit } +.chatbot-window { position: fixed; right: 1.6rem; bottom: 7.6rem; width: 360px; max-width: calc(100% - 3.2rem); height: 520px; max-height: 80vh; display:flex; flex-direction:column; border-radius: 12px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.2); background: #fff; z-index: 14000; } +.chatbot-header-left { display:flex; gap:.6rem; align-items:center } +.chatbot-avatar { border-radius:50%; width:36px; height:36px; } +.chatbot-title strong { display:block; font-size:1.4rem; } +.chatbot-status { display:block; font-size:1.1rem; color:var(--green); } +.chatbot-header-right { display:flex; gap:.4rem } +.chatbot-icon { background:transparent; border:none; font-size:1.1rem; padding:.4rem .6rem; cursor:pointer } + +.chatbot-messages { padding:1rem; overflow:auto; background: linear-gradient(180deg,#bbbaba,#eed2d2); display:flex; flex-direction:column; gap:.6rem; flex:1 } +.chatbot-messages .chatbot-message { max-width:78%; padding:.6rem .8rem; border-radius:10px; font-size:1.4rem; line-height:1.3 } +.chatbot-messages .user { align-self:flex-end; background: #0b84ff; color: #fff; border-bottom-right-radius:2px } +.chatbot-messages .bot { align-self:flex-start; background:#f1f3f5; color:var(--black3) } + +.chatbot-form { display:flex; gap:.6rem; padding:.8rem; border-top:1px solid rgba(0,0,0,0.06); background:#fff } +.chatbot-controls { display:flex; align-items:center; gap:.6rem; width:100% } +.chatbot-attach { background:transparent; border:none; font-size:1.4rem; cursor:pointer; padding:.4rem .6rem } +.chatbot-input { flex:1; padding:.8rem 1rem; border-radius:999px; border:1px solid var(--grey3); font-size:1.4rem } +.chatbot-send { background:var(--orange); color:#fff; border:none; padding:.8rem 1rem; border-radius:999px; cursor:pointer } +.chatbot-attachments { padding: .4rem .8rem; display:flex; gap:.4rem; flex-wrap:wrap } +.chatbot-attachments .attachment { background: #f4f6f8; border-radius:999px; padding: .4rem .8rem; font-size:1.2rem; display:inline-flex; align-items:center; gap:.4rem } +.chatbot-attachments .attachment button { background:transparent; border:none; cursor:pointer; font-size:1.1rem } + +/* Minimized state */ +.chatbot-minimized { height: 48px !important; width: 280px !important; overflow:visible } +.chatbot-minimized .chatbot-messages, .chatbot-minimized .chatbot-form { display:none } +.chatbot-minimized .response-time { display:none } +.chatbot-minimized .chat-body { display:none } +.chatbot-minimized .chat-footer { display:none } + +/* Modern Chatbot UI Styles - Minimized state */ +.chatbot-container.chatbot-minimized { + height: 60px !important; + width: 320px !important; + overflow: hidden; +} + +.chatbot-container.chatbot-minimized .response-time, +.chatbot-container.chatbot-minimized .chat-body, +.chatbot-container.chatbot-minimized .chat-footer, +.chatbot-container.chatbot-minimized .powered-by { + display: none !important; +} + +.chatbot-container.chatbot-minimized .chat-header { + border-bottom: none; +} + +.chatbot-container.chatbot-minimized .header-icons .fa-chevron-down { + transform: rotate(180deg); +} + +@media only screen and (max-width: 480px) { + .chatbot-window { right: .8rem; bottom: 5.2rem; width: 92%; height: 60vh } + .chatbot-fab { right: .8rem; bottom: .8rem } +} + +/* Product page specs grid and lightbox */ +.specs-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.6rem 1.2rem; + margin-top: 0.8rem; +} +.spec-item { display:flex; justify-content:space-between; padding: .45rem .6rem; background: #fafafa; border-radius:6px; } +.spec-key { font-weight:600; color:var(--black2); } +.spec-val { color:var(--black3); } + +/* Lightbox modal */ +.lightbox-modal { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.7); align-items:center; justify-content:center; z-index:2000; } +.lightbox-modal.open { display:flex; } +.lightbox-modal .lightbox-inner { max-width:90%; max-height:90%; } +.lightbox-modal img { max-width:100%; max-height:100%; border-radius:8px; box-shadow:0 6px 30px rgba(0,0,0,0.6); } + color: rgb(20, 19, 19) !important; + border: none; + border-radius: 6px; /* Slightly rounded corners, not an ellipse */ + font-size: 1.4rem; + font-weight: 500; + margin-left: 10px; + height: 38px; /* Match the icon button height */ + min-width: 80px; + box-sizing: border-box; + transition: background-color 0.3s; + cursor: pointer; +} + +.logout-btn:hover { + background-color: #c10020; +} + +/* Fix for alignment with icon buttons */ +.nav__icons { + display: flex; + align-items: center; + gap: 1rem; +} + +.nav__icons .user-name { + margin-right: 0.25rem; +} + +.icon__logout { + width: 1.6rem; + height: 1.6rem; + fill: white; + margin-right: 5px; + flex-shrink: 0; +} + +/* Admin icon styling */ +.icon__admin-img { + width: 1.6rem; + height: 1.6rem; + border-radius: 4px; + object-fit: cover; + display: block; +} +/*for mobile devices*/ + +@media only screen and (max-width: 768px) { + .logout-btn { + margin: 10px 0; + width: 100%; + justify-content: center; + } + + .nav__auth { + flex-direction: column; + width: 100%; + } +} + +/*kasunika dev end*/ + +/* +====================== +Hero Area +====================== +*/ + +.hero, +.hero .glide__slides { + background-color: var(--primaryColor); + position: relative; + width: 100%; + height: 100vh; +} + +.hero .glide__bullet { + background-color: #222; + width: 1.2rem; + height: 1.2rem; +} + +.hero .glide__arrow { + padding: 1.5rem 1.7rem; + opacity: 0; + border: none; + background-color: var(--grey); + transition: all 0.5s ease-in-out 0.2s; + border-radius: 50%; /*vishmitha edited */ +} + +.glide__arrow:hover { + background-color: var(--black); +} + +.glide__arrow--left { + left: 20rem; +} + +.glide__arrow--right { + position: absolute; + right: 20rem; +} + +.hero:hover .glide__arrow { + opacity: 1; +} + +.hero:hover .glide__arrow--left { + left: 23rem; +} + +.hero:hover .glide__arrow--right { + right: 23rem; +} + +.hero .glide__arrow svg { + height: 1.8rem; + width: 1.8rem; + fill: var(--primaryColor); +} + + +.hero__center { + display: flex; + align-items: center; + justify-content: center; + position: relative; + max-width: 114rem; + margin: 0 auto; + height: 100%; + padding-top: 3rem; +} + +.hero__left { + padding: 0 3rem; + flex: 0 0 50%; +} + +.hero__btn { + display: inline-block; + font-weight: 700; + font-size: 1.4rem; + background-color: var(--black); + color: var(--primaryColor); + cursor: pointer; + margin-top: 1rem; + padding: 1.5rem 4rem; + border: 1px solid var(--black); + transition: all 0.3s ease-in-out; +} + +.hero__btn:focus { + outline: none; +} + +.hero__left .hero__btn:hover { + font-weight: 700; + background-color: transparent; + color: var(--black); +} + +.hero__left h1 { + margin: 1rem 0; +} + +.hero__left p { + margin-bottom: 1rem; +} + +.hero__right { + flex: 0 0 50%; + position: relative; + text-align: center; +} + +.hero__right img.banner_03 { + width: 70%; +} + +/* +====================== +Hero Media Query +====================== +*/ +@media only screen and (max-width: 999px) { + .hero__center { + flex-direction: column; + text-align: center; + } + + .hero__right { + top: 50%; + position: absolute; + } + + .hero__left { + position: absolute; + padding: 0 1rem; + top: 20%; + } + + .hero__right img { + width: 55%; + } + + .hero__btn { + padding: 1.1rem 2.8rem; + } + + .hero .glide__arrows { + display: none; + } +} + +@media only screen and (max-width: 567px) { + .hero, + .hero .glide__slides { + height: 60vh; + } + + .hero__right { + display: none; + } +} + +/* +====================== +Collection +====================== +*/ + +.section { + padding: 3rem 0; +} + +.collection { + margin: 3rem 0; +} + +.collection__container { + width: 100%; + padding: 0 1.6rem; + display: flex; + height: 100%; + align-items: center; + justify-content: space-between; +} + +.collection__box { + display: flex; + justify-content: space-around; + align-items: center; + padding: 1rem; + flex: 0 0 48%; + height: 30rem; + background-color: var(--primaryColor); +} + +.collection__box:not(:last-child) { + margin-right: 1.6rem; +} + +.img__container { + width: 25rem; + text-align: center; +} + +.collection__box img.collection_01 { + width: 60%; +} + +.collection__box img.collection_02 { + width: 75%; +} + +.collection__content { + flex: 0 0 50%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.collection__content span { + color: var(--black); +} + +.collection__content h1 { + margin-top: 0.5rem; +} + +.collection__content a:link, +.collection__content a:visited { + font-weight: 700; + display: inline-block; + padding: 1rem 1.4rem; + margin-top: 1.3rem; + border-radius: 3rem; + border: 2px solid var(--secondaryColor); + color: var(--primaryColor); + background-color: var(--secondaryColor); + transition: all 0.3s ease-in-out; +} + +.collection__content a:hover { + background-color: transparent; + color: var(--secondaryColor); +} + +/* +====================== +Collection Media Query +====================== +*/ +@media only screen and (max-width: 999px) { + .collection__container { + width: 80%; + margin: 0 auto; + flex-direction: column; + height: 65rem; + } + + .collection__box { + width: 100%; + margin: 0 auto; + } + + .collection__box:not(:last-child) { + margin: 0 0 1.6rem; + } +} + +@media only screen and (max-width: 568px) { + .collection { + margin: 0; + position: relative; + } + + .collection__container { + width: 100%; + height: 50rem; + text-align: center; + flex-direction: column; + justify-content: space-around; + } + + .collection__box { + justify-content: space-around; + height: 15rem; + } + + .collection__content { + flex: 0 0 30%; + } + + .collection__data span { + font-size: 1.2rem; + } + + .collection__data h1 { + font-size: 2rem; + } +} + +/* +====================== +Latest Products +====================== +*/ + +.title__container { + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 6rem; + padding: 2rem 0; + background-color: var(--primaryColor); +} + +.section__titles:not(:last-child) { + margin-right: 1.5rem; +} + +.section__title { + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.section__title h1 { + font-size: 1.9rem; + font-weight: inherit; +} + +.section__title:hover .primary__title, +.section__title:hover span.dot, +.section__title:hover span.dot::before { + opacity: 1; +} + +.section__title .primary__title { + opacity: 0.6; + padding-left: 0.5rem; + transition: opacity 0.3s ease-in-out; +} + +span.dot { + opacity: 0.6; + padding: 0.45rem; + position: relative; + border: 1px solid var(--black); + cursor: pointer; + transition: opacity 0.3s ease-in-out; +} + +span.dot::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border: 1px solid var(--black); + background-color: var(--black); + margin: 1px; + opacity: 0.6; +} + +.section__title.active span.dot { + opacity: 1; +} + +.section__title.active span.dot::before { + opacity: 1; +} + +.section__title.active .primary__title { + opacity: 1; +} + + +.product { + position: relative; + text-align: center; +} + +.product ul svg { + width: 1.7rem; + height: 1.7rem; + fill: var(--white); +} + +.product ul { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + top: 35%; + left: 50%; + width: 17rem; + height: 5rem; + background-color: rgba(255, 255, 255, 0.5); + opacity: 0; + visibility: hidden; + transform: translate(-50%, -50%) scale(0.7); + transition: all 0.5s ease-in-out; +} + +.product:hover ul { + opacity: 1; + visibility: visible; + transform: translate(-50%, -50%) scale(1); +} + +.product ul li:not(:last-child) { + margin-right: 1.6rem; +} + +.product ul a { + position: relative; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--orange); + width: 3.5rem; + height: 3.5rem; + cursor: pointer; + transition: 0.5s; +} + +.product ul a:hover { + background-color: var(--black); +} + +.product ul a::before { + content: ""; + position: absolute; + top: -0.6rem; + left: -0.6rem; + height: 0; + width: 0; + border-top: 3px solid var(--orange); + border-left: 3px solid var(--orange); + transition: 0.5s; + opacity: 0; + z-index: 1; +} + +.product ul a::after { + content: ""; + position: absolute; + bottom: -0.6rem; + right: -0.6rem; + width: 0; + height: 0; + border-bottom: 3px solid var(--orange); + border-right: 3px solid var(--orange); + z-index: 1; + opacity: 0; + transition: 0.5s; +} + +.product ul a:hover::before { + width: 126%; + height: 126%; + border-top: 3px solid var(--black); + border-left: 3px solid var(--black); + opacity: 1; +} + +.product ul a:hover::after { + width: 126%; + height: 126%; + border-bottom: 3px solid var(--black); + border-right: 3px solid var(--black); + opacity: 1; +} + +@media only screen and (max-width: 567px) { + .title__container { + flex-direction: column; + } + + .section__titles:not(:last-child) { + margin: 0 0 1.3rem; + } +} + +.product { + display: flex; + flex-direction: column; + text-align: center; + width: 25rem; +} + +.product__header { + height: 25rem; + padding: 0.5rem ; + text-align: center; + overflow: hidden; +} + +.product__header img { + width: 100%; + height: 100%; + object-fit: contain; /* This is the key to fixing the distortion. */ +} + +.product__footer { + padding: 1rem 0; +} + +.rating svg { + width: 1.6rem; + height: 1.6rem; + fill: var(--yellow); +} + +.product__footer h3 { + padding: 1rem 0; +} + +.product__footer .product__price { + padding-bottom: 1rem; +} + +.product__footer h3 { + font-size: 1.6rem; +} + +.product__btn { + display: inline-block; + font-weight: 700; + text-transform: uppercase; + width: 100%; + padding: 1.4rem 0; + border: 1px solid var(--black); + color: var(--black); + cursor: pointer; + background-color: var(--black); /*vishmitha edited */ + color: var(--white); /*vishmitha edited */ + border: none; /*vishmitha edited */ + transition: background 0.3s; /*vishmitha edited */ +} + +.product__btn:focus { + outline: none; +} + +.product__btn:hover { + background-color: var(--black); + color: var(--primaryColor); +} + +.latest__products .glide__arrow, +.related__products .glide__arrow { + background-color: transparent; + border: 1px solid var(--primaryColor); + outline: none; + border-radius: 0; + box-shadow: 0 0.25em 0.5em 0 rgba(0, 0, 0, 0); + top: -7%; + left: 80%; +} + +.latest__products .glide__arrow:hover, +.related__products .glide__arrow:hover { + background-color: var(--orange); + border: 1px solid var(--primaryColor); +} + +.latest__products .glide__arrow--left, +.related__products .glide__arrow--left { + left: 75%; +} + +.latest__products .glide__arrow--right, +.related__products .glide__arrow--right { + right: 5%; +} + +.latest__products .glide__arrow svg, +.related__products .glide__arrow svg { + width: 1.5rem; + height: 1.5rem; + fill: var(--grey); +} + +/* +====================== +Latest Products Media Query +====================== +*/ +@media only screen and (max-width: 999px) { + .product__header { + height: 25rem; + } + + .product { + width: 70%; + margin: 0 auto; + } + + .latest__products .glide__arrow--left, + .related__products .glide__arrow--left { + left: 73%; + } + + .latest__products .glide__arrow--right, + .related__products .glide__arrow--right { + right: 7%; + } +} + +@media only screen and (max-width: 768px) { + .latest__products .glide__arrow--left, + .related__products .glide__arrow--left { + left: 70%; + } + + .latest__products .glide__arrow--right, + .related__products .glide__arrow--right { + right: 7%; + } +} + +@media only screen and (max-width: 578px) { + .product__header { + height: 20rem; + } + + .product__btn:hover { + background-color: var(--black); + color: var(--primaryColor); + } + + .product__header img { + width: 100%; + height: 100%; + object-fit: contain; + } + + .product__footer h3 { + font-size: 1.4rem; + } + + .product__btn { + width: 100%; + font-size: 1rem; + padding: 0.8rem 0; + border: 1px solid var(--black); + } + + .product ul a { + width: 2.7rem; + height: 2.7rem; + } + + .product ul { + top: 25%; + left: 50%; + width: 16rem; + height: 4rem; + } + + .rating svg { + width: 1.3rem; + height: 1.3rem; + } + + .latest__products .glide__arrow--left, + .related__products .glide__arrow--left { + left: 66%; + } + + .latest__products .glide__arrow--right, + .related__products .glide__arrow--right { + left: 85%; + } +} + +@media only screen and (max-width: 460px) { + .product__header { + height: 12rem; + } + + .product__footer h3 { + font-size: 1.2rem; + } +} + +/* +====================== +Category Center +====================== +*/ +.category__center { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 3rem 2rem; +} + +@media only screen and (max-width: 999px) { + .category__center { + grid-template-columns: 1fr 1fr 1fr; + } +} + +@media only screen and (max-width: 568px) { + .category__center { + grid-template-columns: 1fr 1fr; + gap: 1.5rem 1rem; + } + + .category__products .product__header { + height: 10rem; + } + + .category__products .product__header img { + object-fit: cover; + } +} + +/* +====================== +Pop Up +====================== +*/ + +.popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background-color: rgba(0, 0, 0, 0.3); + z-index: 9999; + transition: 0.3s; + transform: scale(1); +} + +.popup__content { + position: absolute; + top: 50%; + left: 50%; + width: 90%; + max-width: 110rem; + margin: 0 auto; + height: 55rem; + transform: translate(-50%, -50%); + padding: 1.6rem; + display: table; + overflow: hidden; + background-color: var(--white); +} + +.popup__close { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 2rem; + right: 2rem; + padding: 1.5rem; + background-color: var(--primaryColor); + border-radius: 50%; + cursor: pointer; +} + +.popup__close svg { + width: 1.7rem; + height: 1.7rem; +} + +.popup__left { + display: table-cell; + width: 50%; + height: 100%; + vertical-align: middle; +} + +.popup__right { + display: table-cell; + width: 50%; + vertical-align: middle; + padding: 3rem 5rem; +} + +.popup-img__container { + width: 100%; + overflow: hidden; +} + +.popup-img__container img.popup__img { + display: block; + width: 60rem; + height: 45rem; + max-width: 100%; + border-radius: 1rem; + object-fit: cover; +} + +.right__content { + text-align: center; + width: 85%; + margin: 0 auto; +} + +.right__content h1 { + font-size: 4rem; + color: #000; + margin-bottom: 1.6rem; +} + +.right__content h1 span { + color: var(--green); +} + +.right__content p { + font-size: 1.8rem; + color: var(--grey2); + margin-bottom: 1.6rem; +} + +.popup__form { + width: 100%; + padding: 2rem 0; + text-indent: 1rem; + margin-bottom: 1.6rem; + border-radius: 3rem; + background-color: var(--primaryColor); + border: none; +} + +.popup__form:focus { + outline: none; +} + +.right__content a:link, +.right__content a:visited { + display: inline-block; + padding: 1.8rem 5rem; + border-radius: 3rem; + font-weight: 700; + color: var(--white); + background-color: var(--black); + border: 1px solid var(--grey2); + transition: 0.3s; +} + +.right__content a:hover { + background-color: var(--green); + border: 1px solid var(--grey2); + color: var(--black); +} + +.hide__popup { + transform: scale(0.2); + opacity: 0; + visibility: hidden; +} + +/* Popup animations */ +@keyframes slideInDown { + from { + opacity: 0; + transform: translateX(-50%) translateY(-20px); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +/* Popup accessibility and responsiveness improvements */ +.popup { + display: flex; + align-items: center; + justify-content: center; +} + +.popup__content { + position: relative; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + animation: popupSlideIn 0.3s ease-out; +} + +.popup__close { + cursor: pointer; + transition: all 0.2s ease; + background: rgba(0, 0, 0, 0.1); + border-radius: 50%; + width: 35px; + height: 35px; + display: flex; + align-items: center; + justify-content: center; +} + +.popup__close:hover { + background: rgba(0, 0, 0, 0.2); + transform: scale(1.1); +} + +.popup__form { + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.popup__form:focus { + outline: none; + border-color: var(--orange); + box-shadow: 0 0 0 3px rgba(235, 0, 40, 0.1); +} + +.popup__right a { + transition: all 0.2s ease; + display: inline-block; + text-decoration: none; + padding: 12px 24px; + background: var(--orange); + color: white; + border-radius: 5px; + font-weight: bold; + margin-top: 10px; +} + +.popup__right a:hover { + background: #d10024; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(235, 0, 40, 0.3); +} + +@keyframes popupSlideIn { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.8); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +/* +====================== +Go Up +====================== +*/ +.goto-top:link, +.goto-top:visited { + position: fixed; + right: 2%; + bottom: 10%; + padding: 0.8rem 1.4rem; + border-radius: 1rem; + background-color: var(--orange); + visibility: hidden; + cursor: pointer; + transition: 0.3s; + animation: bounce 2s ease-in-out infinite; +} + +.show-top:link, +.show-top:visited { + visibility: visible; + z-index: 1999; +} + +@keyframes bounce { + 0% { + transform: scale(0.5); + } + + 50% { + transform: scale(1.5); + } + + 0% { + transform: scale(1); + } +} + +.goto-top svg { + width: 1.3rem; + height: 1.3rem; + fill: var(--white); +} + +.goto-top:hover { + background-color: var(--black4); +} + +@media only screen and (min-width: 1250px) { + /* .hero__left span{} */ + + .hero__left h1{ + font-size: 5rem; + } +} + +@media only screen and (max-width: 1200px) { + .right__content { + width: 100%; + } + + .right__content h1 { + font-size: 3.5rem; + margin-bottom: 1.3rem; + } +} + +@media only screen and (max-width: 998px) { + .popup__right { + width: 100%; + } + + .popup__left { + display: none; + } + + .popup__content { + width: 95% !important; + height: auto !important; + max-height: 90vh !important; + display: block !important; + overflow-y: auto !important; + } + + .popup__right { + width: 100% !important; + padding: 2rem !important; + text-align: center; + } + + .popup__close { + top: 10px !important; + right: 10px !important; + position: absolute !important; + } + + .right__content h1 { + font-size: 4rem !important; + margin-bottom: 1rem; + } + + .right__content p { + font-size: 1.4rem !important; + margin-bottom: 1.5rem; + } + + .popup__form { + width: 100% !important; + margin-bottom: 1rem !important; + } +} + +@media only screen and (max-width: 768px) { + .goto-top:link, + .goto-top:visited { + right: 5%; + bottom: 5%; + } +} + +@media only screen and (max-width: 568px) { + .popup__content { + height: auto !important; + width: 95% !important; + margin: 0 auto; + padding: 1rem !important; + } + + .popup__right { + padding: 1rem !important; + } + + .right__content { + width: 100%; + } + + .right__content h1 { + font-size: 2.4rem !important; + } + + .right__content p { + font-size: 1.3rem !important; + } + + .popup__form { + padding: 1.2rem !important; + font-size: 1.4rem !important; + } + + .popup__close { + width: 30px !important; + height: 30px !important; + } + + .right__content p { + font-size: 1.4rem; + } + + .popup__form { + width: 100%; + padding: 1.5rem 0; + margin-bottom: 1.3rem; + } + + .right__content a:link, + .right__content a:visited { + padding: 1.5rem 3rem; + } + + .popup__close { + top: 1rem; + right: 1rem; + padding: 1.3rem; + } + + .popup__close svg { + width: 1.4rem; + height: 1.4rem; + } +} + +/* +====================== +Facility +====================== +*/ + +.facility__section { + background-color: var(--primaryColor); + padding: 6rem 0; +} + +.facility__contianer { + display: grid; + align-items: center; + grid-template-columns: repeat(4, 1fr); +} + +.facility-img__container svg { + width: 3rem; + height: 3rem; + transition: 1s; + perspective: 4000; +} + +.facility__box { + text-align: center; +} + +.facility-img__container { + position: relative; + display: inline-block; + line-height: 9.5rem; + width: 8rem; + height: 8rem; + border-radius: 50%; + border: 1.5px solid var(--white); + z-index: 1; + margin-bottom: 1.6rem; +} + +.facility-img__container::before { + content: ""; + position: absolute; + display: inline-block; + border-radius: 50%; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: 0.7rem; + background-color: var(--white); + z-index: -1; +} + +.facility__box:hover svg { + transform: rotateY(180deg); + line-height: 9.5rem; +} + +/* +====================== +Facility Media Query +====================== +*/ +@media only screen and (max-width: 998px) { + .facility__contianer { + grid-template-columns: 1fr 1fr; + row-gap: 3rem; + } +} + +@media only screen and (max-width: 568px) { + .facility__contianer { + grid-template-columns: 1fr; + } + + .facility-img__container { + width: 7rem; + height: 7rem; + line-height: 8.5rem; + } + + .facility__contianer p { + font-size: 1.4rem; + } +} + +/* +====================== +Testimonial +====================== +*/ + +.testimonial { + position: relative; + background: url("./images/testimonial.jpg") center/cover no-repeat; + height: 50rem; + padding: 5rem 0; + z-index: 1; + text-align: center; +} + +.testimonial::before { + content: ""; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.9); + z-index: -1; +} + +.client__image { + margin-bottom: 3rem; +} + +.client__image img { + width: 7rem; + height: 7rem; + max-width: 100%; + object-fit: cover; + border-radius: 50%; +} + +.testimonial__container { + height: 100%; + padding: 1rem; +} + +.testimonial__box { + width: 90%; + margin: 0 auto; + height: 100%; + color: #ccc; +} + +.testimonial__box p { + width: 70%; + margin: 0 auto; + line-height: 2.5rem; + font-style: italic; + font-size: 1.5rem; + margin-bottom: 3rem; +} + +.client__info h3 { + font-weight: 400; + font-size: 2rem; + margin-bottom: 1rem; +} + +.client__info span { + font-size: 1.4rem; +} + +.testimonial .glide__bullets { + bottom: -20%; +} + +/* +====================== +Testimonial Media Query +====================== +*/ +@media only screen and (max-width: 1200px) { + .testimonial__box { + height: 100%; + } + + .testimonial__box p { + width: 90%; + margin: 0 auto; + line-height: 2.2rem; + margin-bottom: 3rem; + } + + .client__image { + margin-bottom: 2.5rem; + } +} + +@media only screen and (max-width: 568px) { + .testimonial { + height: 100%; + padding: 4rem 0 5rem; + z-index: 1; + text-align: center; + } + + .testimonial__box { + height: 100%; + } + + .testimonial__box p { + width: 100%; + font-size: 1.3rem; + line-height: 2rem; + margin-bottom: 2rem; + } + + .client__image { + margin-bottom: 1.5rem; + } + + .testimonial__box span { + margin-bottom: 1rem; + } + + .testimonial .glide__bullets { + bottom: -8%; + } +} + +/* +====================== +News Section +====================== +*/ + +.news { + padding-bottom: 8rem; +} +.news .glide__slides { + display: flex; + align-items: stretch; +} + +/* each slide must also be flex so its card can stretch */ +.news .glide__slide { + display: flex; + align-items: stretch; +} +.new__card { + background-color: var(--primaryColor); + width: 95%; + margin: 0 auto; + display: flex; + flex-direction: column; + height: 100%; /* fill the parent slide */ + min-height: 38rem; /* ensures same visual height */ + box-sizing: border-box; +} +.new__card .card__header img { + width: 100%; + height: 25rem; + object-fit: cover; + display: block; +} + +.card__footer { + padding: 1rem 1.6rem; + display: flex; + flex-direction: column; + flex: 1 1 auto; + justify-content: space-between; /* keeps button at bottom */ + box-sizing: border-box; +} +.new__card:not(:last-child) { + margin-right: 1rem; +} + + +.card__footer h3 { + font-size: 2.5rem; + font-weight: 600; + color: var(--black); + margin-bottom: 1rem; +} + +.card__footer span { + display: inline-block; + margin-bottom: 1rem; + color: var(--black2); +} + +.card__footer p { + font-size: 1.5rem; + color: var(--black2); + margin-bottom: 1.6rem; + line-height: 2.7rem; + flex: 1; +} + +.card__footer a button, +.card__footer a button { + display: inline-block; + padding: 1.4rem 4rem; + border: 1px solid var(--black); + color: black; /* edited */ + color: var(--black); + cursor: pointer; + transition: 0.3s; +} + +.card__footer a button:focus { + outline: none; +} + +.card__footer a button:hover { + border: 1px solid var(--black); + color: var(--white); + background-color: var(--black); +} + + +/*edit rashmi dev*/ +/* +======================= +Search & Filter Styles +======================= +*/ +.search-wrapper-custom { + position: relative; + display: inline-block; + margin-left: 15px; +} + +.search-wrapper-custom input[type="text"] { + width: 250px; + padding: 8px 40px 8px 14px; /* extra right space for icon */ + border: 1px solid #ccc; + border-radius: 6px; + background: #fff; /* prevent red leaking */ +} + +/* Font Awesome search icon variant */ +.search-wrapper-custom .search-icon { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + font-size: 1.4rem; + color: #666; + cursor: pointer; + transition: color .25s ease; + line-height: 1; +} + +.search-wrapper-custom .search-icon:hover { color: var(--orange); } + +.search-wrapper-custom input[type="text"]:focus + .search-icon, +.search-wrapper-custom .search-icon:focus { color: var(--orange); } + +body.darkMode .search-wrapper-custom .search-icon { color: #bbb; } +body.darkMode .search-wrapper-custom .search-icon:hover { color: var(--orange); } +body.darkMode .search-wrapper-custom input[type="text"]:focus + .search-icon { color: var(--orange); } + +.icon__search { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + width: 18px; + height: 18px; + fill: #555; /* proper gray */ + cursor: pointer; +} + +.filter-dropdown-custom { + display: none; + position: absolute; + top: 42px; + right: 0; + width: 400px; + max-height: 500px; + overflow-y: auto; + background:#E6EAFF; + border: 1px solid #ddd; + border-radius: 8px; + padding: 15px; + box-shadow: 0 2px 12px rgba(0,0,0,0.15); + z-index: 1000; +} + +.filter-dropdown-custom.show { display: block; } + +.filter-dropdown-custom h3 { + margin: 0 0 12px; + font-size: 16px; + border-bottom: 1px solid #eee; + padding-bottom: 6px; +} + +.filter-section { + display: flex; + align-items: center; + margin: 8px 0; +} + +.filter-section label { + width: 140px; + font-size: 14px; + margin: 0; +} + +.filter-section select, +.filter-section input[type="checkbox"], +.filter-section input[type="range"], +.filter-section span { + flex: 1; +} + +.price-filter { + flex-direction: row; + align-items: center; +} + +.price-filter span { + margin-left: 8px; + font-weight: bold; +} + +.price-filter input[type="range"] { + margin-left: 8px; + width: 100%; +} + +.clear-btn { + background: #e60023; + color: #fff; + border: none; + padding: 6px 12px; + border-radius: 6px; + cursor: pointer; + margin-top: 10px; + width: 100%; +} + +.clear-btn:hover { + background: #c5001f; +} + +.dropdown-products { + margin-top: 15px; + border-top: 1px solid #ddd; + padding-top: 10px; + max-height: 300px; + overflow-y: auto; +} + +.dropdown-products .product { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + border-bottom: 1px solid #00308F; + font-size: 14px; + cursor: pointer; + + /* Animation */ + opacity: 0; + transform: translateY(20px); + animation: fadeSlide 0.4s forwards; +} + +.dropdown-products .product:hover { + background: #F0F8FF; +} + +.no-results { + text-align: center; + font-size: 18px; + color: #888; + padding: 10px; +} + +#loadMoreBtn { + background: #35538F; + color: #fff; + border: none; + padding: 6px 12px; + width: 100%; + cursor: pointer; + border-radius: 6px; + margin-top: 10px; +} + +#loadMoreBtn:hover { + background:#003058; +} + +/* Fade + slide animation */ +@keyframes fadeSlide { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Header fix */ +.header, +.navigation, +.nav { + background: #fff; + position: relative; + z-index: 10000; +} + +/* end rashmi dev */ + + +/* +====================== +NewsLetter +====================== +*/ + +.newsletter { + padding: 6rem 0; + border-top: 1px solid var(--primaryColor); +} + +.newsletter__content { + display: flex; + align-items: center; + justify-content: space-between; +} + +.newsletter__data h3 { + font-size: 2.4rem; + font-weight: inherit; + margin-bottom: 1rem; +} + +.newsletter__data p { + font-size: 1.5rem; + color: var(--black2); +} + +.newsletter__email { + font-size: 1.4rem; + display: inline-block; + width: 37rem; + padding: 1.6rem; + background-color: var(--primaryColor); + border: none; + text-indent: 1rem; +} + +.newsletter__email:focus { + outline: none; +} + +.newsletter__link:link, +.newsletter__link:visited { + display: inline-block; + padding: 1.4rem 3rem; + margin-left: -0.5rem; + background-color: var(--black); + color: var(--white); + transition: 0.3s; +} + +.newsletter__link:hover { + background-color: #000; +} + +/* +====================== +Newsletter Media Query +====================== +*/ +@media only screen and (max-width: 998px) { + .newsletter { + padding: 6rem 4rem; + } + + .newsletter__content { + flex-direction: column; + align-items: center; + } + + .newsletter__email { + width: 45rem; + } + + .newsletter__data { + margin-bottom: 2rem; + } +} + +@media only screen and (max-width: 768px) { + .newsletter__content { + align-items: center; + justify-content: center; + text-align: center; + } + + .newsletter__email { + width: 45rem; + display: block; + margin-bottom: 1.6rem; + } +} + +@media only screen and (max-width: 568px) { + .newsletter__email { + width: 23rem; + font-size: 1.2rem; + } + + .newsletter__data h3 { + font-size: 1.6rem; + } + + .newsletter__data p { + font-size: 1rem; + } + + .newsletter__link:link, + .newsletter__link:visited { + padding: 1.2rem 2rem; + margin-left: 0; + } +} + +/* +====================== +Footer +====================== +*/ + +.footer { + background-color: var(--black3); + padding: 6rem 1rem; + line-height: 3rem; +} + +.footer-top__box span svg { + width: 1.6rem; + height: 1.6rem; + fill: var(--grey3); +} + +.footer-top__box span { + margin-right: 1rem; +} + +.footer__top { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 2rem; + color: var(--grey3); +} + +.footer-top__box a:link, +.footer-top__box a:visited { + display: block; + color: var(--grey); + font-size: 1.4rem; + transition: 0.6s; +} + +.footer-top__box a:hover { + color: var(--orange); +} + +.footer-top__box div { + color: var(--grey); + font-size: 1.4rem; +} + +.footer-top__box h3 { + font-size: 1.8rem; + font-weight: 400; + margin-bottom: 1rem; +} + +@media only screen and (max-width: 1200px) { + .footer__top { + grid-template-columns: repeat(3, 1fr); + row-gap: 2rem; + } +} + +@media only screen and (max-width: 998px) { + .footer__top { + grid-template-columns: repeat(2, 1fr); + row-gap: 2rem; + } +} + +@media only screen and (max-width: 768px) { + .footer__top { + grid-template-columns: 1fr; + row-gap: 2rem; + } +} + +/* +====================== +Embedded Map Styling +====================== +*/ +.embedded-map { + margin-top: 1rem; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.embedded-map iframe { + width: 100%; + height: 180px; + border: none; + display: block; +} + +/* +====================== +Embedded Map Media Query +====================== +*/ +@media only screen and (max-width: 1200px) { + .embedded-map iframe { + height: 160px; + } +} + +@media only screen and (max-width: 998px) { + .embedded-map iframe { + height: 150px; + } +} + +@media only screen and (max-width: 768px) { + .embedded-map { + margin-top: 0.8rem; + } + + .embedded-map iframe { + height: 180px; + } +} + +@media only screen and (max-width: 568px) { + .embedded-map iframe { + height: 160px; + } +} + +/* +====================== +Product Details +====================== +*/ + +.details__container--left, +.product-detail__container { + display: flex; + align-items: flex-start; +} + +.product-detail__container { + display: grid; + grid-template-columns: repeat(2, 1fr); + padding: 2.5rem 0; + margin: 0 auto; +} + +.product-detail__left { + flex: 0 0 50%; + margin-right: 2rem; +} + +.product-detail__right { + flex: 0 0 50%; +} + +.product-detail__container--left img { + width: 100%; + object-fit: cover; +} + +.product__pictures { + display: flex; + flex-direction: column; +} + +.pictures__container { + padding: 1rem; + border: 1px solid var(--primaryColor); + border-right-color: transparent; + cursor: pointer; + width: 6.2rem; + transition: 0.3s; +} + +.pictures__container:not(:last-child) { + border-bottom-color: transparent; +} + +.pictures__container:hover { + border: 1px solid var(--orange); +} + +.pictures__container img { + transition: 0.3s; +} + +.pictures__container:hover img { + scale: 1.1; +} + +.product__picture { + width: 100%; + border: 1px solid var(--primaryColor); + padding: 1rem; + display: flex; + justify-content: center; +} + +.product-details__btn { + display: flex; + justify-content: space-between; + margin-top: 2rem; +} + +.product-details__btn a { + flex: 0 0 47%; + display: inline-block; + padding: 1.6rem 3rem; + text-align: center; + color: var(--black); + border: 1px solid var(--black); +} + +.product-details__btn svg { + width: 1.9rem; + height: 1.9rem; + transition: 0.3s; +} + +.product-details__btn .add, +.product-details__btn .buy { + display: flex; + align-items: center; + justify-content: center; + transition: 0.3s; +} + +.product-details__btn .add span, +.product-details__btn .buy span { + margin-right: 1rem; +} + +.product-details__btn .add:hover, +.product-details__btn .buy:hover { + background-color: var(--black); + color: var(--primaryColor); +} + +.product-details__btn .add:hover svg, +.product-details__btn .buy:hover svg { + fill: var(--primaryColor); +} + +.product-detail__content { + width: 90%; + margin: 0 auto; +} + +.product-detail__content h3 { + font-size: 2.5rem; + margin-bottom: 1.3rem; +} + +.price { + margin-bottom: 1rem; +} + +.new__price { + font-size: 2rem; + color: var(--orange); +} + +.product-detail__content .product__review { + display: flex; + align-items: center; + margin-bottom: 1.6rem; + padding-bottom: 1.6rem; + border-bottom: 0.5px solid var(--primaryColor); +} + +.rating { + margin-right: 1rem; +} + +.product__review a:link, +.product__review a:visited { + color: var(--black); +} + +.product-detail__content p { + font-size: 1.4rem; + color: var(--black2); + line-height: 2.4rem; + margin-bottom: 1.6rem; +} + +.product__info .select { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1.6rem; +} + +.select .select-box { + background: none; + width: 18rem; + border: none; + padding: 0.5rem 1rem; + border-bottom: 1px solid var(--primaryColor); +} + +.select .select-box:focus { + outline: none; +} + +.select__option label { + font-size: 1.4rem; + color: var(--black3); + display: inline-block; + padding-bottom: 1rem; +} + +.input-counter { + display: flex; + align-items: center; +} + +.input-counter div { + display: flex; +} + +.input-counter li span { + font-size: 1.4rem; + color: var(--black3); + margin-right: 1rem; +} + +.minus-btn, +.plus-btn { + display: inline-block; + border: 1px solid var(--primaryColor); + padding: 0.8rem 1rem; + margin-right: 0; + cursor: pointer; +} + +.plus-btn { + border-left-color: transparent; +} + +.minus-btn { + border-right-color: transparent; +} + +.counter-btn { + width: 7rem; + padding: 1rem 0; + text-align: center; + border: 1px solid var(--primaryColor); +} + +.input-counter svg { + width: 1.8rem; + height: 1.8rem; + fill: var(--grey3); +} + +.product__info li { + margin-bottom: 1.6rem; +} + +.product__info .in-stock { + color: var(--green); +} + +.product__info li a { + font-size: 1.4rem; + color: var(--black2); +} + +.product-info__btn span svg { + width: 1.8rem; + height: 1.8rem; +} + +.product-info__btn { + display: flex; + align-items: center; +} + +.product-info__btn a { + display: flex; + align-items: center; + font-size: 1.2rem; + color: var(--black2); +} + +.product-info__btn a:not(:last-child) { + margin-right: 1rem; +} + +/* Product Details Bottom */ + +.detail__content { + position: relative; + height: 55rem; +} + +.detail__content .content { + position: absolute; + transform: translate(0, 25vh); + transition: all 0.6s ease-in-out; + opacity: 0; + visibility: hidden; + z-index: 555; +} + +.detail__content .content.active { + transform: translate(0, 0vh); + opacity: 1; + visibility: visible; +} + +#shipping h3, +#shipping p { + color: var(--grey2); +} + +#shipping p, +#description p { + padding: 1.6rem 0; + line-height: 2.8rem; +} + +#reviews { + font-size: 3rem; + font-weight: 500; + color: var(--grey2); + border-bottom: 1px solid var(--primaryColor); +} + +#description p, +#description li, +#description h2 { + color: var(--grey2); +} + +#description h2 { + font-weight: 500; + padding: 1rem 0; +} + +#description li { + line-height: 3rem; + color: vaf; +} + +#description ol { + padding-left: 1.6rem; +} + +/* Product Details Bottom Media Query*/ + +@media only screen and (max-width: 1200px) { + .detail__content { + height: 65rem; + } +} + +@media only screen and (max-width: 998px) { + .detail__content { + height: 70rem; + } +} + +@media only screen and (max-width: 768px) { + .detail__content { + height: 85rem; + } +} + +@media only screen and (max-width: 568px) { + .detail__content { + height: 110rem; + } +} + +@media only screen and (max-width: 450px) { + .detail__content { + height: 130rem; + } +} + +@media only screen and (max-width: 340px) { + .detail__content { + height: 160rem; + } +} + +/* +====================== +Product Details Media Query +====================== +*/ +@media only screen and (max-width: 998px) { + .select .select-box { + width: 15rem; + } + + .select__option label { + display: block; + } + + .product-info__btn { + flex-wrap: wrap; + } + + .product-details__btn a { + padding: 1rem 2.5rem; + font-size: 1.4rem; + } + + .picture__container { + width: 90%; + } +} + +@media only screen and (max-width: 768px) { + .details__container--left { + flex-direction: column-reverse; + text-align: center; + } + + .product__pictures { + margin-top: 2rem; + flex-direction: row; + justify-content: center; + } + + .pictures__container { + width: 50%; + border-right-color: var(--primaryColor); + } + + .pictures__container:not(:last-child) { + border-bottom-color: var(--primaryColor); + } + + .product-detail__container { + grid-template-columns: 1fr; + row-gap: 5rem; + } + + .product__info .select { + align-items: flex-start; + flex-direction: column; + } + + .select .select-box { + display: block; + width: 20rem; + } +} + +@media only screen and (max-width: 568px) { + .select .select-box { + width: 15rem; + } + + .input-counter { + align-items: flex-start; + flex-direction: column; + } + + .input-counter div { + margin-top: 1rem; + } +} + +@media only screen and (max-width: 400px) { + .product-details__btn a { + padding: 0.7rem 2rem; + font-size: 1.2rem; + } + + .product-details__btn .add span, + .product-details__btn .buy span { + margin-right: 0rem; + } + + .product__review .rating svg { + width: 1.4rem; + height: 1.4rem; + } +} + +/* +====================== +Cart Area +====================== +*/ +.cart__area { + padding-bottom: 5rem; +} + +.cart__form { + display: block; +} + +.product__thumbnail img { + width: 10rem; + height: 15rem; + object-fit: contain; +} + +.remove__cart-item svg { + width: 1.6rem; + height: 1.6rem; + fill: var(--grey2); + transition: all 0.3s ease-in-out; +} + +.cart__table { + display: block; + width: 100%; + margin-bottom: 4rem; + overflow-x: auto; +} + +.cart__table .table { + border-collapse: collapse; + width: 100%; + max-width: 150rem; +} + +.cart__table .table th { + font-weight: 500; + font-style: 2rem; + text-align: left; + padding: 1.8rem 0; +} + +.cart__table .table td { + vertical-align: middle; + padding: 1.8rem 0; + white-space: nowrap; + border-bottom: 1px solid var(--primaryColor); +} + +.cart__table .table thead { + border-bottom: 1px solid var(--primaryColor); +} + +.product__name a:link, +.product__name a:visited { + font-size: 1.5rem; + color: var(--black2); +} + +.product__name small { + color: var(--grey); + margin-top: 1.6rem; +} + +.product__subtotal .price { + display: inline; +} + +.product__subtotal .price .new__price, +.product__price .price .new__price { + font-size: 1.6rem; +} + +.remove__cart-item { + margin-left: 1rem; +} + +.remove__cart-item:hover svg { + fill: var(--orange); +} + +.cart-btns { + display: flex; + justify-content: space-between; + border-bottom: 1px solid var(--primaryColor); + padding-bottom: 4rem; + margin: 3rem 0; +} + +.continue__shopping a:link, +.continue__shopping a:visited { + font-size: 1.5rem; + padding: 1.2rem 3rem; + color: var(--black); + text-transform: uppercase; + border: 1px solid var(--black); + transition: all 0.4s ease-in-out; +} + +.continue__shopping a:hover { + background-color: var(--black); + color: var(--white); + border: 1px solid var(--black); +} + +.cart__totals { + width: 60rem; + /* height: 30rem; */ + margin: 5rem auto 0 auto; + color: var(--black5); + padding: 4rem 5rem; + background-color: rgba(255, 255, 255, 0.8); + box-shadow: 0px 2px 30px 10px rgba(0, 0, 0, 0.09); + border-radius: 0.5rem; +} + +.cart__totals h3 { + font-weight: 500; + font-size: 1.8rem; + margin-bottom: 1.6rem; +} + +.cart__totals .new__price { + font-size: 1.5rem; +} + +.cart__totals ul { + margin-bottom: 2.5rem; +} + +.cart__totals li { + border: 1px solid var(--primaryColor); + padding: 1.4rem 0.5rem; + position: relative; +} + +.cart__totals li:not(:last-child) { + border-bottom-color: transparent; +} + +.cart__totals li span { + position: absolute; + right: 1rem; +} + +.cart__totals a:link, +.cart__totals a:visited { + font-size: 1.5rem; + padding: 1.2rem 3rem; + color: var(--black); + text-transform: uppercase; + border: 1px solid var(--black); + transition: all 0.4s ease-in-out; +} + +.cart__totals a:hover { + background-color: var(--black); + color: var(--white); + border: 1px solid var(--black); +} + +.auth .login-form h3{ + font-size: 2.5rem; + text-transform: uppercase; + color: var(--black); +} + +.auth .login-form .box{ + width: 100%; + margin: .7rem 0; + background-color: #eee; + border-radius: .5rem; + padding: 1rem; + font-size: 1.6rem; + color: var(--black); + text-transform: none; +} +.auth .login-form p{ + font-size: 1.4rem; + padding: .5rem 0; + color: var(--light-color); +} +.auth .login-form p a{ + color: var(--orange); + text-decoration: underline; +} + +/* kasunika dev edit start */ +.searchF .search-form{ + top: 14%; +} + +/* Auth styles */ +.auth-container { + max-width: 400px; + margin: 5rem auto; + padding: 2rem; + background-color: #fff; + border-radius: 8px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.1); +} + +.auth-tabs { + display: flex; + margin-bottom: 2rem; + border-bottom: 1px solid #ddd; +} + +.auth-tab { + flex: 1; + text-align: center; + padding: 1rem; + cursor: pointer; + border-bottom: 3px solid transparent; +} + +.auth-tab.active { + border-bottom: 3px solid #eb0028; + font-weight: bold; +} + +.auth-form { + display: none; +} + +.auth-form.active { + display: block; +} + +.btn-auth { + width: 100%; + padding: 1rem; + background-color: #eb0028; + color: white; + border: none; + border-radius: 4px; + font-size: 1.6rem; + cursor: pointer; + transition: background-color 0.3s; +} + +.btn-auth:hover { + background-color: #c10020; +} + +.auth-links { + margin-top: 1.5rem; + text-align: center; +} + +.auth-links a { + color: #eb0028; + text-decoration: none; + display: block; + margin-bottom: 0.5rem; +} + +.error-message { + color: #eb0028; + font-size: 1.2rem; + margin-top: 0.5rem; + display: none; +} + +.auth-error { + color: #eb0028; + text-align: center; + margin-bottom: 1.5rem; + padding: 1rem; + background-color: #ffe6e6; + border-radius: 4px; + display: none; +} + +.success-message { + color: #59b210; + text-align: center; + margin-bottom: 1.5rem; + padding: 1rem; + background-color: #f0ffe6; + border-radius: 4px; + display: none; +} +/* Navbar Auth Button (Login/Logout) */ +.nav__auth { + display: flex; + align-items: center; + gap: 1rem; + margin-left: auto; +} + +.nav__auth .btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.8rem 1.5rem; + background-color: #f8f8f8; + border-radius: 4px; + text-decoration: none; + color: #333; + font-weight: 500; + transition: all 0.3s ease; +} + +.nav__auth .btn:hover { + background-color: #eb0028; + color: white; +} + +.nav__auth svg { + width: 16px; + height: 16px; + fill: currentColor; +} + +/* ====================== + User Authentication Overrides + ====================== */ + +.user-authenticated { +background-color: #eb0028 !important; /* Red background */ +border-color: #eb0028 !important; /* Red border */ +} + +.user-authenticated .icon__user { + fill: white !important; /* White icon on red background */ +} + +.user-authenticated:hover { + background-color: #c10020 !important; /* Darker red on hover */ + border-color: #c10020 !important; +} + +.user-authenticated:hover .icon__user { + fill: white !important; /* Keep white on hover */ +} + +.logout-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 0 1.5rem; + background-color: #eb0028; + color: white !important; + border: none; + border-radius: 6px; /* Small rounded corners, NOT circle */ + font-size: 1.4rem; + font-weight: 500; + height: 38px; + min-width: 80px; + margin-left: 1rem; + box-sizing: border-box; + cursor: pointer; + transition: background-color 0.3s; + text-decoration: none !important; +} +.logout-btn:hover { + background-color: #c10020; + color: white !important; + text-decoration: none !important; +} +/* Remove circular styles specifically from logout button */ +.logout-btn, +.logout-btn:hover { + border-radius: 6px !important; /* Force square corners */ +} +/* Ensure logout button doesn't inherit icon item styles */ +.nav__icons .logout-btn { + border-radius: 6px !important; + border: none !important; + padding: 0.8rem 1.5rem !important; +} +/* Fix cart total badge positioning */ +.nav__icons #cart__total { + font-size: 1rem; + position: absolute; + top: -5px; /* Adjust this value */ + right: -5px; /* Adjust this value */ + background-color: var(--orange); + padding: 0.2rem 0.4rem; + border-radius: 50%; + color: var(--primaryColor); + z-index: 10; /* Ensure it's above other elements */ +} + +/* Make sure cart icon has relative positioning */ +.nav__icons .icon__item[href="/cart/"] { + position: relative; +} + +/* Prevent any circular inheritance */ +.nav__icons .logout-btn { + border-radius: 6px !important; +} + +/* Remove any icon-item specific styles from logout button */ +.nav__icons .logout-btn:not(.icon__item) { + border-radius: 6px; + border: none; + padding: 0.8rem 1.5rem; +} + +/* Ensure cart total badge doesn't overlap logout button */ +.nav__icons { + position: relative; + display: flex; + align-items: center; + gap: 1rem; +} + +.nav__icons > * { + position: relative; + z-index: 1; +} + +/* Username label in nav */ +.nav__icons .user-name { + font-size: 1.4rem; + color: var(--black); + margin-right: 0.5rem; + white-space: nowrap; +} + +/* Override existing login form styles */ + +.auth .login-form { + display: none !important; /* Ensure it's hidden */ +} + +/*kasunika dev end */ + +.searchF .search-form{ + position: absolute; + top: 9%; + right: -110%; + max-width: 50rem; + height: 5rem; + background-color: #fff; + border-radius: .5rem; + overflow: hidden; + display: flex; + box-shadow: var(--box-shadow); + z-index: 5; +} + +.searchF .search-form.active{ + right: 2rem; + transition: .4s linear; +} +.searchF .search-form input{ + height: 100%; + width: 100%; + background-color: none; + text-transform: none; + font-size: 1.6rem; + color: var(--black); + padding: 0 1.5rem; +} + +.searchF .search-form label{ + font-size: 2.2rem; + padding-right: 1.5rem; + padding-top: 1.4rem; + color: var(--black); + cursor: pointer; +} +.searchF .search-form label:hover{ + color: var(--orange); +} + +.shopping-cart{ + position: absolute; + top: 9%; right: -110%; + padding: 1rem; + border-radius: .5rem; + box-shadow: var(--box-shadow); + width: 30rem; + background-color: #fff; + z-index: 5; +} +.shopping-cart.active{ + right: 6rem; + transition: .4s linear; +} +.shopping-cart .box{ + display: flex; + align-items: center; + gap: 1rem; + position: relative; + margin: 1rem 0; +} + +.shopping-cart .box img{ + height: 10rem; + width: 10rem; + object-fit: contain; +} + +.shopping-cart .box .fa-trash{ + font-size: 2rem; + position: absolute; + top: 50%; right: 2rem; + cursor: pointer; + color: var(--light-color); + transform: translateY(-50%); +} +.shopping-cart .box .fa-trash:hover{ + color: var(--orange); +} + +.shopping-cart .box .content h3{ + color: var(--black); + font-size: 1.7rem; + padding-bottom: 1rem; +} +.shopping-cart .box .content span{ + color: var(--light-color); + font-size: 1.6rem; +} + +.shopping-cart .box .content .quantity{ + padding-left: 1rem; +} + +.shopping-cart .total{ + font-size: 2.5rem; + padding: 1rem 0; + text-align: center; + color:var(--black); +} +.shopping-cart .btn{ + display: block; + text-align: center; + margin: 1rem; +} + +/* +====================== +Cart Area Media Query +====================== +*/ + +@media only screen and (max-width: 1200px) { + .minus-btn, + .plus-btn { + padding: 0.6rem 0.8rem; + margin-right: 0; + } + + .counter-btn { + width: 4rem; + padding: 1rem 0; + } + + .input-counter svg { + width: 1.5rem; + height: 1.5rem; + } +} + +/* Chatbot styles */ +#chatbot-container { + position: fixed; + bottom: 60px; + right: 60px; + z-index: 99999; + font-family: "Archivo", sans-serif; +} + +#chatbot-toggle { + background: var(--white); + border: 6px solid var(--orange); + cursor: pointer; + outline: none; + box-shadow: 0 4px 10px rgba(0,0,0,0.2); + border-radius: 50%; + padding: 12px; + width: 70px; + height: 70px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +#chatbot-toggle:hover { + transform: translateY(-3px); + box-shadow: 0 5px 15px rgba(0,0,0,0.3); + background: #872010; +} + +#chatbot-toggle svg { + width: 24px; + height: 24px; + fill: var(--white); + transition: transform 0.3s ease; +} + +#chatbot-toggle:hover svg { + transform: scale(1.1); +} + +#chatbot-window { + width: 350px; + height: 450px; + background: var(--white); + border-radius: 15px; + box-shadow: 0 5px 25px rgba(0,0,0,0.2); + display: flex; + flex-direction: column; + position: absolute; + bottom: 75px; + right: 0; + overflow: hidden; + transition: all 0.3s ease; + transform-origin: bottom right; + opacity: 1; + border: none; +} + +#chatbot-window.chatbot-hide { + opacity: 0; + transform: scale(0.5); + visibility: hidden; + pointer-events: none; +} + +#chatbot-header { + background: linear-gradient(135deg, var(--orange) 0%, #ff682d 100%); + color: var(--white); + padding: 15px 20px; + font-weight: 600; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 1.6rem; + letter-spacing: 0.5px; + border-bottom: 1px solid rgba(255,255,255,0.1); +} + +#chatbot-header button { + background: rgba(255,255,255,0.2); + border: none; + color: var(--white); + font-size: 20px; + cursor: pointer; + padding: 3px 8px; + line-height: 1; + border-radius: 50%; + transition: background 0.6s; +} + +#chatbot-header button:hover { + background: rgba(255,255,255,0.3); +} + +#chatbot-messages { + flex: 1; + padding: 16px 12px; + overflow-y: auto; + background: #f9f9f9; + font-size: 1.4rem; + scrollbar-width: thin; + scrollbar-color: var(--orange) #f0f0f0; + background-image: linear-gradient(rgba(235,235,235,0.2) 1px, transparent 1px); + background-size: 100% 20px; +} + +#chatbot-messages::-webkit-scrollbar { + width: 6px; +} + +#chatbot-messages::-webkit-scrollbar-thumb { + background: var(--orange); + border-radius: 10px; +} + +#chatbot-messages::-webkit-scrollbar-track { + background: #e93f3f; + border-radius: 10px; +} + +.chatbot-message { + margin-bottom: 15px; + line-height: 1.5; + max-width: 85%; + word-break: break-word; + padding: 10px 15px; + border-radius: 18px; + font-size: 1.4rem; + position: relative; + clear: both; + animation: fadeIn 0.6s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.chatbot-message.user { + background: linear-gradient(135deg, var(--orange) 0%, #ff2d4a 100%); + color: var(--white); + float: right; + border-bottom-right-radius: 5px; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +.chatbot-message.bot { + background: var(--white); + color: var(--black); + float: left; + border-bottom-left-radius: 5px; + box-shadow: 0 2px 5px rgba(0,0,0,0.05); + border: 1px solid #eee; +} + +#chatbot-form { + display: flex; + border-top: 1px solid #eee; + background: var(--white); + padding: 8px; +} + +#chatbot-input { + flex: 1; + border: 1px solid #e0e0e0; + padding: 12px 15px; + font-size: 1.4rem; + outline: none; + background: #f9f9f9; + border-radius: 25px; + transition: border 0.3s ease, box-shadow 0.3s ease; + margin-right: 8px; +} + +#chatbot-input:focus { + border-color: var(--orange); + box-shadow: 0 0 0 2px rgba(235, 0, 40, 0.1); +} + +#chatbot-form button { + background: var(--orange); + color: var(--white); + border: none; + padding: 0 20px; + font-size: 1.4rem; + cursor: pointer; + border-radius: 25px; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +#chatbot-form button:hover { + background: #d41200f0; + transform: translateY(-1px); +} + +#chatbot-form button:active { + transform: translateY(0); +} + +@media only screen and (max-width: 568px) { + #chatbot-window { + width: 90vw; + height: 70vh; + right: 5vw; + bottom: 75px; + border-radius: 12px; + } + + #chatbot-toggle { + width: 50px; + height: 50px; + padding: 20px; + } + + #chatbot-toggle svg { + width: 24px; + height: 24px; + } + + #chatbot-header { + padding: 12px 15px; + font-size: 1.5rem; + } + + .chatbot-message { + max-width: 90%; + font-size: 1.3rem; + } +} + +@media only screen and (max-width: 400px) { + #chatbot-toggle { + width: 60px; + height: 60px; + padding: 8px; + } +} + +/* ====================== + Dark Mode Variables and Styles + ====================== */ + +body.darkMode { + --dark-bg: #1a1a1a; + --dark-card-bg: #2d2d2d; + --dark-text: #ffffff; + --dark-text-muted: #b3b3b3; + --dark-border: #404040; + --dark-input-bg: #3a3a3a; + --dark-hover-bg: #404040; + --dark-shadow: rgba(255, 255, 255, 0.1); + + /* Override existing CSS custom properties for dark mode */ + --white: #1a1a1a !important; + --black: #ffffff !important; + --primaryColor: #2d2d2d !important; + --black2: #b3b3b3 !important; + --black3: #2d2d2d !important; + --black4: #ffffff !important; + --black5: #2d2d2d !important; + + background-color: var(--dark-bg) !important; + color: var(--dark-text) !important; + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* Immediate dark mode application */ +body.darkMode, +body.darkMode * { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease !important; +} + +/* Theme Switch Button */ +#theme-switch { + background: none; + border: 1px solid var(--black); + cursor: pointer; + padding: 0.7rem; + border-radius: 50%; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + color: var(--black); +} + +#theme-switch:hover { + background-color: var(--orange); + border-color: var(--black); +} + +.theme-icon { + font-size: 1.6rem; + transition: transform 0.3s ease; + display: inline-block; + line-height: 1; +} + +/* ====================== + CRITICAL DARK MODE OVERRIDES + ====================== */ + +/* High specificity dark mode rules */ +html body.darkMode, +html body.darkMode .header, +html body.darkMode .navigation { + background-color: #1a1a1a !important; + color: #ffffff !important; +} + +html body.darkMode .nav__logo a, +html body.darkMode .nav__link, +html body.darkMode .nav__list .nav__link:link, +html body.darkMode .nav__list .nav__link:visited { + color: #ffffff !important; +} + +html body.darkMode .nav__icons .icon__item { + border-color: #ffffff !important; + color: #ffffff !important; +} + +html body.darkMode .nav__icons .icon__item svg { + fill: #ffffff !important; +} + +/* ====================== + Navigation Dark Mode + ====================== */ + +/* Force dark header background */ +body.darkMode .header, +body.darkMode header, +body.darkMode #header { + background-color: #1a1a1a !important; + color: #ffffff !important; +} + +/* Force dark navigation background */ +body.darkMode .navigation, +body.darkMode nav, +body.darkMode .nav { + background-color: #1a1a1a !important; + border-bottom: 1px solid #404040 !important; +} + +/* Force dark fixed navigation */ +body.darkMode .fix__nav { + background-color: #1a1a1a !important; +} + +/* Force dark logo text */ +body.darkMode .nav__logo a, +body.darkMode .nav__logo { + color: #ffffff !important; +} + +/* Force dark hamburger menu */ +body.darkMode .nav__hamburger svg { + fill: #ffffff !important; +} + +/* Force dark navigation links */ +body.darkMode .nav__link, +body.darkMode .nav__list .nav__link:link, +body.darkMode .nav__list .nav__link:visited { + color: #ffffff !important; +} + +body.darkMode .nav__link:hover, +body.darkMode .nav__list .nav__link:hover { + color: #eb0028 !important; +} + +/* Force dark navigation icons */ +body.darkMode .nav__icons .icon__item { + border-color: #ffffff !important; + color: #ffffff !important; +} + +body.darkMode .nav__icons .icon__item svg { + fill: #ffffff !important; +} + +body.darkMode .nav__icons .icon__item:hover { + background-color: #eb0028 !important; + border-color: #eb0028 !important; +} + +body.darkMode .nav__icons .icon__item:hover svg { + fill: #ffffff !important; +} + +/* Force dark mobile menu */ +body.darkMode .nav__menu { + background-color: #1a1a1a !important; +} + +body.darkMode .menu__top { + background-color: #eb0028 !important; +} + +body.darkMode .nav__category { + color: #ffffff !important; +} + +/* Force cart badge styling */ +body.darkMode .nav__icons #cart__total { + background-color: #eb0028 !important; + color: #ffffff !important; +} + +/* Force theme toggle styling */ +body.darkMode #theme-switch { + border-color: #ffffff !important; + color: #ffffff !important; +} + +body.darkMode #theme-switch:hover { + background-color: #eb0028 !important; + border-color: #eb0028 !important; +} + +body.darkMode #theme-switch .theme-icon { + color: #ffffff !important; +} + +/* ====================== + Hero Section Dark Mode + ====================== */ + +body.darkMode .hero { + background-color: var(--dark-bg); +} + +body.darkMode .hero__left h1 { + color: var(--dark-text); +} + +body.darkMode .hero__left p { + color: var(--dark-text-muted); +} + +body.darkMode .hero__left span { + color: var(--dark-text-muted); +} + +body.darkMode .hero__btn { + background-color: var(--orange); + color: var(--white); + border: 2px solid var(--orange); +} + +body.darkMode .hero__btn:hover { + background-color: transparent; + color: var(--orange); + border-color: var(--orange); +} + +body.darkMode .hero .glide__bullet { + background-color: var(--dark-text-muted); +} + +body.darkMode .hero .glide__bullet.glide__bullet--active { + background-color: var(--orange); +} + +body.darkMode .hero .glide__arrow { + background-color: var(--dark-card-bg); + border: 1px solid var(--dark-border); +} + +body.darkMode .hero .glide__arrow svg { + fill: var(--dark-text); +} + +body.darkMode .hero .glide__arrow:hover { + background-color: var(--orange); +} + +body.darkMode .hero .glide__arrow:hover svg { + fill: var(--white); +} + +/* ====================== + Collection Boxes Dark Mode + ====================== */ + +body.darkMode .collection__box { + background-color: var(--dark-card-bg); + box-shadow: 0 5px 15px var(--dark-shadow); +} + +body.darkMode .collection__content { + background-color: var(--dark-card-bg); +} + +body.darkMode .collection__data h1 { + color: var(--dark-text); +} + +body.darkMode .collection__data span { + color: var(--dark-text-muted); +} + +body.darkMode .collection__data a { + color: var(--orange); + border-bottom: 2px solid var(--orange); +} + +body.darkMode .collection__data a:hover { + color: var(--dark-text); + background-color: var(--orange); +} + +/* ====================== + Product Cards Dark Mode + ====================== */ + +body.darkMode .product { + background-color: var(--dark-card-bg); + box-shadow: 0 5px 15px var(--dark-shadow); + border: 1px solid var(--dark-border); +} + +body.darkMode .product:hover { + box-shadow: 0 10px 25px var(--dark-shadow); +} + +body.darkMode .product__footer h3 { + color: var(--dark-text); +} + +body.darkMode .product__footer .product__price h4 { + color: var(--orange); +} + +body.darkMode .product__btn { + background-color: var(--orange); + color: var(--white); + border: 2px solid var(--orange); +} + +body.darkMode .product__btn:hover { + background-color: transparent; + color: var(--orange); + border-color: var(--orange); +} + +body.darkMode .product ul li a { + background-color: var(--dark-hover-bg); + border: 1px solid var(--dark-border); +} + +body.darkMode .product ul li a svg { + fill: var(--dark-text); +} + +body.darkMode .product ul li a:hover { + background-color: var(--orange); +} + +body.darkMode .product ul li a:hover svg { + fill: var(--white); +} + +/* ====================== + Section Titles Dark Mode + ====================== */ + +body.darkMode .section__title h1 { + color: var(--dark-text); +} + +body.darkMode .section__title.active .dot { + background-color: var(--orange); +} + +body.darkMode .section__title .dot { + background-color: var(--dark-text-muted); +} + +body.darkMode .primary__title { + color: var(--dark-text); +} + +/* ====================== + Facility Section Dark Mode + ====================== */ + +body.darkMode .facility__box { + background-color: var(--dark-card-bg); + border: 1px solid var(--dark-border); +} + +body.darkMode .facility__box p { + color: var(--dark-text); +} + +body.darkMode .facility-img__container svg { + fill: var(--orange); +} + +/* ====================== + Testimonial Dark Mode + ====================== */ + +body.darkMode .testimonial { + background-color: var(--dark-bg); +} + +body.darkMode .testimonial__box { + background-color: var(--dark-card-bg); + box-shadow: 0 5px 15px var(--dark-shadow); +} + +body.darkMode .testimonial__box p { + color: var(--dark-text-muted); +} + +body.darkMode .client__info h3 { + color: var(--dark-text); +} + +body.darkMode .client__info span { + color: var(--dark-text-muted); +} + +/* ====================== + News Section Dark Mode + ====================== */ + +body.darkMode .new__card { + background-color: var(--dark-card-bg); + box-shadow: 0 5px 15px var(--dark-shadow); + border: 1px solid var(--dark-border); +} + +body.darkMode .card__footer h3 { + color: var(--dark-text); +} + +body.darkMode .card__footer span { + color: var(--dark-text-muted); +} + +body.darkMode .card__footer p { + color: var(--dark-text-muted); +} + +body.darkMode .card__footer button { + background-color: var(--orange); + color: var(--white); + border: 2px solid var(--orange); +} + +body.darkMode .card__footer button:hover { + background-color: transparent; + color: var(--orange); + border-color: var(--orange); +} + +/* ====================== + Newsletter Dark Mode + ====================== */ + +body.darkMode .newsletter { + background-color: var(--dark-card-bg); +} + +body.darkMode .newsletter__data h3 { + color: var(--dark-text); +} + +body.darkMode .newsletter__data p { + color: var(--dark-text-muted); +} + +body.darkMode .newsletter__email { + background-color: var(--dark-input-bg); + border: 1px solid var(--dark-border); + color: var(--dark-text); +} + +body.darkMode .newsletter__email:focus { + border-color: var(--orange); +} + +body.darkMode .newsletter__link { + background-color: var(--orange); + color: var(--white); +} + +body.darkMode .newsletter__link:hover { + background-color: var(--dark-text); + color: var(--orange); +} + +/* ====================== + Footer Dark Mode + ====================== */ + +body.darkMode .footer { + background-color: var(--dark-bg); + border-top: 1px solid var(--dark-border); +} + +body.darkMode .footer-top__box h3 { + color: var(--dark-text); +} + +body.darkMode .footer-top__box a { + color: var(--dark-text-muted); +} + +body.darkMode .footer-top__box a:hover { + color: var(--orange); +} + +body.darkMode .footer-top__box div { + color: var(--dark-text-muted); +} + +body.darkMode .footer-top__box svg { + fill: var(--orange); +} + +/* ====================== + Popup Dark Mode + ====================== */ + +/* Dark mode popup styles */ +body.darkMode .popup__content { + background-color: var(--black3) !important; + color: var(--white) !important; + border: 1px solid var(--grey2); +} + +body.darkMode .popup__content h1 { + color: var(--white) !important; +} + +body.darkMode .popup__content h1 span { + color: var(--orange) !important; +} + +body.darkMode .popup__content p { + color: var(--grey) !important; +} + +body.darkMode .popup__form { + background-color: var(--black2) !important; + color: var(--white) !important; + border-color: var(--grey2) !important; +} + +body.darkMode .popup__form::placeholder { + color: var(--grey) !important; +} + +body.darkMode .popup__form:focus { + border-color: var(--orange) !important; + box-shadow: 0 0 0 3px rgba(235, 0, 40, 0.2) !important; +} + +body.darkMode .popup__close { + background: rgba(255, 255, 255, 0.1) !important; +} + +body.darkMode .popup__close:hover { + background: rgba(255, 255, 255, 0.2) !important; +} + +body.darkMode .popup__close svg { + fill: var(--white) !important; +} + +body.darkMode .popup__form { + background-color: var(--dark-input-bg); + border: 1px solid var(--dark-border); + color: var(--dark-text); +} + +body.darkMode .popup__form:focus { + border-color: var(--orange); +} + +body.darkMode .popup__right a { + background-color: var(--orange); + color: var(--white); +} + +body.darkMode .popup__close svg { + fill: var(--dark-text); +} + +/* ====================== + Shopping Cart Dark Mode + ====================== */ + +body.darkMode .shopping-cart { + background-color: var(--dark-card-bg); + border: 1px solid var(--dark-border); + box-shadow: 0 5px 15px var(--dark-shadow); +} + +body.darkMode .shopping-cart .box { + border-bottom: 1px solid var(--dark-border); +} + +body.darkMode .shopping-cart .content h3 { + color: var(--dark-text); +} + +body.darkMode .shopping-cart .content .price { + color: var(--orange); +} + +body.darkMode .shopping-cart .content .quantity { + color: var(--dark-text-muted); +} + +body.darkMode .shopping-cart .total { + color: var(--dark-text); + border-top: 1px solid var(--dark-border); +} + +body.darkMode .shopping-cart .btn { + background-color: var(--orange); + color: var(--white); +} + +/* ====================== + Search & Filter Dark Mode + ====================== */ + +body.darkMode .search-wrapper-custom input { + background-color: var(--dark-input-bg); + border: 1px solid var(--dark-border); + color: var(--dark-text); +} + +body.darkMode .search-wrapper-custom input:focus { + border-color: var(--orange); +} + +body.darkMode .filter-dropdown-custom { + background-color: var(--dark-card-bg); + border: 1px solid var(--dark-border); + box-shadow: 0 5px 15px var(--dark-shadow); +} + +body.darkMode .filter-dropdown-custom h3 { + color: var(--dark-text); +} + +body.darkMode .filter-dropdown-custom label { + color: var(--dark-text); +} + +body.darkMode .filter-dropdown-custom select, +body.darkMode .filter-dropdown-custom input { + background-color: var(--dark-input-bg); + border: 1px solid var(--dark-border); + color: var(--dark-text); +} + +body.darkMode .clear-btn { + background-color: var(--orange); + color: var(--white); +} + +/* ====================== + Chatbot Dark Mode + ====================== */ + +body.darkMode #chatbot-window { + background: var(--dark-card-bg); + border: 1px solid var(--dark-border); +} + +body.darkMode #chatbot-header { + background: var(--dark-bg); + color: var(--dark-text); + border-bottom: 1px solid var(--dark-border); +} + +body.darkMode #chatbot-messages { + background: var(--dark-card-bg); +} + +body.darkMode .chatbot-message.user { + background: var(--orange); +} + +body.darkMode .chatbot-message.bot { + background: var(--dark-input-bg); + color: var(--dark-text); +} + +body.darkMode #chatbot-form { + background: var(--dark-card-bg); + border-top: 1px solid var(--dark-border); +} + +body.darkMode #chatbot-input { + background: var(--dark-input-bg); + color: var(--dark-text); + border: 1px solid var(--dark-border); +} + +body.darkMode #chatbot-input:focus { + border-color: var(--orange); + box-shadow: 0 0 0 2px rgba(235, 0, 40, 0.2); +} + +body.darkMode #chatbot-toggle { + background: var(--dark-card-bg); + border: 2px solid #eb0028 !important; +} + +body.darkMode #chatbot-toggle:hover { + background: var(--orange); + border: 2px solid #eb0028 !important; +} + +/* ====================== + Page Title Area Dark Mode + ====================== */ + +body.darkMode .page__title-area { + background-color: var(--dark-card-bg); + border-bottom: 1px solid var(--dark-border); +} + +body.darkMode .page__titles { + color: var(--dark-text-muted); +} + +body.darkMode .page__titles a { + color: var(--dark-text-muted); +} + +body.darkMode .page__titles a:hover { + color: var(--orange); +} + +body.darkMode .page__titles svg { + fill: var(--dark-text-muted); +} + +/* ====================== + Glide Controls Dark Mode + ====================== */ + +body.darkMode .glide__bullet { + background-color: var(--dark-text-muted); +} + +body.darkMode .glide__bullet--active { + background-color: var(--orange); +} + +body.darkMode .glide__arrow { + background-color: var(--dark-card-bg); + border: 1px solid var(--dark-border); +} + +body.darkMode .glide__arrow svg { + fill: var(--dark-text); +} + +body.darkMode .glide__arrow:hover { + background-color: var(--orange); +} + +body.darkMode .glide__arrow:hover svg { + fill: var(--white); +} + +/* ====================== + Modern Chatbot UI Styles + ====================== */ + +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap'); + +.chatbot-container { + font-family: 'Poppins', sans-serif; + width: 380px; + background-color: #fff; + border-radius: 20px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + overflow: hidden; + display: flex; + flex-direction: column; + position: fixed; + bottom: 20px; + right: 20px; + z-index: 10000; +} + +.chat-header { + background: linear-gradient(to right, #ff4757, #ff3838); + color: #fff; + padding: 15px 20px; + display: flex; + justify-content: space-between; + align-items: center; + border-top-left-radius: 20px; + border-top-right-radius: 20px; +} + +.avatar-info { + display: flex; + align-items: center; +} + +.avatar { + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; + margin-right: 10px; + border: 2px solid #fff; +} + +.avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.chat-details { + display: flex; + flex-direction: column; +} + +.chat-with { + font-size: 12px; + opacity: 0.8; +} + +.bot-name { + font-weight: 600; + font-size: 15px; +} + +.header-icons i { + margin-left: 15px; + font-size: 18px; + cursor: pointer; +} + +.response-time { + background-color: #ffeaea; + color: #ff4757; + font-size: 12px; + padding: 8px 20px; + text-align: center; + border-bottom: 1px solid #e0e0e0; +} + +.chat-body { + flex-grow: 1; + padding: 20px; + overflow-y: auto; + background-color: #fff; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; + min-height: 300px; + max-height: 400px; +} + +.message { + max-width: 75%; + padding: 10px 15px; + border-radius: 18px; + margin-bottom: 10px; + line-height: 1.4; + font-size: 14px; +} + +.message.received { + background-color: #f0f0f0; + align-self: flex-start; + border-bottom-left-radius: 5px; + position: relative; + padding-left: 45px; +} + +.message.received::before { + content: ''; + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + width: 24px; + height: 24px; + background-image: url('/static/images/bot.png'); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + border-radius: 50%; + border: 2px solid #fff; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} + +/* Loading animation for typing indicator with wave effect */ +.message.received.loading-animation { + opacity: 0.9; + font-style: italic; +} + +.wave-text { + display: inline-block; +} + +.wave-letter, .wave-dot { + display: inline-block; + animation: wave 1.5s ease-in-out infinite; +} + +.wave-letter:nth-child(1) { animation-delay: 0s; } +.wave-letter:nth-child(2) { animation-delay: 0.1s; } +.wave-letter:nth-child(3) { animation-delay: 0.2s; } +.wave-letter:nth-child(4) { animation-delay: 0.3s; } +.wave-letter:nth-child(5) { animation-delay: 0.4s; } +.wave-letter:nth-child(6) { animation-delay: 0.5s; } + +.wave-dots .wave-dot:nth-child(1) { animation-delay: 0.6s; } +.wave-dots .wave-dot:nth-child(2) { animation-delay: 0.7s; } +.wave-dots .wave-dot:nth-child(3) { animation-delay: 0.8s; } + +@keyframes wave { + 0%, 60%, 100% { + transform: translateY(0); + opacity: 1; + } + 30% { + transform: translateY(-8px); + opacity: 0.7; + } +} + +.wave-dots { + display: inline-block; + margin-left: 2px; +} + +.message.sent { + background-color: #ff4757; + color: #fff; + align-self: flex-end; + margin-left: auto; + border-bottom-right-radius: 5px; +} + +.suggestions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 5px; + margin-bottom: 15px; +} + +.suggestion-button { + background-color: #e0e0e0; + color: #333; + padding: 8px 15px; + border-radius: 20px; + font-size: 13px; + cursor: pointer; + transition: background-color 0.2s ease; + border: none; +} + +.suggestion-button.active { + background-color: #ff4757; + color: #fff; +} + +.suggestion-button:hover { + background-color: #ff4757; + color: #fff; +} + +.chat-footer { + display: flex; + align-items: center; + padding: 10px 15px; + border-top: 1px solid #e0e0e0; + background-color: #fff; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; +} + +.input-area { + flex-grow: 1; + display: flex; + align-items: center; + background-color: #f5f5f5; + border-radius: 25px; + padding: 8px 15px; + margin-right: 10px; +} + +.input-area input { + flex-grow: 1; + border: none; + outline: none; + background: none; + font-size: 14px; + padding: 0 5px; +} + +.input-area i { + color: #ff4757; + margin-left: 10px; + cursor: pointer; + font-size: 16px; +} + +.send-button { + background: linear-gradient(to right, #ff4757, #ff3838); + border: none; + border-radius: 50%; + width: 45px; + height: 45px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); +} + +.send-button i { + color: #fff; + font-size: 20px; +} + +.powered-by { + font-size: 10px; + text-align: center; + padding: 8px; + color: #999; + background-color: #fff; + border-top: 1px solid #eee; + display: flex; + justify-content: center; + align-items: center; + gap: 5px; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; +} + +.powered-by img { + height: 15px; + vertical-align: middle; +} + +/* Responsive design for mobile */ +@media (max-width: 768px) { + .chatbot-container { + width: 95%; + right: 2.5%; + bottom: 10px; + } +} + +@media (max-width: 480px) { + .chatbot-container { + width: 98%; + right: 1%; + bottom: 5px; + } + + .chat-body { + min-height: 250px; + max-height: 300px; + } + +/* Chatbot toggle button styles */ +.chatbot-toggle-btn, +.chatbot-toggle-btn:focus { + position: fixed !important; + bottom: 80px !important; + right: 60px !important; + width: 60px !important; + height: 80px !important; + border-radius: 50% !important; + background: linear-gradient(to right, #ff4757, #ff3838) !important; + color: white !important; + border: none !important; + font-size: 24px !important; + cursor: pointer !important; + z-index: 99999 !important; + box-shadow: 1 5px 15px rgba(0, 0, 0, 0.2) !important; + + /* Animation properties */ + animation: chatbotPulse 2s infinite, chatbotFloat 3s ease-in-out infinite !important; + transition: all 0.3s ease !important; +} + +.chatbot-toggle-btn:hover { + transform: scale(1.1) translateY(-2px) !important; + box-shadow: 0 8px 25px rgba(255, 71, 87, 0.4) !important; + animation: chatbotBounce 0.6s ease-in-out !important; +} + +.chatbot-toggle-btn:active { + transform: scale(0.95) !important; + animation: chatbotPing 0.4s ease-out !important; +} + +.chatbot-toggle-btn:focus { + outline: 3px solid rgba(255,71,87,0.25) !important; + animation: chatbotFocusPulse 1s infinite !important; +} + +.chatbot-toggle-btn img { + width: 30px; + height: 30px; + object-fit: cover; + border-radius: 50%; + display: block; + margin: 0; + border: 10px solid rgba(255,255,255,0.9); + transition: transform 0.3s ease !important; +} + +.chatbot-toggle-btn:hover img { + transform: rotate(10deg) scale(1.1) !important; +} + +/* Chatbot toggle button animations */ +@keyframes chatbotPulse { + 0% { + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2), 0 0 0 0 rgba(255, 71, 87, 0.7); + } + 70% { + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2), 0 0 0 10px rgba(255, 71, 87, 0); + } + 100% { + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2), 0 0 0 0 rgba(255, 71, 87, 0); + } +} + +@keyframes chatbotFloat { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-5px); + } +} + +@keyframes chatbotBounce { + 0%, 20%, 50%, 80%, 100% { + transform: translateY(0) scale(1.1); + } + 40% { + transform: translateY(-8px) scale(1.15); + } + 60% { + transform: translateY(-4px) scale(1.12); + } +} + +@keyframes chatbotPing { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.7); + } + 50% { + transform: scale(1.05); + box-shadow: 0 0 0 15px rgba(255, 71, 87, 0.3); + } + 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(255, 71, 87, 0); + } +} + +@keyframes chatbotFocusPulse { + 0%, 100% { + outline-color: rgba(255, 71, 87, 0.25); + outline-width: 3px; + } + 50% { + outline-color: rgba(255, 71, 87, 0.5); + outline-width: 5px; + } +} + +/* Hidden state for chatbot */ +.chatbot-container.chatbot-hide { + display: none !important; +} + +.chatbot-container { + transition: transform 0.18s ease, opacity 0.18s ease; +} +} diff --git a/styles.css b/styles.css deleted file mode 100644 index bd06d81..0000000 --- a/styles.css +++ /dev/null @@ -1,2614 +0,0 @@ -/* -====================== -Reset -====================== -*/ -:root { - --primaryColor: #f1f1f1; - --black: #222; - --black2: #555; - --black3: #252525; - --black4: #000; - --black5: #212529; - --orange: #eb0028; - --white: #fff; - --grey: #959595; - --grey2: #666; - --grey3: #ccc; - --secondaryColor: #2b1f4d; - --yellow: #ffcc00; - --green: #59b210; - --blue: rgb(56, 10, 223); -} - -* { - margin: 0; - padding: 0; - box-sizing: inherit; -} - -html { - font-size: 62.5%; - box-sizing: border-box; - scroll-behavior: smooth; - overflow-x: hidden; -} - -body{ - overflow-x: hidden; -} - -body, -input { - font-size: 1.6rem; - font-weight: 400; - font-family: "Archivo", sans-serif; - color: var(--black); -} - -a { - text-decoration: none; -} - -ul { - list-style: none; -} - -img { - max-width: 100%; -} - -h3, -h4 { - font-weight: 500; -} - -/* -====================== -Header -====================== -*/ - -.header { - position: relative; -} - -.container { - max-width: 117rem; - margin: 0 auto; - padding: 0 1.6rem; -} - -/* -====================== -Navigation -====================== -*/ - -.navigation { - position: relative; - height: 7rem; - box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.1); -} - -.nav { - display: flex; - align-items: center; - justify-content: space-between; - height: 100%; - height: 7rem; - padding: 0 1rem; -} - -.fix__nav { - position: fixed; - top: 0; - left: 0; - width: 100%; - background-color: var(--white); - z-index: 1200; -} - -.nav__logo a { - font-size: 2.5rem; - color: var(--black); - padding: 1.6rem; - font-weight: 700; -} - -.nav__hamburger { - display: none; - cursor: pointer; -} - -.nav__hamburger svg { - height: 2.3rem; - width: 2.3rem; -} - -.menu__top { - display: none; -} - -.nav__menu { - width: 50%; -} - -.nav__list { - display: flex; - align-items: center; - height: 100%; - width: 50%; -} - -.nav__item:not(:last-child) { - margin-right: 1.6rem; -} - -.nav__list .nav__link:link, -.nav__list .nav__link:visited { - display: inline-block; - font-size: 1.4rem; - text-transform: uppercase; - color: var(--black); - transition: color 0.3s ease-in-out; -} - -.nav__list .nav__link:hover { - color: var(--orange); -} - -.nav__icons { - display: flex; - position: relative; -} - -.nav__icons .icon__item svg { - width: 1.6rem; - height: 1.6rem; -} - -.nav__icons .icon__item { - display: flex; - justify-content: center; - align-items: center; - padding: 0.7rem; - border: 1px solid var(--black); - border-radius: 50%; - transition: background-color 0.3s ease-in-out; -} - -.nav__icons .icon__item:link, -.nav__icons .icon__item:visited { - color: var(--black); -} - -.nav__icons .icon__item:hover { - background-color: var(--orange); - border: 1px solid var(--black); -} - -.nav__icons .icon__item:not(:last-child) { - margin-right: 1rem; -} - -.nav__icons #cart__total { - font-size: 1rem; - position: absolute; - top: 2px; - right: -6px; - background-color: var(--orange); - padding: 0.2rem 0.4rem; - border-radius: 100%; - color: var(--primaryColor); -} - -.page__title-area { - background-color: var(--primaryColor); -} - -.page__title-container { - padding: 1rem; -} - -.page__titles { - display: flex; - align-items: center; - font-size: 1.2rem; - color: var(--grey2); -} - -.page__titles a { - margin-right: 2rem; -} - -.page__titles a svg { - width: 1.8rem; - height: 1.8rem; - fill: var(--grey2); -} - -.page__title { - position: relative; -} - -.page__title::before { - position: absolute; - content: "/"; - top: 0; - left: -1rem; -} - -/* -====================== -Navigation Media Query -====================== -*/ -@media only screen and (max-width: 768px) { - .nav__menu { - position: fixed; - top: 0; - left: -30rem; - width: 0; - background-color: var(--white); - z-index: 9990; - height: 100%; - transition: all 0.5s; - } - - .nav__menu.open { - left: 30rem; - width: 30rem; - } - - .nav__logo { - width: 50%; - } - - .nav__hamburger { - display: block; - } - - .menu__top { - display: flex; - align-items: center; - justify-content: space-between; - background-color: var(--orange); - padding: 1.8rem 1rem; - } - - .menu__top svg { - height: 1.6rem; - width: 1.6rem; - fill: var(--primaryColor); - } - - .nav__category { - color: var(--white); - font-size: 2.3rem; - font-weight: 700; - } - - .nav__list { - flex-direction: column; - align-items: start; - padding: 1.6rem 1rem; - } - - .nav__item:not(:last-child) { - margin-right: 0; - } - - .nav__item { - width: 100%; - } - - .nav__list .nav__link:link, - .nav__list .nav__link:visited { - padding: 1.6rem 0; - width: 100%; - color: var(--grey2); - } - - body.active::before { - content: ""; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 110%; - background: var(--black) none no-repeat 0 0; - opacity: 0.7; - z-index: 999; - transition: 0.8s; - } -} - -@media only screen and (max-width: 568px) { - .nav__icons .icon__item svg { - width: 1.4rem; - height: 1.4rem; - } - - .nav__icons .icon__item { - padding: 0.4rem; - } -} - -/* -====================== -Hero Area -====================== -*/ - -.hero, -.hero .glide__slides { - background-color: var(--primaryColor); - position: relative; - width: 100%; - height: 100vh; -} - -.hero .glide__bullet { - background-color: #222; - width: 1.2rem; - height: 1.2rem; -} - -.hero .glide__arrow { - padding: 1.5rem 1.7rem; - opacity: 0; - border: none; - background-color: var(--grey); - transition: all 0.5s ease-in-out 0.2s; -} - -.glide__arrow:hover { - background-color: var(--black); -} - -.glide__arrow--left { - left: 20rem; -} - -.glide__arrow--right { - position: absolute; - right: 20rem; -} - -.hero:hover .glide__arrow { - opacity: 1; -} - -.hero:hover .glide__arrow--left { - left: 23rem; -} - -.hero:hover .glide__arrow--right { - right: 23rem; -} - -.hero .glide__arrow svg { - height: 1.8rem; - width: 1.8rem; - fill: var(--primaryColor); -} - -.hero .glide__arrow { - border-radius: 50%; -} - -.hero__center { - display: flex; - align-items: center; - justify-content: center; - position: relative; - max-width: 114rem; - margin: 0 auto; - height: 100%; - padding-top: 3rem; -} - -.hero__left { - padding: 0 3rem; - flex: 0 0 50%; -} - -.hero__btn { - display: inline-block; - font-weight: 700; - font-size: 1.4rem; - background-color: var(--black); - color: var(--primaryColor); - cursor: pointer; - margin-top: 1rem; - padding: 1.5rem 4rem; - border: 1px solid var(--black); - transition: all 0.3s ease-in-out; -} - -.hero__btn:focus { - outline: none; -} - -.hero__left .hero__btn:hover { - font-weight: 700; - background-color: transparent; - color: var(--black); -} - -.hero__left h1 { - margin: 1rem 0; -} - -.hero__left p { - margin-bottom: 1rem; -} - -.hero__right { - flex: 0 0 50%; - position: relative; - text-align: center; -} - -.hero__right img.banner_03 { - width: 70%; -} - -/* -====================== -Hero Media Query -====================== -*/ -@media only screen and (max-width: 999px) { - .hero__center { - flex-direction: column; - text-align: center; - } - - .hero__right { - top: 50%; - position: absolute; - } - - .hero__left { - position: absolute; - padding: 0 1rem; - top: 20%; - } - - .hero__right img { - width: 55%; - } - - .hero__btn { - padding: 1.1rem 2.8rem; - } - - .hero .glide__arrows { - display: none; - } -} - -@media only screen and (max-width: 567px) { - .hero, - .hero .glide__slides { - height: 60vh; - } - - .hero__right { - display: none; - } -} - -/* -====================== -Collection -====================== -*/ - -.section { - padding: 3rem 0; -} - -.collection { - margin: 3rem 0; -} - -.collection__container { - width: 100%; - padding: 0 1.6rem; - display: flex; - height: 100%; - align-items: center; - justify-content: space-between; -} - -.collection__box { - display: flex; - justify-content: space-around; - align-items: center; - padding: 1rem; - flex: 0 0 48%; - height: 30rem; - background-color: var(--primaryColor); -} - -.collection__box:not(:last-child) { - margin-right: 1.6rem; -} - -.img__container { - width: 25rem; - text-align: center; -} - -.collection__box img.collection_01 { - width: 60%; -} - -.collection__box img.collection_02 { - width: 75%; -} - -.collection__content { - flex: 0 0 50%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.collection__content span { - color: var(--black); -} - -.collection__content h1 { - margin-top: 0.5rem; -} - -.collection__content a:link, -.collection__content a:visited { - font-weight: 700; - display: inline-block; - padding: 1rem 1.4rem; - margin-top: 1.3rem; - border-radius: 3rem; - border: 2px solid var(--secondaryColor); - color: var(--primaryColor); - background-color: var(--secondaryColor); - transition: all 0.3s ease-in-out; -} - -.collection__content a:hover { - background-color: transparent; - color: var(--secondaryColor); -} - -/* -====================== -Collection Media Query -====================== -*/ -@media only screen and (max-width: 999px) { - .collection__container { - width: 80%; - margin: 0 auto; - flex-direction: column; - height: 65rem; - } - - .collection__box { - width: 100%; - margin: 0 auto; - } - - .collection__box:not(:last-child) { - margin: 0 0 1.6rem; - } -} - -@media only screen and (max-width: 568px) { - .collection { - margin: 0; - position: relative; - } - - .collection__container { - width: 100%; - height: 50rem; - text-align: center; - flex-direction: column; - justify-content: space-around; - } - - .collection__box { - justify-content: space-around; - height: 15rem; - } - - .collection__content { - flex: 0 0 30%; - } - - .collection__data span { - font-size: 1.2rem; - } - - .collection__data h1 { - font-size: 2rem; - } -} - -/* -====================== -Latest Products -====================== -*/ - -.title__container { - display: flex; - align-items: center; - justify-content: center; - margin: 0 auto 6rem; - padding: 2rem 0; - background-color: var(--primaryColor); -} - -.section__titles:not(:last-child) { - margin-right: 1.5rem; -} - -.section__title { - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; -} - -.section__title h1 { - font-size: 1.9rem; - font-weight: inherit; -} - -.section__title:hover .primary__title, -.section__title:hover span.dot, -.section__title:hover span.dot::before { - opacity: 1; -} - -.section__title .primary__title { - opacity: 0.6; - padding-left: 0.5rem; - transition: opacity 0.3s ease-in-out; -} - -span.dot { - opacity: 0.6; - padding: 0.45rem; - position: relative; - border: 1px solid var(--black); - cursor: pointer; - transition: opacity 0.3s ease-in-out; -} - -span.dot::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border: 1px solid var(--black); - background-color: var(--black); - margin: 1px; - opacity: 0.6; -} - -.section__title.active span.dot { - opacity: 1; -} - -.section__title.active span.dot::before { - opacity: 1; -} - -.section__title.active .primary__title { - opacity: 1; -} - -.product { - position: relative; - text-align: center; -} - -.product ul svg { - width: 1.7rem; - height: 1.7rem; - fill: var(--white); -} - -.product ul { - position: absolute; - display: flex; - align-items: center; - justify-content: center; - top: 35%; - left: 50%; - width: 17rem; - height: 5rem; - background-color: rgba(255, 255, 255, 0.5); - opacity: 0; - visibility: hidden; - transform: translate(-50%, -50%) scale(0.7); - transition: all 0.5s ease-in-out; -} - -.product:hover ul { - opacity: 1; - visibility: visible; - transform: translate(-50%, -50%) scale(1); -} - -.product ul li:not(:last-child) { - margin-right: 1.6rem; -} - -.product ul a { - position: relative; - display: flex; - align-items: center; - justify-content: center; - background-color: var(--orange); - width: 3.5rem; - height: 3.5rem; - cursor: pointer; - transition: 0.5s; -} - -.product ul a:hover { - background-color: var(--black); -} - -.product ul a::before { - content: ""; - position: absolute; - top: -0.6rem; - left: -0.6rem; - height: 0; - width: 0; - border-top: 3px solid var(--orange); - border-left: 3px solid var(--orange); - transition: 0.5s; - opacity: 0; - z-index: 1; -} - -.product ul a::after { - content: ""; - position: absolute; - bottom: -0.6rem; - right: -0.6rem; - width: 0; - height: 0; - border-bottom: 3px solid var(--orange); - border-right: 3px solid var(--orange); - z-index: 1; - opacity: 0; - transition: 0.5s; -} - -.product ul a:hover::before { - width: 126%; - height: 126%; - border-top: 3px solid var(--black); - border-left: 3px solid var(--black); - opacity: 1; -} - -.product ul a:hover::after { - width: 126%; - height: 126%; - border-bottom: 3px solid var(--black); - border-right: 3px solid var(--black); - opacity: 1; -} - -@media only screen and (max-width: 567px) { - .title__container { - flex-direction: column; - } - - .section__titles:not(:last-child) { - margin: 0 0 1.3rem; - } -} - -.product { - display: flex; - flex-direction: column; - text-align: center; - width: 25rem; -} - -.product__header { - height: 25rem; - padding: 0.5rem 0; - text-align: center; -} - -.product__header img { - max-width: 100%; - max-height: 100%; -} - -.product__footer { - padding: 1rem 0; -} - -.rating svg { - width: 1.6rem; - height: 1.6rem; - fill: var(--yellow); -} - -.product__footer h3 { - padding: 1rem 0; -} - -.product__footer .product__price { - padding-bottom: 1rem; -} - -.product__footer h3 { - font-size: 1.6rem; -} - -.product__btn { - display: inline-block; - font-weight: 700; - text-transform: uppercase; - width: 100%; - padding: 1.4rem 0; - border: 1px solid var(--black); - color: var(--black); - cursor: pointer; -} - -.product__btn:focus { - outline: none; -} - -.product__btn:hover { - background-color: var(--black); - color: var(--primaryColor); -} - -.latest__products .glide__arrow, -.related__products .glide__arrow { - background-color: transparent; - border: 1px solid var(--primaryColor); - outline: none; - border-radius: 0; - box-shadow: 0 0.25em 0.5em 0 rgba(0, 0, 0, 0); - top: -7%; - left: 80%; -} - -.latest__products .glide__arrow:hover, -.related__products .glide__arrow:hover { - background-color: var(--orange); - border: 1px solid var(--primaryColor); -} - -.latest__products .glide__arrow--left, -.related__products .glide__arrow--left { - left: 75%; -} - -.latest__products .glide__arrow--right, -.related__products .glide__arrow--right { - right: 5%; -} - -.latest__products .glide__arrow svg, -.related__products .glide__arrow svg { - width: 1.5rem; - height: 1.5rem; - fill: var(--grey); -} - -/* -====================== -Latest Products Media Query -====================== -*/ -@media only screen and (max-width: 999px) { - .product__header { - height: 25rem; - } - - .product { - width: 70%; - margin: 0 auto; - } - - .latest__products .glide__arrow--left, - .related__products .glide__arrow--left { - left: 73%; - } - - .latest__products .glide__arrow--right, - .related__products .glide__arrow--right { - right: 7%; - } -} - -@media only screen and (max-width: 768px) { - .latest__products .glide__arrow--left, - .related__products .glide__arrow--left { - left: 70%; - } - - .latest__products .glide__arrow--right, - .related__products .glide__arrow--right { - right: 7%; - } -} - -@media only screen and (max-width: 578px) { - .product__header { - height: 20rem; - } - - .product__btn:hover { - background-color: var(--black); - color: var(--primaryColor); - } - - .product__header img { - width: 50%; - } - - .product__footer h3 { - font-size: 1.4rem; - } - - .product__btn { - width: 100%; - font-size: 1rem; - padding: 0.8rem 0; - border: 1px solid var(--black); - } - - .product ul a { - width: 2.7rem; - height: 2.7rem; - } - - .product ul { - top: 25%; - left: 50%; - width: 16rem; - height: 4rem; - } - - .rating svg { - width: 1.3rem; - height: 1.3rem; - } - - .latest__products .glide__arrow--left, - .related__products .glide__arrow--left { - left: 66%; - } - - .latest__products .glide__arrow--right, - .related__products .glide__arrow--right { - left: 85%; - } -} - -@media only screen and (max-width: 460px) { - .product__header { - height: 12rem; - } - - .product__footer h3 { - font-size: 1.2rem; - } -} - -/* -====================== -Category Center -====================== -*/ -.category__center { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - gap: 3rem 2rem; -} - -@media only screen and (max-width: 999px) { - .category__center { - grid-template-columns: 1fr 1fr 1fr; - } -} - -@media only screen and (max-width: 568px) { - .category__center { - grid-template-columns: 1fr 1fr; - gap: 1.5rem 1rem; - } - - .category__products .product__header { - height: 10rem; - } - - .category__products .product__header img { - object-fit: contain; - } -} - -/* -====================== -Pop Up -====================== -*/ - -.popup { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100vh; - background-color: rgba(0, 0, 0, 0.3); - z-index: 9999; - transition: 0.3s; - transform: scale(1); -} - -.popup__content { - position: absolute; - top: 50%; - left: 50%; - width: 90%; - max-width: 110rem; - margin: 0 auto; - height: 55rem; - transform: translate(-50%, -50%); - padding: 1.6rem; - display: table; - overflow: hidden; - background-color: var(--white); -} - -.popup__close { - display: flex; - align-items: center; - justify-content: center; - position: absolute; - top: 2rem; - right: 2rem; - padding: 1.5rem; - background-color: var(--primaryColor); - border-radius: 50%; - cursor: pointer; -} - -.popup__close svg { - width: 1.7rem; - height: 1.7rem; -} - -.popup__left { - display: table-cell; - width: 50%; - height: 100%; - vertical-align: middle; -} - -.popup__right { - display: table-cell; - width: 50%; - vertical-align: middle; - padding: 3rem 5rem; -} - -.popup-img__container { - width: 100%; - overflow: hidden; -} - -.popup-img__container img.popup__img { - display: block; - width: 60rem; - height: 45rem; - max-width: 100%; - border-radius: 1rem; - object-fit: cover; -} - -.right__content { - text-align: center; - width: 85%; - margin: 0 auto; -} - -.right__content h1 { - font-size: 4rem; - color: #000; - margin-bottom: 1.6rem; -} - -.right__content h1 span { - color: var(--green); -} - -.right__content p { - font-size: 1.8rem; - color: var(--grey2); - margin-bottom: 1.6rem; -} - -.popup__form { - width: 100%; - padding: 2rem 0; - text-indent: 1rem; - margin-bottom: 1.6rem; - border-radius: 3rem; - background-color: var(--primaryColor); - border: none; -} - -.popup__form:focus { - outline: none; -} - -.right__content a:link, -.right__content a:visited { - display: inline-block; - padding: 1.8rem 5rem; - border-radius: 3rem; - font-weight: 700; - color: var(--white); - background-color: var(--black); - border: 1px solid var(--grey2); - transition: 0.3s; -} - -.right__content a:hover { - background-color: var(--green); - border: 1px solid var(--grey2); - color: var(--black); -} - -.hide__popup { - transform: scale(0.2); - opacity: 0; - visibility: hidden; -} - -/* -====================== -Go Up -====================== -*/ -.goto-top:link, -.goto-top:visited { - position: fixed; - right: 2%; - bottom: 10%; - padding: 0.8rem 1.4rem; - border-radius: 1rem; - background-color: var(--orange); - visibility: hidden; - cursor: pointer; - transition: 0.3s; - animation: bounce 2s ease-in-out infinite; -} - -.show-top:link, -.show-top:visited { - visibility: visible; - z-index: 1999; -} - -@keyframes bounce { - 0% { - transform: scale(0.5); - } - - 50% { - transform: scale(1.5); - } - - 0% { - transform: scale(1); - } -} - -.goto-top svg { - width: 1.3rem; - height: 1.3rem; - fill: var(--white); -} - -.goto-top:hover { - background-color: var(--black4); -} - -@media only screen and (min-width: 1250px) { - /* .hero__left span{} */ - - .hero__left h1{ - font-size: 5rem; - } -} - -@media only screen and (max-width: 1200px) { - .right__content { - width: 100%; - } - - .right__content h1 { - font-size: 3.5rem; - margin-bottom: 1.3rem; - } -} - -@media only screen and (max-width: 998px) { - .popup__right { - width: 100%; - } - - .popup__left { - display: none; - } - - .right__content h1 { - font-size: 5rem; - } -} - -@media only screen and (max-width: 768px) { - .right__content h1 { - font-size: 4rem; - } - - .right__content p { - font-size: 1.6rem; - } - - .popup__form { - width: 90%; - margin: 0 auto; - padding: 1.8rem 0; - margin-bottom: 1.5rem; - } - - .goto-top:link, - .goto-top:visited { - right: 5%; - bottom: 5%; - } -} - -@media only screen and (max-width: 568px) { - .popup__right { - padding: 0 1.6rem; - } - - .popup__content { - height: 35rem; - width: 90%; - margin: 0 auto; - } - - .right__content { - width: 100%; - } - - .right__content h1 { - font-size: 3rem; - } - - .right__content p { - font-size: 1.4rem; - } - - .popup__form { - width: 100%; - padding: 1.5rem 0; - margin-bottom: 1.3rem; - } - - .right__content a:link, - .right__content a:visited { - padding: 1.5rem 3rem; - } - - .popup__close { - top: 1rem; - right: 1rem; - padding: 1.3rem; - } - - .popup__close svg { - width: 1.4rem; - height: 1.4rem; - } -} - -/* -====================== -Facility -====================== -*/ - -.facility__section { - background-color: var(--primaryColor); - padding: 6rem 0; -} - -.facility__contianer { - display: grid; - align-items: center; - grid-template-columns: repeat(4, 1fr); -} - -.facility-img__container svg { - width: 3rem; - height: 3rem; - transition: 1s; - perspective: 4000; -} - -.facility__box { - text-align: center; -} - -.facility-img__container { - position: relative; - display: inline-block; - line-height: 9.5rem; - width: 8rem; - height: 8rem; - border-radius: 50%; - border: 1.5px solid var(--white); - z-index: 1; - margin-bottom: 1.6rem; -} - -.facility-img__container::before { - content: ""; - position: absolute; - display: inline-block; - border-radius: 50%; - top: 0; - left: 0; - right: 0; - bottom: 0; - margin: 0.7rem; - background-color: var(--white); - z-index: -1; -} - -.facility__box:hover svg { - transform: rotateY(180deg); - line-height: 9.5rem; -} - -/* -====================== -Facility Media Query -====================== -*/ -@media only screen and (max-width: 998px) { - .facility__contianer { - grid-template-columns: 1fr 1fr; - row-gap: 3rem; - } -} - -@media only screen and (max-width: 568px) { - .facility__contianer { - grid-template-columns: 1fr; - } - - .facility-img__container { - width: 7rem; - height: 7rem; - line-height: 8.5rem; - } - - .facility__contianer p { - font-size: 1.4rem; - } -} - -/* -====================== -Testimonial -====================== -*/ - -.testimonial { - position: relative; - background: url("./images/testimonial.jpg") center/cover no-repeat; - height: 50rem; - padding: 5rem 0; - z-index: 1; - text-align: center; -} - -.testimonial::before { - content: ""; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - background-color: rgba(0, 0, 0, 0.9); - z-index: -1; -} - -.client__image { - margin-bottom: 3rem; -} - -.client__image img { - width: 7rem; - height: 7rem; - max-width: 100%; - object-fit: cover; - border-radius: 50%; -} - -.testimonial__container { - height: 100%; - padding: 1rem; -} - -.testimonial__box { - width: 90%; - margin: 0 auto; - height: 100%; - color: #ccc; -} - -.testimonial__box p { - width: 70%; - margin: 0 auto; - line-height: 2.5rem; - font-style: italic; - font-size: 1.5rem; - margin-bottom: 3rem; -} - -.client__info h3 { - font-weight: 400; - font-size: 2rem; - margin-bottom: 1rem; -} - -.client__info span { - font-size: 1.4rem; -} - -.testimonial .glide__bullets { - bottom: -20%; -} - -/* -====================== -Testimonial Media Query -====================== -*/ -@media only screen and (max-width: 1200px) { - .testimonial__box { - height: 100%; - } - - .testimonial__box p { - width: 90%; - margin: 0 auto; - line-height: 2.2rem; - margin-bottom: 3rem; - } - - .client__image { - margin-bottom: 2.5rem; - } -} - -@media only screen and (max-width: 568px) { - .testimonial { - height: 100%; - padding: 4rem 0 5rem; - z-index: 1; - text-align: center; - } - - .testimonial__box { - height: 100%; - } - - .testimonial__box p { - width: 100%; - font-size: 1.3rem; - line-height: 2rem; - margin-bottom: 2rem; - } - - .client__image { - margin-bottom: 1.5rem; - } - - .testimonial__box span { - margin-bottom: 1rem; - } - - .testimonial .glide__bullets { - bottom: -8%; - } -} - -/* -====================== -News Section -====================== -*/ - -.news { - padding-bottom: 8rem; -} - -.new__card { - background-color: var(--primaryColor); - width: 95%; - margin: 0 auto; -} - -.new__card:not(:last-child) { - margin-right: 1rem; -} - -.card__footer { - padding: 3rem 1.4rem; -} - -.card__footer h3 { - font-size: 2.5rem; - font-weight: 600; - color: var(--black); - margin-bottom: 1rem; -} - -.card__footer span { - display: inline-block; - margin-bottom: 1rem; - color: var(--black2); -} - -.card__footer p { - font-size: 1.5rem; - color: var(--black2); - margin-bottom: 1.6rem; - line-height: 2.7rem; -} - -.card__footer a button, -.card__footer a button { - display: inline-block; - padding: 1.4rem 4rem; - border: 1px solid var(--black); - color: var(--black); - cursor: pointer; - transition: 0.3s; -} - -.card__footer a button:focus { - outline: none; -} - -.card__footer a button:hover { - border: 1px solid var(--black); - color: var(--white); - background-color: var(--black); -} - -/* -====================== -NewsLetter -====================== -*/ - -.newsletter { - padding: 6rem 0; - border-top: 1px solid var(--primaryColor); -} - -.newsletter__content { - display: flex; - align-items: center; - justify-content: space-between; -} - -.newsletter__data h3 { - font-size: 2.4rem; - font-weight: inherit; - margin-bottom: 1rem; -} - -.newsletter__data p { - font-size: 1.5rem; - color: var(--black2); -} - -.newsletter__email { - font-size: 1.4rem; - display: inline-block; - width: 37rem; - padding: 1.6rem; - background-color: var(--primaryColor); - border: none; - text-indent: 1rem; -} - -.newsletter__email:focus { - outline: none; -} - -.newsletter__link:link, -.newsletter__link:visited { - display: inline-block; - padding: 1.4rem 3rem; - margin-left: -0.5rem; - background-color: var(--black); - color: var(--white); - transition: 0.3s; -} - -.newsletter__link:hover { - background-color: #000; -} - -/* -====================== -Newsletter Media Query -====================== -*/ -@media only screen and (max-width: 998px) { - .newsletter { - padding: 6rem 4rem; - } - - .newsletter__content { - flex-direction: column; - align-items: center; - } - - .newsletter__email { - width: 45rem; - } - - .newsletter__data { - margin-bottom: 2rem; - } -} - -@media only screen and (max-width: 768px) { - .newsletter__content { - align-items: center; - justify-content: center; - text-align: center; - } - - .newsletter__email { - width: 45rem; - display: block; - margin-bottom: 1.6rem; - } -} - -@media only screen and (max-width: 568px) { - .newsletter__email { - width: 23rem; - font-size: 1.2rem; - } - - .newsletter__data h3 { - font-size: 1.6rem; - } - - .newsletter__data p { - font-size: 1rem; - } - - .newsletter__link:link, - .newsletter__link:visited { - padding: 1.2rem 2rem; - margin-left: 0; - } -} - -/* -====================== -Footer -====================== -*/ - -.footer { - background-color: var(--black3); - padding: 6rem 1rem; - line-height: 3rem; -} - -.footer-top__box span svg { - width: 1.6rem; - height: 1.6rem; - fill: var(--grey3); -} - -.footer-top__box span { - margin-right: 1rem; -} - -.footer__top { - display: grid; - grid-template-columns: repeat(4, 1fr); - color: var(--grey3); -} - -.footer-top__box a:link, -.footer-top__box a:visited { - display: block; - color: var(--grey); - font-size: 1.4rem; - transition: 0.6s; -} - -.footer-top__box a:hover { - color: var(--orange); -} - -.footer-top__box div { - color: var(--grey); - font-size: 1.4rem; -} - -.footer-top__box h3 { - font-size: 1.8rem; - font-weight: 400; - margin-bottom: 1rem; -} - -@media only screen and (max-width: 998px) { - .footer__top { - grid-template-columns: repeat(2, 1fr); - row-gap: 2rem; - } -} - -@media only screen and (max-width: 768px) { - .footer__top { - grid-template-columns: 1fr; - row-gap: 2rem; - } -} - -/* -====================== -Product Details -====================== -*/ - -.details__container--left, -.product-detail__container { - display: flex; - align-items: flex-start; -} - -.product-detail__container { - display: grid; - grid-template-columns: repeat(2, 1fr); - padding: 2.5rem 0; - margin: 0 auto; -} - -.product-detail__left { - flex: 0 0 50%; - margin-right: 2rem; -} - -.product-detail__right { - flex: 0 0 50%; -} - -.product-detail__container--left img { - width: 100%; - object-fit: cover; -} - -.product__pictures { - display: flex; - flex-direction: column; -} - -.pictures__container { - padding: 1rem; - border: 1px solid var(--primaryColor); - border-right-color: transparent; - cursor: pointer; - width: 6.2rem; - transition: 0.3s; -} - -.pictures__container:not(:last-child) { - border-bottom-color: transparent; -} - -.pictures__container:hover { - border: 1px solid var(--orange); -} - -.pictures__container img { - transition: 0.3s; -} - -.pictures__container:hover img { - scale: 1.1; -} - -.product__picture { - width: 100%; - border: 1px solid var(--primaryColor); - padding: 1rem; - display: flex; - justify-content: center; -} - -.product-details__btn { - display: flex; - justify-content: space-between; - margin-top: 2rem; -} - -.product-details__btn a { - flex: 0 0 47%; - display: inline-block; - padding: 1.6rem 3rem; - text-align: center; - color: var(--black); - border: 1px solid var(--black); -} - -.product-details__btn svg { - width: 1.9rem; - height: 1.9rem; - transition: 0.3s; -} - -.product-details__btn .add, -.product-details__btn .buy { - display: flex; - align-items: center; - justify-content: center; - transition: 0.3s; -} - -.product-details__btn .add span, -.product-details__btn .buy span { - margin-right: 1rem; -} - -.product-details__btn .add:hover, -.product-details__btn .buy:hover { - background-color: var(--black); - color: var(--primaryColor); -} - -.product-details__btn .add:hover svg, -.product-details__btn .buy:hover svg { - fill: var(--primaryColor); -} - -.product-detail__content { - width: 90%; - margin: 0 auto; -} - -.product-detail__content h3 { - font-size: 2.5rem; - margin-bottom: 1.3rem; -} - -.price { - margin-bottom: 1rem; -} - -.new__price { - font-size: 2rem; - color: var(--orange); -} - -.product-detail__content .product__review { - display: flex; - align-items: center; - margin-bottom: 1.6rem; - padding-bottom: 1.6rem; - border-bottom: 0.5px solid var(--primaryColor); -} - -.rating { - margin-right: 1rem; -} - -.product__review a:link, -.product__review a:visited { - color: var(--black); -} - -.product-detail__content p { - font-size: 1.4rem; - color: var(--black2); - line-height: 2.4rem; - margin-bottom: 1.6rem; -} - -.product__info .select { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 1.6rem; -} - -.select .select-box { - background: none; - width: 18rem; - border: none; - padding: 0.5rem 1rem; - border-bottom: 1px solid var(--primaryColor); -} - -.select .select-box:focus { - outline: none; -} - -.select__option label { - font-size: 1.4rem; - color: var(--black3); - display: inline-block; - padding-bottom: 1rem; -} - -.input-counter { - display: flex; - align-items: center; -} - -.input-counter div { - display: flex; -} - -.input-counter li span { - font-size: 1.4rem; - color: var(--black3); - margin-right: 1rem; -} - -.minus-btn, -.plus-btn { - display: inline-block; - border: 1px solid var(--primaryColor); - padding: 0.8rem 1rem; - margin-right: 0; - cursor: pointer; -} - -.plus-btn { - border-left-color: transparent; -} - -.minus-btn { - border-right-color: transparent; -} - -.counter-btn { - width: 7rem; - padding: 1rem 0; - text-align: center; - border: 1px solid var(--primaryColor); -} - -.input-counter svg { - width: 1.8rem; - height: 1.8rem; - fill: var(--grey3); -} - -.product__info li { - margin-bottom: 1.6rem; -} - -.product__info .in-stock { - color: var(--green); -} - -.product__info li a { - font-size: 1.4rem; - color: var(--black2); -} - -.product-info__btn span svg { - width: 1.8rem; - height: 1.8rem; -} - -.product-info__btn { - display: flex; - align-items: center; -} - -.product-info__btn a { - display: flex; - align-items: center; - font-size: 1.2rem; - color: var(--black2); -} - -.product-info__btn a:not(:last-child) { - margin-right: 1rem; -} - -/* Product Details Bottom */ - -.detail__content { - position: relative; - height: 55rem; -} - -.detail__content .content { - position: absolute; - transform: translate(0, 25vh); - transition: all 0.6s ease-in-out; - opacity: 0; - visibility: hidden; - z-index: 555; -} - -.detail__content .content.active { - transform: translate(0, 0vh); - opacity: 1; - visibility: visible; -} - -#shipping h3, -#shipping p { - color: var(--grey2); -} - -#shipping p, -#description p { - padding: 1.6rem 0; - line-height: 2.8rem; -} - -#reviews { - font-size: 3rem; - font-weight: 500; - color: var(--grey2); - border-bottom: 1px solid var(--primaryColor); -} - -#description p, -#description li, -#description h2 { - color: var(--grey2); -} - -#description h2 { - font-weight: 500; - padding: 1rem 0; -} - -#description li { - line-height: 3rem; - color: vaf; -} - -#description ol { - padding-left: 1.6rem; -} - -/* Product Details Bottom Media Query*/ - -@media only screen and (max-width: 1200px) { - .detail__content { - height: 65rem; - } -} - -@media only screen and (max-width: 998px) { - .detail__content { - height: 70rem; - } -} - -@media only screen and (max-width: 768px) { - .detail__content { - height: 85rem; - } -} - -@media only screen and (max-width: 568px) { - .detail__content { - height: 110rem; - } -} - -@media only screen and (max-width: 450px) { - .detail__content { - height: 130rem; - } -} - -@media only screen and (max-width: 340px) { - .detail__content { - height: 160rem; - } -} - -/* -====================== -Product Details Media Query -====================== -*/ -@media only screen and (max-width: 998px) { - .select .select-box { - width: 15rem; - } - - .select__option label { - display: block; - } - - .product-info__btn { - flex-wrap: wrap; - } - - .product-details__btn a { - padding: 1rem 2.5rem; - font-size: 1.4rem; - } - - .picture__container { - width: 90%; - } -} - -@media only screen and (max-width: 768px) { - .details__container--left { - flex-direction: column-reverse; - text-align: center; - } - - .product__pictures { - margin-top: 2rem; - flex-direction: row; - justify-content: center; - } - - .pictures__container { - width: 50%; - border-right-color: var(--primaryColor); - } - - .pictures__container:not(:last-child) { - border-bottom-color: var(--primaryColor); - } - - .product-detail__container { - grid-template-columns: 1fr; - row-gap: 5rem; - } - - .product__info .select { - align-items: flex-start; - flex-direction: column; - } - - .select .select-box { - display: block; - width: 20rem; - } -} - -@media only screen and (max-width: 568px) { - .select .select-box { - width: 15rem; - } - - .input-counter { - align-items: flex-start; - flex-direction: column; - } - - .input-counter div { - margin-top: 1rem; - } -} - -@media only screen and (max-width: 400px) { - .product-details__btn a { - padding: 0.7rem 2rem; - font-size: 1.2rem; - } - - .product-details__btn .add span, - .product-details__btn .buy span { - margin-right: 0rem; - } - - .product__review .rating svg { - width: 1.4rem; - height: 1.4rem; - } -} - -/* -====================== -Cart Area -====================== -*/ -.cart__area { - padding-bottom: 5rem; -} - -.cart__form { - display: block; -} - -.product__thumbnail img { - width: 10rem; - height: 15rem; - object-fit: contain; -} - -.remove__cart-item svg { - width: 1.6rem; - height: 1.6rem; - fill: var(--grey2); - transition: all 0.3s ease-in-out; -} - -.cart__table { - display: block; - width: 100%; - margin-bottom: 4rem; - overflow-x: auto; -} - -.cart__table .table { - border-collapse: collapse; - width: 100%; - max-width: 150rem; -} - -.cart__table .table th { - font-weight: 500; - font-style: 2rem; - text-align: left; - padding: 1.8rem 0; -} - -.cart__table .table td { - vertical-align: middle; - padding: 1.8rem 0; - white-space: nowrap; - border-bottom: 1px solid var(--primaryColor); -} - -.cart__table .table thead { - border-bottom: 1px solid var(--primaryColor); -} - -.product__name a:link, -.product__name a:visited { - font-size: 1.5rem; - color: var(--black2); -} - -.product__name small { - color: var(--grey); - margin-top: 1.6rem; -} - -.product__subtotal .price { - display: inline; -} - -.product__subtotal .price .new__price, -.product__price .price .new__price { - font-size: 1.6rem; -} - -.remove__cart-item { - margin-left: 1rem; -} - -.remove__cart-item:hover svg { - fill: var(--orange); -} - -.cart-btns { - display: flex; - justify-content: space-between; - border-bottom: 1px solid var(--primaryColor); - padding-bottom: 4rem; - margin: 3rem 0; -} - -.continue__shopping a:link, -.continue__shopping a:visited { - font-size: 1.5rem; - padding: 1.2rem 3rem; - color: var(--black); - text-transform: uppercase; - border: 1px solid var(--black); - transition: all 0.4s ease-in-out; -} - -.continue__shopping a:hover { - background-color: var(--black); - color: var(--white); - border: 1px solid var(--black); -} - -.cart__totals { - width: 60rem; - /* height: 30rem; */ - margin: 5rem auto 0 auto; - color: var(--black5); - padding: 4rem 5rem; - background-color: rgba(255, 255, 255, 0.8); - box-shadow: 0px 2px 30px 10px rgba(0, 0, 0, 0.09); - border-radius: 0.5rem; -} - -.cart__totals h3 { - font-weight: 500; - font-size: 1.8rem; - margin-bottom: 1.6rem; -} - -.cart__totals .new__price { - font-size: 1.5rem; -} - -.cart__totals ul { - margin-bottom: 2.5rem; -} - -.cart__totals li { - border: 1px solid var(--primaryColor); - padding: 1.4rem 0.5rem; - position: relative; -} - -.cart__totals li:not(:last-child) { - border-bottom-color: transparent; -} - -.cart__totals li span { - position: absolute; - right: 1rem; -} - -.cart__totals a:link, -.cart__totals a:visited { - font-size: 1.5rem; - padding: 1.2rem 3rem; - color: var(--black); - text-transform: uppercase; - border: 1px solid var(--black); - transition: all 0.4s ease-in-out; -} - -.cart__totals a:hover { - background-color: var(--black); - color: var(--white); - border: 1px solid var(--black); -} - -.auth .login-form{ - position: absolute; - top: 9%; - right: -110%; - width: 35rem; - box-shadow: var(--box-shadow); - padding: 2rem; - border-radius: .5rem; - background-color: #fff; - text-align: center; - z-index: 5; -} -.auth .login-form.active{ - right: 6rem; - transition: .4s linear; -} - -.auth .login-form h3{ - font-size: 2.5rem; - text-transform: uppercase; - color: var(--black); -} - -.auth .login-form .box{ - width: 100%; - margin: .7rem 0; - background-color: #eee; - border-radius: .5rem; - padding: 1rem; - font-size: 1.6rem; - color: var(--black); - text-transform: none; -} -.auth .login-form p{ - font-size: 1.4rem; - padding: .5rem 0; - color: var(--light-color); -} -.auth .login-form p a{ - color: var(--orange); - text-decoration: underline; -} - -.searchF .search-form{ - position: absolute; - top: 9%; - right: -110%; - max-width: 50rem; - height: 5rem; - background-color: #fff; - border-radius: .5rem; - overflow: hidden; - display: flex; - box-shadow: var(--box-shadow); - z-index: 5; -} - -.searchF .search-form.active{ - right: 2rem; - transition: .4s linear; -} -.searchF .search-form input{ - height: 100%; - width: 100%; - background-color: none; - text-transform: none; - font-size: 1.6rem; - color: var(--black); - padding: 0 1.5rem; -} - -.searchF .search-form label{ - font-size: 2.2rem; - padding-right: 1.5rem; - padding-top: 1.4rem; - color: var(--black); - cursor: pointer; -} -.searchF .search-form label:hover{ - color: var(--orange); -} - -.shopping-cart{ - position: absolute; - top: 9%; right: -110%; - padding: 1rem; - border-radius: .5rem; - box-shadow: var(--box-shadow); - width: 30rem; - background-color: #fff; - z-index: 5; -} -.shopping-cart.active{ - right: 6rem; - transition: .4s linear; -} -.shopping-cart .box{ - display: flex; - align-items: center; - gap: 1rem; - position: relative; - margin: 1rem 0; -} - -.shopping-cart .box img{ - height: 10rem; - width: 10rem; - object-fit: contain; -} - -.shopping-cart .box .fa-trash{ - font-size: 2rem; - position: absolute; - top: 50%; right: 2rem; - cursor: pointer; - color: var(--light-color); - transform: translateY(-50%); -} -.shopping-cart .box .fa-trash:hover{ - color: var(--orange); -} - -.shopping-cart .box .content h3{ - color: var(--black); - font-size: 1.7rem; - padding-bottom: 1rem; -} -.shopping-cart .box .content span{ - color: var(--light-color); - font-size: 1.6rem; -} - -.shopping-cart .box .content .quantity{ - padding-left: 1rem; -} - -.shopping-cart .total{ - font-size: 2.5rem; - padding: 1rem 0; - text-align: center; - color:var(--black); -} -.shopping-cart .btn{ - display: block; - text-align: center; - margin: 1rem; -} - -/* -====================== -Cart Area Media Query -====================== -*/ - -@media only screen and (max-width: 1200px) { - .minus-btn, - .plus-btn { - padding: 0.6rem 0.8rem; - margin-right: 0; - } - - .counter-btn { - width: 4rem; - padding: 1rem 0; - } - - .input-counter svg { - width: 1.5rem; - height: 1.5rem; - } -} - -@media only screen and (max-width: 568px) { - .shopping-cart.active { - top: 14%; - right: 50%; - transform: translateX(50%); - } - - .auth .login-form.active { - top: 14%; - right: 50%; - transform: translateX(50%); - } - - .searchF .search-form{ - top: 14%; - } -} diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..91279a4 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,238 @@ + + + + + + Admin Dashboard + + + + + +
+
+
+
+ +
+
+

Admin Dashboard

+
+ +
+
+

Create / Edit Product

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

Products

+
+ + + {% include 'includes/export_controls.html' %} +
+
+ + + + + + + + + + + + + +
TitleBrandPriceStockCategoryStatusActions
+ +
+
+
+
+ + + + + + + diff --git a/templates/cart.html b/templates/cart.html new file mode 100644 index 0000000..f6fd582 --- /dev/null +++ b/templates/cart.html @@ -0,0 +1,386 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + Phone Shop - Cart + + + + + +
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + {% endwith %} +
+ {% if cart %} +
+
+ + + + + + + + + + + + + + + + +
PRODUCTNAMEUNIT PRICEQUANTITYTOTAL
+
+

Loading your cart...

+
+
+
+
+ +
+ +
+ + Shipping(+7$) +
+
+ +
+

Cart Totals

+
    +
  • + Subtotal + $0.00 +
  • +
  • + Shipping + $0 +
  • +
  • + Total + $0.00 +
  • +
+ PROCEED TO CHECKOUT +
+
+
+
+
+ + +
+
+
+
+
+ + + +
+

FREE SHIPPING WORLD WIDE

+
+ +
+
+ + + +
+

100% MONEY BACK GUARANTEE

+
+ +
+
+ + + +
+

MANY PAYMENT GATWAYS

+
+ +
+
+ + + +
+

24/7 ONLINE SUPPORT

+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/includes/export_controls.html b/templates/includes/export_controls.html new file mode 100644 index 0000000..32e1f74 --- /dev/null +++ b/templates/includes/export_controls.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/index.html b/templates/index.html similarity index 59% rename from index.html rename to templates/index.html index 6bfec5f..28266b4 100644 --- a/index.html +++ b/templates/index.html @@ -7,8 +7,11 @@ + + + - + @@ -18,13 +21,21 @@ - + + + + Phone Shop -