Tools: Latest: FullStack example React JSX

Tools: Latest: FullStack example React JSX

Building a Full-Stack User Authentication & Profile System with React and Flask

Prerequisites

Step 1 — Setting Up the Database

Step 2 — Setting Up the Flask Backend

Step 3 — Setting Up the React Frontend

Step 4 — Login Page

Key points:

Step 6 — Profile Page

Example of CSS for profile

Key points:

How It All Connects In this tutorial, we will build a complete user authentication system with a profile page using React on the frontend and Flask with MySQL on the backend. We will cover registration, login, and a dynamic profile page with a product carousel and loyalty card. Project Structure

project/├── frontend/│ └── src/│ ├── pages/│ │ ├── login/│ │ │ ├── login.jsx│ │ │ └── login.css│ │ ├── signUp/│ │ │ ├── signUp.jsx│ │ │ └── signUp.css│ │ └── profile/│ │ ├── profile.jsx│ │ └── profile.css│ ├── components/│ │ └── productCard/│ │ └── productCard.jsx│ ├── data/│ │ └── products.js│ └── assets/│ ├── chicken.png│ ├── bag.png│ └── cow.svg└── backend/ └── app.py First, create your MySQL database and tables: Install the required Python packages: bashpip install flask flask-cors mysql-connector-python werkzeug python-dotenv Install the required packages: bashnpm install react-router-dom The login page handles user authentication, stores the userId in localStorage, and displays Flask error messages directly in the UI. The profile page fetches the logged-in user's data using the stored userId, displays a product carousel, and shows a loyalty card. User fills Sign Up / Login form ↓React sends POST to Flask (/api/register or /api/login) ↓Flask validates → queries MySQL → returns { success, user } ↓React saves userId to localStorage ↓Profile page reads userId from localStorage ↓React sends GET to Flask (/api/user/profile?userId=...) ↓Flask queries MySQL → returns user object ↓React renders Welcome, Carousel, Loyalty Card Backend:bashcd backendpython app.py Frontend:bashcd frontendnpm run dev Make sure your Vite config proxies API calls to Flask: Conclusion

In this tutorial we built a full-stack authentication system using React and Flask. We covered secure password hashing with Werkzeug, MySQL database integration, localStorage for session persistence, and a dynamic profile page with a product carousel and loyalty card. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

-- Drop existing database if it exists DROP DATABASE IF EXISTS task_db; -- Create a fresh database CREATE DATABASE task_db; USE task_db; -- Role table CREATE TABLE IF NOT EXISTS role ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE, description VARCHAR(255) ); -- Insert default roles INSERT INTO role (name, description) VALUES ('admin', 'Full system access'), ('customer', 'Can browse and place orders'), ('staff', 'Can manage orders and products'), ('vendor', 'Supplier with limited access') ON DUPLICATE KEY UPDATE name = name; -- Users table (renamed from user) CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, role_id INT NOT NULL, firstName VARCHAR(100) NOT NULL, lastName VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, phone VARCHAR(20), userPoints INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (role_id) REFERENCES role(id) ); -- Producer table CREATE TABLE IF NOT EXISTS producer ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL UNIQUE, bio TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Category table CREATE TABLE IF NOT EXISTS category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE ); -- Insert default categories INSERT INTO category (name) VALUES ('Vegetables'), ('Fruits'), ('Dairy'), ('Honey & Preserves'), ('Meat & Poultry'), ('Drinks'), ('Bakery'), ('Herbs & Flowers') ON DUPLICATE KEY UPDATE name = name; -- Product table CREATE TABLE IF NOT EXISTS product ( id INT AUTO_INCREMENT PRIMARY KEY, producer_id INT NOT NULL, category_id INT NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, stock_qty INT NOT NULL DEFAULT 0, unit VARCHAR(50) NOT NULL DEFAULT 'item', is_organic BOOLEAN DEFAULT FALSE, is_available BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (producer_id) REFERENCES producer(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES category(id) ); -- Address table CREATE TABLE IF NOT EXISTS address ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, street VARCHAR(255) NOT NULL, city VARCHAR(100) NOT NULL, postcode VARCHAR(20) NOT NULL, country VARCHAR(100) NOT NULL DEFAULT 'United Kingdom', is_default BOOLEAN DEFAULT FALSE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Orders table (renamed from `order`) CREATE TABLE IF NOT EXISTS orders ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, address_id INT NOT NULL, status ENUM('pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending', total_price DECIMAL(10, 2) NOT NULL, payment_status ENUM('unpaid', 'paid', 'refunded') DEFAULT 'unpaid', payment_method VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (address_id) REFERENCES address(id) ); -- Order item table CREATE TABLE IF NOT EXISTS order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, unit_price DECIMAL(10, 2) NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES product(id) ); -- Drop existing database if it exists DROP DATABASE IF EXISTS task_db; -- Create a fresh database CREATE DATABASE task_db; USE task_db; -- Role table CREATE TABLE IF NOT EXISTS role ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE, description VARCHAR(255) ); -- Insert default roles INSERT INTO role (name, description) VALUES ('admin', 'Full system access'), ('customer', 'Can browse and place orders'), ('staff', 'Can manage orders and products'), ('vendor', 'Supplier with limited access') ON DUPLICATE KEY UPDATE name = name; -- Users table (renamed from user) CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, role_id INT NOT NULL, firstName VARCHAR(100) NOT NULL, lastName VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, phone VARCHAR(20), userPoints INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (role_id) REFERENCES role(id) ); -- Producer table CREATE TABLE IF NOT EXISTS producer ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL UNIQUE, bio TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Category table CREATE TABLE IF NOT EXISTS category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE ); -- Insert default categories INSERT INTO category (name) VALUES ('Vegetables'), ('Fruits'), ('Dairy'), ('Honey & Preserves'), ('Meat & Poultry'), ('Drinks'), ('Bakery'), ('Herbs & Flowers') ON DUPLICATE KEY UPDATE name = name; -- Product table CREATE TABLE IF NOT EXISTS product ( id INT AUTO_INCREMENT PRIMARY KEY, producer_id INT NOT NULL, category_id INT NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, stock_qty INT NOT NULL DEFAULT 0, unit VARCHAR(50) NOT NULL DEFAULT 'item', is_organic BOOLEAN DEFAULT FALSE, is_available BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (producer_id) REFERENCES producer(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES category(id) ); -- Address table CREATE TABLE IF NOT EXISTS address ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, street VARCHAR(255) NOT NULL, city VARCHAR(100) NOT NULL, postcode VARCHAR(20) NOT NULL, country VARCHAR(100) NOT NULL DEFAULT 'United Kingdom', is_default BOOLEAN DEFAULT FALSE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Orders table (renamed from `order`) CREATE TABLE IF NOT EXISTS orders ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, address_id INT NOT NULL, status ENUM('pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending', total_price DECIMAL(10, 2) NOT NULL, payment_status ENUM('unpaid', 'paid', 'refunded') DEFAULT 'unpaid', payment_method VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (address_id) REFERENCES address(id) ); -- Order item table CREATE TABLE IF NOT EXISTS order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, unit_price DECIMAL(10, 2) NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES product(id) ); -- Drop existing database if it exists DROP DATABASE IF EXISTS task_db; -- Create a fresh database CREATE DATABASE task_db; USE task_db; -- Role table CREATE TABLE IF NOT EXISTS role ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE, description VARCHAR(255) ); -- Insert default roles INSERT INTO role (name, description) VALUES ('admin', 'Full system access'), ('customer', 'Can browse and place orders'), ('staff', 'Can manage orders and products'), ('vendor', 'Supplier with limited access') ON DUPLICATE KEY UPDATE name = name; -- Users table (renamed from user) CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, role_id INT NOT NULL, firstName VARCHAR(100) NOT NULL, lastName VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, phone VARCHAR(20), userPoints INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (role_id) REFERENCES role(id) ); -- Producer table CREATE TABLE IF NOT EXISTS producer ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL UNIQUE, bio TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Category table CREATE TABLE IF NOT EXISTS category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE ); -- Insert default categories INSERT INTO category (name) VALUES ('Vegetables'), ('Fruits'), ('Dairy'), ('Honey & Preserves'), ('Meat & Poultry'), ('Drinks'), ('Bakery'), ('Herbs & Flowers') ON DUPLICATE KEY UPDATE name = name; -- Product table CREATE TABLE IF NOT EXISTS product ( id INT AUTO_INCREMENT PRIMARY KEY, producer_id INT NOT NULL, category_id INT NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, stock_qty INT NOT NULL DEFAULT 0, unit VARCHAR(50) NOT NULL DEFAULT 'item', is_organic BOOLEAN DEFAULT FALSE, is_available BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (producer_id) REFERENCES producer(id) ON DELETE CASCADE, FOREIGN KEY (category_id) REFERENCES category(id) ); -- Address table CREATE TABLE IF NOT EXISTS address ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, street VARCHAR(255) NOT NULL, city VARCHAR(100) NOT NULL, postcode VARCHAR(20) NOT NULL, country VARCHAR(100) NOT NULL DEFAULT 'United Kingdom', is_default BOOLEAN DEFAULT FALSE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- Orders table (renamed from `order`) CREATE TABLE IF NOT EXISTS orders ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, address_id INT NOT NULL, status ENUM('pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending', total_price DECIMAL(10, 2) NOT NULL, payment_status ENUM('unpaid', 'paid', 'refunded') DEFAULT 'unpaid', payment_method VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (address_id) REFERENCES address(id) ); -- Order item table CREATE TABLE IF NOT EXISTS order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, unit_price DECIMAL(10, 2) NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES product(id) ); from flask import Flask, jsonify, request from flask_cors import CORS from werkzeug.security import generate_password_hash, check_password_hash import mysql.connector import os from dotenv import load_dotenv load_dotenv() app = Flask(__name__) CORS(app) # ---------- DB CONNECTION ---------- def get_cursor(): conn = mysql.connector.connect( host=os.getenv("DB_HOST", "localhost"), port=int(os.getenv("DB_PORT", 3306)), user=os.getenv("DB_USER"), password=os.getenv("DB_PASSWORD"), database=os.getenv("DB_NAME", "task_db") ) return conn, conn.cursor(dictionary=True) # ---------- REGISTER ---------- @app.post("/api/register") def register(): conn, cursor = get_cursor() try: data = request.json or {} firstName = data.get("firstName", "").strip() lastName = data.get("lastName", "").strip() email = data.get("email", "").strip() password = data.get("password", "").strip() confirmPassword = data.get("confirmPassword", "").strip() # Validation if not firstName: return jsonify({"success": False, "message": "First name is required."}), 400 if not lastName: return jsonify({"success": False, "message": "Last name is required."}), 400 if not email: return jsonify({"success": False, "message": "Email is required."}), 400 if not password: return jsonify({"success": False, "message": "Password is required."}), 400 if len(password) < 8: return jsonify({"success": False, "message": "Password must be at least 8 characters."}), 400 if password != confirmPassword: return jsonify({"success": False, "message": "Passwords do not match."}), 400 # Check duplicate email cursor.execute("SELECT id FROM users WHERE email = %s", (email,)) if cursor.fetchone(): return jsonify({"success": False, "message": "Email already registered."}), 400 # Hash password before storing hashed_password = generate_password_hash(password) # Insert new user (role_id 2 = customer) cursor.execute(""" INSERT INTO users (role_id, firstName, lastName, email, password_hash) VALUES (%s, %s, %s, %s, %s) """, (2, firstName, lastName, email, hashed_password)) conn.commit() # Get new user ID cursor.execute("SELECT LAST_INSERT_ID() AS id") new_user = cursor.fetchone() return jsonify({ "success": True, "message": "User registered successfully!", "user": {"id": new_user["id"]} }), 200 except Exception as e: conn.rollback() print("REGISTER ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() # ---------- LOGIN ---------- @app.post("/api/login") def login(): conn, cursor = get_cursor() try: data = request.json or {} email = data.get("email", "").strip() password = data.get("password", "").strip() # Validation if not email: return jsonify({"success": False, "message": "Email is required."}), 400 if not password: return jsonify({"success": False, "message": "Password is required."}), 400 cursor.execute("SELECT * FROM users WHERE email = %s", (email,)) user = cursor.fetchone() if not user: return jsonify({"success": False, "message": "Email not found."}), 400 # Check hashed password if not check_password_hash(user["password_hash"], password): return jsonify({"success": False, "message": "Incorrect password."}), 400 return jsonify({ "success": True, "message": "Login successful.", "user": { "id": user["id"], "firstName": user["firstName"], "lastName": user["lastName"], "email": user["email"], "phone": user.get("phone"), } }), 200 except Exception as e: print("LOGIN ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() # ---------- GET PROFILE ---------- @app.get("/api/user/profile") def get_profile(): conn, cursor = get_cursor() try: user_id = request.args.get("userId") if not user_id: return jsonify({"success": False, "message": "Missing userId"}), 400 cursor.execute(""" SELECT id, firstName, lastName, email, phone FROM users WHERE id = %s """, (user_id,)) user = cursor.fetchone() if not user: return jsonify({"success": False, "message": "User not found"}), 404 return jsonify({"success": True, "user": user}), 200 except Exception as e: print("PROFILE ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() # ---------- UPDATE PROFILE ---------- @app.post("/api/user/update") def update_profile(): conn, cursor = get_cursor() try: data = request.json or {} user_id = data.get("userId") if not user_id: return jsonify({"success": False, "message": "Missing userId"}), 400 firstName = data.get("firstName", "").strip() lastName = data.get("lastName", "").strip() email = data.get("email", "").strip() phone = data.get("phone", "").strip() if not firstName: return jsonify({"success": False, "message": "First name is required."}), 400 if not lastName: return jsonify({"success": False, "message": "Last name is required."}), 400 if not email: return jsonify({"success": False, "message": "Email is required."}), 400 # Check if new email is taken by another user cursor.execute("SELECT id FROM users WHERE email = %s AND id != %s", (email, user_id)) if cursor.fetchone(): return jsonify({"success": False, "message": "Email already in use."}), 400 cursor.execute(""" UPDATE users SET firstName=%s, lastName=%s, email=%s, phone=%s WHERE id=%s """, (firstName, lastName, email, phone, user_id)) conn.commit() return jsonify({"success": True, "message": "Profile updated"}), 200 except Exception as e: conn.rollback() print("UPDATE ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() if __name__ == "__main__": app.run(debug=True) from flask import Flask, jsonify, request from flask_cors import CORS from werkzeug.security import generate_password_hash, check_password_hash import mysql.connector import os from dotenv import load_dotenv load_dotenv() app = Flask(__name__) CORS(app) # ---------- DB CONNECTION ---------- def get_cursor(): conn = mysql.connector.connect( host=os.getenv("DB_HOST", "localhost"), port=int(os.getenv("DB_PORT", 3306)), user=os.getenv("DB_USER"), password=os.getenv("DB_PASSWORD"), database=os.getenv("DB_NAME", "task_db") ) return conn, conn.cursor(dictionary=True) # ---------- REGISTER ---------- @app.post("/api/register") def register(): conn, cursor = get_cursor() try: data = request.json or {} firstName = data.get("firstName", "").strip() lastName = data.get("lastName", "").strip() email = data.get("email", "").strip() password = data.get("password", "").strip() confirmPassword = data.get("confirmPassword", "").strip() # Validation if not firstName: return jsonify({"success": False, "message": "First name is required."}), 400 if not lastName: return jsonify({"success": False, "message": "Last name is required."}), 400 if not email: return jsonify({"success": False, "message": "Email is required."}), 400 if not password: return jsonify({"success": False, "message": "Password is required."}), 400 if len(password) < 8: return jsonify({"success": False, "message": "Password must be at least 8 characters."}), 400 if password != confirmPassword: return jsonify({"success": False, "message": "Passwords do not match."}), 400 # Check duplicate email cursor.execute("SELECT id FROM users WHERE email = %s", (email,)) if cursor.fetchone(): return jsonify({"success": False, "message": "Email already registered."}), 400 # Hash password before storing hashed_password = generate_password_hash(password) # Insert new user (role_id 2 = customer) cursor.execute(""" INSERT INTO users (role_id, firstName, lastName, email, password_hash) VALUES (%s, %s, %s, %s, %s) """, (2, firstName, lastName, email, hashed_password)) conn.commit() # Get new user ID cursor.execute("SELECT LAST_INSERT_ID() AS id") new_user = cursor.fetchone() return jsonify({ "success": True, "message": "User registered successfully!", "user": {"id": new_user["id"]} }), 200 except Exception as e: conn.rollback() print("REGISTER ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() # ---------- LOGIN ---------- @app.post("/api/login") def login(): conn, cursor = get_cursor() try: data = request.json or {} email = data.get("email", "").strip() password = data.get("password", "").strip() # Validation if not email: return jsonify({"success": False, "message": "Email is required."}), 400 if not password: return jsonify({"success": False, "message": "Password is required."}), 400 cursor.execute("SELECT * FROM users WHERE email = %s", (email,)) user = cursor.fetchone() if not user: return jsonify({"success": False, "message": "Email not found."}), 400 # Check hashed password if not check_password_hash(user["password_hash"], password): return jsonify({"success": False, "message": "Incorrect password."}), 400 return jsonify({ "success": True, "message": "Login successful.", "user": { "id": user["id"], "firstName": user["firstName"], "lastName": user["lastName"], "email": user["email"], "phone": user.get("phone"), } }), 200 except Exception as e: print("LOGIN ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() # ---------- GET PROFILE ---------- @app.get("/api/user/profile") def get_profile(): conn, cursor = get_cursor() try: user_id = request.args.get("userId") if not user_id: return jsonify({"success": False, "message": "Missing userId"}), 400 cursor.execute(""" SELECT id, firstName, lastName, email, phone FROM users WHERE id = %s """, (user_id,)) user = cursor.fetchone() if not user: return jsonify({"success": False, "message": "User not found"}), 404 return jsonify({"success": True, "user": user}), 200 except Exception as e: print("PROFILE ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() # ---------- UPDATE PROFILE ---------- @app.post("/api/user/update") def update_profile(): conn, cursor = get_cursor() try: data = request.json or {} user_id = data.get("userId") if not user_id: return jsonify({"success": False, "message": "Missing userId"}), 400 firstName = data.get("firstName", "").strip() lastName = data.get("lastName", "").strip() email = data.get("email", "").strip() phone = data.get("phone", "").strip() if not firstName: return jsonify({"success": False, "message": "First name is required."}), 400 if not lastName: return jsonify({"success": False, "message": "Last name is required."}), 400 if not email: return jsonify({"success": False, "message": "Email is required."}), 400 # Check if new email is taken by another user cursor.execute("SELECT id FROM users WHERE email = %s AND id != %s", (email, user_id)) if cursor.fetchone(): return jsonify({"success": False, "message": "Email already in use."}), 400 cursor.execute(""" UPDATE users SET firstName=%s, lastName=%s, email=%s, phone=%s WHERE id=%s """, (firstName, lastName, email, phone, user_id)) conn.commit() return jsonify({"success": True, "message": "Profile updated"}), 200 except Exception as e: conn.rollback() print("UPDATE ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() if __name__ == "__main__": app.run(debug=True) from flask import Flask, jsonify, request from flask_cors import CORS from werkzeug.security import generate_password_hash, check_password_hash import mysql.connector import os from dotenv import load_dotenv load_dotenv() app = Flask(__name__) CORS(app) # ---------- DB CONNECTION ---------- def get_cursor(): conn = mysql.connector.connect( host=os.getenv("DB_HOST", "localhost"), port=int(os.getenv("DB_PORT", 3306)), user=os.getenv("DB_USER"), password=os.getenv("DB_PASSWORD"), database=os.getenv("DB_NAME", "task_db") ) return conn, conn.cursor(dictionary=True) # ---------- REGISTER ---------- @app.post("/api/register") def register(): conn, cursor = get_cursor() try: data = request.json or {} firstName = data.get("firstName", "").strip() lastName = data.get("lastName", "").strip() email = data.get("email", "").strip() password = data.get("password", "").strip() confirmPassword = data.get("confirmPassword", "").strip() # Validation if not firstName: return jsonify({"success": False, "message": "First name is required."}), 400 if not lastName: return jsonify({"success": False, "message": "Last name is required."}), 400 if not email: return jsonify({"success": False, "message": "Email is required."}), 400 if not password: return jsonify({"success": False, "message": "Password is required."}), 400 if len(password) < 8: return jsonify({"success": False, "message": "Password must be at least 8 characters."}), 400 if password != confirmPassword: return jsonify({"success": False, "message": "Passwords do not match."}), 400 # Check duplicate email cursor.execute("SELECT id FROM users WHERE email = %s", (email,)) if cursor.fetchone(): return jsonify({"success": False, "message": "Email already registered."}), 400 # Hash password before storing hashed_password = generate_password_hash(password) # Insert new user (role_id 2 = customer) cursor.execute(""" INSERT INTO users (role_id, firstName, lastName, email, password_hash) VALUES (%s, %s, %s, %s, %s) """, (2, firstName, lastName, email, hashed_password)) conn.commit() # Get new user ID cursor.execute("SELECT LAST_INSERT_ID() AS id") new_user = cursor.fetchone() return jsonify({ "success": True, "message": "User registered successfully!", "user": {"id": new_user["id"]} }), 200 except Exception as e: conn.rollback() print("REGISTER ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() # ---------- LOGIN ---------- @app.post("/api/login") def login(): conn, cursor = get_cursor() try: data = request.json or {} email = data.get("email", "").strip() password = data.get("password", "").strip() # Validation if not email: return jsonify({"success": False, "message": "Email is required."}), 400 if not password: return jsonify({"success": False, "message": "Password is required."}), 400 cursor.execute("SELECT * FROM users WHERE email = %s", (email,)) user = cursor.fetchone() if not user: return jsonify({"success": False, "message": "Email not found."}), 400 # Check hashed password if not check_password_hash(user["password_hash"], password): return jsonify({"success": False, "message": "Incorrect password."}), 400 return jsonify({ "success": True, "message": "Login successful.", "user": { "id": user["id"], "firstName": user["firstName"], "lastName": user["lastName"], "email": user["email"], "phone": user.get("phone"), } }), 200 except Exception as e: print("LOGIN ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() # ---------- GET PROFILE ---------- @app.get("/api/user/profile") def get_profile(): conn, cursor = get_cursor() try: user_id = request.args.get("userId") if not user_id: return jsonify({"success": False, "message": "Missing userId"}), 400 cursor.execute(""" SELECT id, firstName, lastName, email, phone FROM users WHERE id = %s """, (user_id,)) user = cursor.fetchone() if not user: return jsonify({"success": False, "message": "User not found"}), 404 return jsonify({"success": True, "user": user}), 200 except Exception as e: print("PROFILE ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() # ---------- UPDATE PROFILE ---------- @app.post("/api/user/update") def update_profile(): conn, cursor = get_cursor() try: data = request.json or {} user_id = data.get("userId") if not user_id: return jsonify({"success": False, "message": "Missing userId"}), 400 firstName = data.get("firstName", "").strip() lastName = data.get("lastName", "").strip() email = data.get("email", "").strip() phone = data.get("phone", "").strip() if not firstName: return jsonify({"success": False, "message": "First name is required."}), 400 if not lastName: return jsonify({"success": False, "message": "Last name is required."}), 400 if not email: return jsonify({"success": False, "message": "Email is required."}), 400 # Check if new email is taken by another user cursor.execute("SELECT id FROM users WHERE email = %s AND id != %s", (email, user_id)) if cursor.fetchone(): return jsonify({"success": False, "message": "Email already in use."}), 400 cursor.execute(""" UPDATE users SET firstName=%s, lastName=%s, email=%s, phone=%s WHERE id=%s """, (firstName, lastName, email, phone, user_id)) conn.commit() return jsonify({"success": True, "message": "Profile updated"}), 200 except Exception as e: conn.rollback() print("UPDATE ERROR:", e) return jsonify({"success": False, "message": "Server error"}), 500 finally: cursor.close() conn.close() if __name__ == "__main__": app.run(debug=True) DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PASSWORD=yourpassword DB_NAME=task_db Create app.py: DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PASSWORD=yourpassword DB_NAME=task_db Create app.py: DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PASSWORD=yourpassword DB_NAME=task_db Create app.py: import { useState } from "react"; import { useNavigate } from "react-router-dom"; import "./login.css"; import Chicken from "../../assets/chicken.png"; export default function Login() { const navigate = useNavigate(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const handleLogin = async () => { try { const res = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }) }); const data = await res.json(); if (!data.success) { setError(data.message); return; } localStorage.setItem("userId", data.user.id); navigate("/profile"); } catch (err) { setError("Something went wrong."); } }; return ( <div className="body"> <div className="loginContainer"> <div className="mainLoginContainer"> <h1 className="loginHeader">Login</h1> <div className="loginTop"> <input className="email" placeholder="Email" value={email} type="email" onChange={(e) => setEmail(e.target.value)} /> <input className="password" placeholder="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> {error && <div className="errorPopup">{error}</div>} <div className="loginBottom"> <button className="loginSubmit" onClick={handleLogin}> Login </button> </div> </div> </div> <div className="imgContainer"> <img src={Chicken} alt="chicken" className="chickenImg" /> </div> </div> ); } import { useState } from "react"; import { useNavigate } from "react-router-dom"; import "./login.css"; import Chicken from "../../assets/chicken.png"; export default function Login() { const navigate = useNavigate(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const handleLogin = async () => { try { const res = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }) }); const data = await res.json(); if (!data.success) { setError(data.message); return; } localStorage.setItem("userId", data.user.id); navigate("/profile"); } catch (err) { setError("Something went wrong."); } }; return ( <div className="body"> <div className="loginContainer"> <div className="mainLoginContainer"> <h1 className="loginHeader">Login</h1> <div className="loginTop"> <input className="email" placeholder="Email" value={email} type="email" onChange={(e) => setEmail(e.target.value)} /> <input className="password" placeholder="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> {error && <div className="errorPopup">{error}</div>} <div className="loginBottom"> <button className="loginSubmit" onClick={handleLogin}> Login </button> </div> </div> </div> <div className="imgContainer"> <img src={Chicken} alt="chicken" className="chickenImg" /> </div> </div> ); } import { useState } from "react"; import { useNavigate } from "react-router-dom"; import "./login.css"; import Chicken from "../../assets/chicken.png"; export default function Login() { const navigate = useNavigate(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const handleLogin = async () => { try { const res = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }) }); const data = await res.json(); if (!data.success) { setError(data.message); return; } localStorage.setItem("userId", data.user.id); navigate("/profile"); } catch (err) { setError("Something went wrong."); } }; return ( <div className="body"> <div className="loginContainer"> <div className="mainLoginContainer"> <h1 className="loginHeader">Login</h1> <div className="loginTop"> <input className="email" placeholder="Email" value={email} type="email" onChange={(e) => setEmail(e.target.value)} /> <input className="password" placeholder="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> </div> {error && <div className="errorPopup">{error}</div>} <div className="loginBottom"> <button className="loginSubmit" onClick={handleLogin}> Login </button> </div> </div> </div> <div className="imgContainer"> <img src={Chicken} alt="chicken" className="chickenImg" /> </div> </div> ); } import { useState, useEffect } from "react"; import { products } from "../../data/products.js"; import { useNavigate } from "react-router-dom"; import "../profile/profile.css"; import ProductCard from "../../components/productCard/productCard.jsx"; import Cow from "../../assets/cow.svg"; export default function Profile() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [currentIndex, setCurrentIndex] = useState(0); const navigate = useNavigate(); const nextSlide = () => { setCurrentIndex((prev) => (prev + 2 >= products.length ? 0 : prev + 2)); }; const prevSlide = () => { setCurrentIndex((prev) => { if (prev === 0) { return products.length % 2 === 0 ? products.length - 2 : products.length - 1; } return prev - 2; }); }; const handleAddToBasket = (product) => { console.log("Added to basket:", product.name); }; useEffect(() => { const storedUserId = localStorage.getItem("userId"); if (!storedUserId) { setLoading(false); return; } fetch(`/api/user/profile?userId=${storedUserId}`) .then((res) => res.json()) .then((data) => { if (data.success) { setUser(data.user); } }) .catch((err) => console.error("Error:", err)) .finally(() => setLoading(false)); }, []); if (loading) return <p>Loading...</p>; if (!user) return <p>Please log in.</p>; return ( <div className="profilePage"> <div className="topSection"> <div className="welcomeBox"> <h2>Welcome {user.firstName}!</h2> <p className="welcomeDesc">Recommended products for you</p> <div className="carouselContainer"> <button className="carouselArrow" onClick={prevSlide}>❮</button> <div className="carouselWrapper" key={currentIndex}> {products.slice(currentIndex, currentIndex + 2).map((product) => ( <ProductCard key={product.id} product={product} onAdd={handleAddToBasket} /> ))} </div> <button className="carouselArrow" onClick={nextSlide}>❯</button> </div> </div> <div className="mainLoyalBox"> <h3 className="loyalTitle">Loyalty Card</h3> <div className="loyaltyBox"> <div className="loyalLeft"> <h2 className="points">{user.userPoints || 400} Points</h2> <p className="loyalDesc">Points are added when you shop online</p> <div className="progressWrapper"> <h3 className="rewardText">Next Reward:</h3> <p className="progressText">1000 points: 50% Discount</p> <button className="continueShopBtn" onClick={() => navigate("/products")}> Continue Shopping </button> </div> </div> <div className="loyalRight"> <img src={Cow} alt="Loyalty Mascot" /> </div> </div> </div> </div> <div className="bottomSection"> <div className="banner"> <div className="bannerRight"> <div className="bannerRightTop"> <h2>Shop with our best deals</h2> </div> <div className="bannerRightBottom"> <img /> </div> </div> <div className="bannerLeft"></div> </div> </div> </div> ); } import { useState, useEffect } from "react"; import { products } from "../../data/products.js"; import { useNavigate } from "react-router-dom"; import "../profile/profile.css"; import ProductCard from "../../components/productCard/productCard.jsx"; import Cow from "../../assets/cow.svg"; export default function Profile() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [currentIndex, setCurrentIndex] = useState(0); const navigate = useNavigate(); const nextSlide = () => { setCurrentIndex((prev) => (prev + 2 >= products.length ? 0 : prev + 2)); }; const prevSlide = () => { setCurrentIndex((prev) => { if (prev === 0) { return products.length % 2 === 0 ? products.length - 2 : products.length - 1; } return prev - 2; }); }; const handleAddToBasket = (product) => { console.log("Added to basket:", product.name); }; useEffect(() => { const storedUserId = localStorage.getItem("userId"); if (!storedUserId) { setLoading(false); return; } fetch(`/api/user/profile?userId=${storedUserId}`) .then((res) => res.json()) .then((data) => { if (data.success) { setUser(data.user); } }) .catch((err) => console.error("Error:", err)) .finally(() => setLoading(false)); }, []); if (loading) return <p>Loading...</p>; if (!user) return <p>Please log in.</p>; return ( <div className="profilePage"> <div className="topSection"> <div className="welcomeBox"> <h2>Welcome {user.firstName}!</h2> <p className="welcomeDesc">Recommended products for you</p> <div className="carouselContainer"> <button className="carouselArrow" onClick={prevSlide}>❮</button> <div className="carouselWrapper" key={currentIndex}> {products.slice(currentIndex, currentIndex + 2).map((product) => ( <ProductCard key={product.id} product={product} onAdd={handleAddToBasket} /> ))} </div> <button className="carouselArrow" onClick={nextSlide}>❯</button> </div> </div> <div className="mainLoyalBox"> <h3 className="loyalTitle">Loyalty Card</h3> <div className="loyaltyBox"> <div className="loyalLeft"> <h2 className="points">{user.userPoints || 400} Points</h2> <p className="loyalDesc">Points are added when you shop online</p> <div className="progressWrapper"> <h3 className="rewardText">Next Reward:</h3> <p className="progressText">1000 points: 50% Discount</p> <button className="continueShopBtn" onClick={() => navigate("/products")}> Continue Shopping </button> </div> </div> <div className="loyalRight"> <img src={Cow} alt="Loyalty Mascot" /> </div> </div> </div> </div> <div className="bottomSection"> <div className="banner"> <div className="bannerRight"> <div className="bannerRightTop"> <h2>Shop with our best deals</h2> </div> <div className="bannerRightBottom"> <img /> </div> </div> <div className="bannerLeft"></div> </div> </div> </div> ); } import { useState, useEffect } from "react"; import { products } from "../../data/products.js"; import { useNavigate } from "react-router-dom"; import "../profile/profile.css"; import ProductCard from "../../components/productCard/productCard.jsx"; import Cow from "../../assets/cow.svg"; export default function Profile() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [currentIndex, setCurrentIndex] = useState(0); const navigate = useNavigate(); const nextSlide = () => { setCurrentIndex((prev) => (prev + 2 >= products.length ? 0 : prev + 2)); }; const prevSlide = () => { setCurrentIndex((prev) => { if (prev === 0) { return products.length % 2 === 0 ? products.length - 2 : products.length - 1; } return prev - 2; }); }; const handleAddToBasket = (product) => { console.log("Added to basket:", product.name); }; useEffect(() => { const storedUserId = localStorage.getItem("userId"); if (!storedUserId) { setLoading(false); return; } fetch(`/api/user/profile?userId=${storedUserId}`) .then((res) => res.json()) .then((data) => { if (data.success) { setUser(data.user); } }) .catch((err) => console.error("Error:", err)) .finally(() => setLoading(false)); }, []); if (loading) return <p>Loading...</p>; if (!user) return <p>Please log in.</p>; return ( <div className="profilePage"> <div className="topSection"> <div className="welcomeBox"> <h2>Welcome {user.firstName}!</h2> <p className="welcomeDesc">Recommended products for you</p> <div className="carouselContainer"> <button className="carouselArrow" onClick={prevSlide}>❮</button> <div className="carouselWrapper" key={currentIndex}> {products.slice(currentIndex, currentIndex + 2).map((product) => ( <ProductCard key={product.id} product={product} onAdd={handleAddToBasket} /> ))} </div> <button className="carouselArrow" onClick={nextSlide}>❯</button> </div> </div> <div className="mainLoyalBox"> <h3 className="loyalTitle">Loyalty Card</h3> <div className="loyaltyBox"> <div className="loyalLeft"> <h2 className="points">{user.userPoints || 400} Points</h2> <p className="loyalDesc">Points are added when you shop online</p> <div className="progressWrapper"> <h3 className="rewardText">Next Reward:</h3> <p className="progressText">1000 points: 50% Discount</p> <button className="continueShopBtn" onClick={() => navigate("/products")}> Continue Shopping </button> </div> </div> <div className="loyalRight"> <img src={Cow} alt="Loyalty Mascot" /> </div> </div> </div> </div> <div className="bottomSection"> <div className="banner"> <div className="bannerRight"> <div className="bannerRightTop"> <h2>Shop with our best deals</h2> </div> <div className="bannerRightBottom"> <img /> </div> </div> <div className="bannerLeft"></div> </div> </div> </div> ); } .profilePage{ display: flex; flex-direction: column; } .topSection{ display: flex; flex-direction: row; justify-content: space-evenly; margin:2rem 1rem; } .welcomeBox{ display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: var(--primary); border-radius: 15px; } .welcomeBox h2{ color: var(--text-h); font-size: 3rem; } .welcomeDesc{ margin:0px; font-size: 1.3rem; } .carouselContainer { display: flex; align-items: center; justify-content: center; gap: 15px; margin-top: 20px; padding: 20px; } .carouselArrow { background: var(--primary); color: var(--text); border: none; border-radius: 50%; width: 35px; height: 35px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; transition: background 0.2s; } .carouselArrow:hover { background: var(--accent); } .carouselWrapper { display: flex; gap: 20px; justify-content: center; align-items: flex-start; min-width: 450px; } @keyframes slideInRight { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } .carouselWrapper { animation: slideInRight 0.3s ease-in-out; } .mainLoyalBox{ display: flex; flex-direction: column; justify-content: center; align-items: center; } .loyalTitle{ font-size: 2rem; margin: 1rem; color: var(--text-h-2); } .loyaltyBox{ display: flex; flex-direction: row; background-color: var(--secondary); border-radius: 15px; padding: 20px; padding-right: 0px; } .points{ margin:20px; font-size: 3.5rem; color: var(--text-h-2); } .loyalDesc{ margin:20px; } .rewardText, .progressText{ font-size: 1.5rem; margin: 10px; } .continueShopBtn{ background-color: #D8EBED; color: var(--text-h-2); font-family: var(--main-font); font-size: 1.2rem; border: none; border-radius: 7px; padding:10px; margin-top: 1.5rem; margin-left: 8rem; } .loyalRight img{ width: 10rem; } .banner{ display: flex; align-items: center; justify-content: space-evenly; background-color: var(--accent); } .profilePage{ display: flex; flex-direction: column; } .topSection{ display: flex; flex-direction: row; justify-content: space-evenly; margin:2rem 1rem; } .welcomeBox{ display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: var(--primary); border-radius: 15px; } .welcomeBox h2{ color: var(--text-h); font-size: 3rem; } .welcomeDesc{ margin:0px; font-size: 1.3rem; } .carouselContainer { display: flex; align-items: center; justify-content: center; gap: 15px; margin-top: 20px; padding: 20px; } .carouselArrow { background: var(--primary); color: var(--text); border: none; border-radius: 50%; width: 35px; height: 35px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; transition: background 0.2s; } .carouselArrow:hover { background: var(--accent); } .carouselWrapper { display: flex; gap: 20px; justify-content: center; align-items: flex-start; min-width: 450px; } @keyframes slideInRight { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } .carouselWrapper { animation: slideInRight 0.3s ease-in-out; } .mainLoyalBox{ display: flex; flex-direction: column; justify-content: center; align-items: center; } .loyalTitle{ font-size: 2rem; margin: 1rem; color: var(--text-h-2); } .loyaltyBox{ display: flex; flex-direction: row; background-color: var(--secondary); border-radius: 15px; padding: 20px; padding-right: 0px; } .points{ margin:20px; font-size: 3.5rem; color: var(--text-h-2); } .loyalDesc{ margin:20px; } .rewardText, .progressText{ font-size: 1.5rem; margin: 10px; } .continueShopBtn{ background-color: #D8EBED; color: var(--text-h-2); font-family: var(--main-font); font-size: 1.2rem; border: none; border-radius: 7px; padding:10px; margin-top: 1.5rem; margin-left: 8rem; } .loyalRight img{ width: 10rem; } .banner{ display: flex; align-items: center; justify-content: space-evenly; background-color: var(--accent); } .profilePage{ display: flex; flex-direction: column; } .topSection{ display: flex; flex-direction: row; justify-content: space-evenly; margin:2rem 1rem; } .welcomeBox{ display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: var(--primary); border-radius: 15px; } .welcomeBox h2{ color: var(--text-h); font-size: 3rem; } .welcomeDesc{ margin:0px; font-size: 1.3rem; } .carouselContainer { display: flex; align-items: center; justify-content: center; gap: 15px; margin-top: 20px; padding: 20px; } .carouselArrow { background: var(--primary); color: var(--text); border: none; border-radius: 50%; width: 35px; height: 35px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; transition: background 0.2s; } .carouselArrow:hover { background: var(--accent); } .carouselWrapper { display: flex; gap: 20px; justify-content: center; align-items: flex-start; min-width: 450px; } @keyframes slideInRight { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } .carouselWrapper { animation: slideInRight 0.3s ease-in-out; } .mainLoyalBox{ display: flex; flex-direction: column; justify-content: center; align-items: center; } .loyalTitle{ font-size: 2rem; margin: 1rem; color: var(--text-h-2); } .loyaltyBox{ display: flex; flex-direction: row; background-color: var(--secondary); border-radius: 15px; padding: 20px; padding-right: 0px; } .points{ margin:20px; font-size: 3.5rem; color: var(--text-h-2); } .loyalDesc{ margin:20px; } .rewardText, .progressText{ font-size: 1.5rem; margin: 10px; } .continueShopBtn{ background-color: #D8EBED; color: var(--text-h-2); font-family: var(--main-font); font-size: 1.2rem; border: none; border-radius: 7px; padding:10px; margin-top: 1.5rem; margin-left: 8rem; } .loyalRight img{ width: 10rem; } .banner{ display: flex; align-items: center; justify-content: space-evenly; background-color: var(--accent); } - Node.js and npm installed - Python 3 and pip installed - MySQL installed and running - Basic knowledge of React and Python - Uses a pattern with e.preventDefault() to handle submission. - All validation (password length, matching passwords, duplicate email) is handled server-side in Flask and returned as error messages. - On success, userId is saved to localStorage and the user is redirected to /profile. - localStorage.getItem("userId") retrieves the stored ID on mount. - The useEffect runs once on mount ([] dependency array) and fetches the user's profile. - loading state prevents rendering before the fetch completes. - user.userPoints || 400 displays a default of 400 points if the field is not yet in the database. - The carousel uses currentIndex to slice 2 products at a time from the products array.