diff --git a/.gitignore b/.gitignore index cee315cfa872af4cd8fc6738c45225cf74941ecc..5085a4c71728c5a6683f081738f5fa8cd100b83e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ __pycache__ # ignore all files in the static folder except the default folder app/static/* -!app/static/default/ \ No newline at end of file +!app/static/default/ +!app/static/front_end_img/ \ No newline at end of file diff --git a/app/backend/main.py b/app/backend/main.py index 658f0cadb39adc6136f2dbdb2bd3ab70be22cf8e..9606ccc22b982a4ee3f358b37390df63fc26d397 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -4,13 +4,20 @@ import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles from backend.routes import auth, shop, product, category, search, order from backend.database import init_db from core.config import settings app = FastAPI(title="Shopping App", version="1.0.0", debug=settings.debug) -# initialize database +# ------------------- NEW: MOUNT STATIC FILES ------------------- +# Suppose your static files are located in "app/static" +# Adjust the path if needed. +static_dir_path = os.path.join(os.path.dirname(__file__), "..", "static") +app.mount("/static", StaticFiles(directory=static_dir_path), name="static") + +# Initialize database init_db() # Include API routes diff --git a/app/backend/routes/product.py b/app/backend/routes/product.py index fcad951db6c8c2d2cf4b7727a2c555599cb95b6b..30ca0e9f308fcd5d627ec95dbcaa7db6407bba75 100644 --- a/app/backend/routes/product.py +++ b/app/backend/routes/product.py @@ -11,9 +11,7 @@ import os router = APIRouter() -static_dir = os.path.join("app", "static") -os.makedirs(static_dir, exist_ok=True) - +BACKEND_HOST = "http://127.0.0.1:8000" # Or use settings.backend_url if available @router.post("/create", response_model=ProductRead) def create_product( @@ -27,6 +25,12 @@ def create_product( session: Session = Depends(get_session), current_user: User = Depends(get_current_user), ): + shop = session.get(Shop, shop_id) + if not shop or shop.owner_id != current_user.id: + raise HTTPException( + status_code=403, detail="Unauthorized to create product for this shop" + ) + product = Product( name=name, description=description, @@ -40,35 +44,31 @@ def create_product( session.commit() session.refresh(product) - shop = session.get(Shop, shop_id) - if not shop or shop.owner_id != current_user.id: - raise HTTPException( - status_code=403, detail="Unauthorized to create product for this shop" - ) - - # Directory structure: static/shop_{shop_name}/product_{product_name}/ + # Directory: static/shop_{shop.name}/product_{product.name}/ shop_dir = os.path.join(settings.static_dir, f"shop_{shop.name}") os.makedirs(shop_dir, exist_ok=True) product_dir = os.path.join(shop_dir, f"product_{product.name}") os.makedirs(product_dir, exist_ok=True) - # Handling multiple image uploads if images: for image in images: if image.filename: file_location = os.path.join(product_dir, image.filename) with open(file_location, "wb") as buffer: shutil.copyfileobj(image.file, buffer) - - # Save image record in ProductImage - product_image = ProductImage( - product_id=product.id, image_url=file_location - ) + relative_path = os.path.relpath(file_location, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + product_image = ProductImage(product_id=product.id, image_url=public_url) session.add(product_image) session.commit() - else: # Save default image - file_location = os.path.join(settings.static_dir, "default/default_product.png") - product_image = ProductImage(product_id=product.id, image_url=file_location) + else: + # Use default product image from static/default folder + default_path = os.path.join(settings.static_dir, "default", "default_product.png") + relative_path = os.path.relpath(default_path, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + product_image = ProductImage(product_id=product.id, image_url=public_url) session.add(product_image) session.commit() @@ -82,7 +82,6 @@ def read_all_products(order: str = "desc", session: Session = Depends(get_sessio if order == "desc" else func.count(OrderItem.id).asc() ) - products = ( session.query(Product) .outerjoin(OrderItem, Product.id == OrderItem.product_id) @@ -90,7 +89,6 @@ def read_all_products(order: str = "desc", session: Session = Depends(get_sessio .order_by(order_by) .all() ) - return products @@ -120,28 +118,16 @@ def update_product( status_code=403, detail="Unauthorized to update this product" ) - if name: - product.name = name - if description: - product.description = description - if price is not None: - product.price = price - if stock is not None: - product.stock = stock - if category_id is not None: - product.category_id = category_id + product.name = name + product.description = description + product.price = price + product.stock = stock + product.category_id = category_id session.add(product) session.commit() session.refresh(product) - product = session.get(Product, product_id) - if not product or product.shop.owner_id != current_user.id: - raise HTTPException( - status_code=403, detail="Unauthorized to update this product" - ) - - # Directory structure: static/shop_{shop_name}/product_{product_name}/ shop = session.get(Shop, product.shop_id) shop_dir = os.path.join(settings.static_dir, f"shop_{shop.name}") os.makedirs(shop_dir, exist_ok=True) @@ -154,8 +140,10 @@ def update_product( file_location = os.path.join(product_dir, file.filename) with open(file_location, "wb") as buffer: shutil.copyfileobj(file.file, buffer) - - image = ProductImage(product_id=product.id, image_url=file_location) + relative_path = os.path.relpath(file_location, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + image = ProductImage(product_id=product.id, image_url=public_url) session.add(image) session.commit() @@ -173,7 +161,6 @@ def delete_product( raise HTTPException( status_code=403, detail="Unauthorized to delete this product" ) - session.delete(product) session.commit() return {"message": "Product deleted successfully"} diff --git a/app/backend/routes/shop.py b/app/backend/routes/shop.py index a240d1be12ee0774988de102a631da60042acbc5..d5a651ae41d8c67d3f84b711e2e270707f83e5a7 100644 --- a/app/backend/routes/shop.py +++ b/app/backend/routes/shop.py @@ -11,6 +11,8 @@ import os router = APIRouter() +# Define your backend host URL. +BACKEND_HOST = "http://127.0.0.1:8000" # Or use settings.backend_url if available @router.post("/create", response_model=ShopRead) def create_shop( @@ -39,6 +41,7 @@ def create_shop( session.commit() session.refresh(shop) + # Create a folder in the static directory for the shop shop_dir = os.path.join(settings.static_dir, f"shop_{shop.name}") os.makedirs(shop_dir, exist_ok=True) @@ -46,13 +49,20 @@ def create_shop( file_location = os.path.join(shop_dir, file.filename) with open(file_location, "wb") as buffer: shutil.copyfileobj(file.file, buffer) - shop.image_url = file_location + relative_path = os.path.relpath(file_location, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + shop.image_url = public_url else: - shop.image_url = os.path.join(settings.static_dir, "default/default_shop.png") + # Use default shop image from static/default folder + default_path = os.path.join(settings.static_dir, "default", "default_shop.png") + relative_path = os.path.relpath(default_path, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + shop.image_url = public_url session.commit() session.refresh(shop) - return shop @@ -61,7 +71,6 @@ def get_all_shops(order: str = "desc", session: Session = Depends(get_session)): order_by = ( func.count(Order.id).desc() if order == "desc" else func.count(Order.id).asc() ) - shops = ( session.query(Shop) .outerjoin(Order, Shop.id == Order.shop_id) @@ -69,7 +78,6 @@ def get_all_shops(order: str = "desc", session: Session = Depends(get_session)): .order_by(order_by) .all() ) - return shops @@ -94,10 +102,9 @@ def update_shop( shop = session.get(Shop, shop_id) if not shop: raise HTTPException(status_code=404, detail="Shop not found") - - # Ensure the current user is the shop owner if shop.owner_id != current_user.id: raise HTTPException(status_code=403, detail="Unauthorized to update this shop") + if name: shop.name = name if description: @@ -118,14 +125,20 @@ def update_shop( file_location = os.path.join(shop_dir, file.filename) with open(file_location, "wb") as buffer: shutil.copyfileobj(file.file, buffer) - shop.image_url = file_location + relative_path = os.path.relpath(file_location, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + shop.image_url = public_url else: - shop.image_url = os.path.join(settings.static_dir, "default/default_shop.png") + default_path = os.path.join(settings.static_dir, "default", "default_shop.png") + relative_path = os.path.relpath(default_path, settings.static_dir) + relative_path = relative_path.replace("\\", "/") + public_url = f"{BACKEND_HOST}/static/{relative_path}" + shop.image_url = public_url session.add(shop) session.commit() session.refresh(shop) - return shop @@ -138,11 +151,8 @@ def delete_shop( shop = session.get(Shop, shop_id) if not shop: raise HTTPException(status_code=404, detail="Shop not found") - - # Ensure the current user is the shop owner if shop.owner_id != current_user.id: raise HTTPException(status_code=403, detail="Unauthorized to delete this shop") - session.delete(shop) session.commit() return {"message": "Shop deleted successfully"} diff --git a/app/frontend/components/auth/login.py b/app/frontend/components/auth/login.py index 87b5082ff5682aa992a9ce58138dc956d55d6485..31d2560c45f5d7f2890cbcdc6d35b9c98eed19a4 100644 --- a/app/frontend/components/auth/login.py +++ b/app/frontend/components/auth/login.py @@ -1,10 +1,31 @@ import customtkinter as ctk from tkinter import messagebox -from utils.api_requests import login_api # Import the login function from login_api.py +from PIL import Image +from utils.api_requests import login_api + def login_frame(parent, switch_func, API_URL): - frame = ctk.CTkFrame(parent) + # Overall container + container = ctk.CTkFrame(parent, fg_color="#F3F4F6") # Light gray background + container.grid_columnconfigure(0, weight=1) + container.grid_columnconfigure(1, weight=1) + container.grid_rowconfigure(0, weight=1) + + # Left image frame + left_frame = ctk.CTkFrame(container, fg_color="transparent") + left_frame.grid(row=0, column=0, sticky="nsew") + + image_path = "../../app/static/front_end_img/login.jpg" + img = ctk.CTkImage(light_image=Image.open(image_path), size=(800, 800)) + image_label = ctk.CTkLabel(left_frame, image=img, text="") + image_label.place(relwidth=1, relheight=1) + # Right login frame + right_frame = ctk.CTkFrame(container, fg_color="white", corner_radius=20) + right_frame.grid(row=0, column=1, sticky="nsew", padx=50, pady=100) + right_frame.grid_columnconfigure(0, weight=1) + + # Login Function def login(): email = entry_email.get() password = entry_password.get() @@ -13,33 +34,61 @@ def login_frame(parent, switch_func, API_URL): messagebox.showwarning("Input Error", "Both fields are required!") return - # Call the API function from login_api.py status_code, response_data = login_api(email, password, API_URL) if status_code == 200: access_token = response_data.get("access_token") messagebox.showinfo("Login Successful", f"Welcome back, {email}!") - switch_func("view_shop", access_token) - print(f"Access Token in login: {access_token}") # Debugging line + switch_func("dashboard", access_token) else: messagebox.showerror("Login Failed", response_data.get("detail", "Invalid credentials")) - ctk.CTkLabel(frame, text="Login", font=("Helvetica", 18, "bold")).pack(pady=10) + # Title + ctk.CTkLabel( + right_frame, + text="Login", + font=ctk.CTkFont("Helvetica", size=26, weight="bold"), + text_color="#111827" + ).pack(pady=(20, 10)) - ctk.CTkLabel(frame, text="Email:").pack(pady=5) - entry_email = ctk.CTkEntry(frame) - entry_email.pack(pady=5) + # Email + ctk.CTkLabel( + right_frame, text="Email", font=("Helvetica", 14), text_color="#374151" + ).pack(pady=(10, 5)) + entry_email = ctk.CTkEntry( + right_frame, height=40, corner_radius=10, placeholder_text="Enter your email" + ) + entry_email.pack(pady=5, padx=20, fill="x") - ctk.CTkLabel(frame, text="Password:").pack(pady=5) - entry_password = ctk.CTkEntry(frame, show="*") - entry_password.pack(pady=5) + # Password + ctk.CTkLabel( + right_frame, text="Password", font=("Helvetica", 14), text_color="#374151" + ).pack(pady=(10, 5)) + entry_password = ctk.CTkEntry( + right_frame, height=40, show="*", corner_radius=10, placeholder_text="Enter your password" + ) + entry_password.pack(pady=5, padx=20, fill="x") - ctk.CTkButton(frame, text="Login", command=login).pack(pady=15) + # Login Button + ctk.CTkButton( + right_frame, + text="Login", + command=login, + corner_radius=10, + height=45, + font=("Helvetica", 14), + fg_color="#2563EB", + hover_color="#1E40AF" + ).pack(pady=20, padx=20, fill="x") + # Register Redirect ctk.CTkButton( - frame, + right_frame, text="Don't have an account? Register", command=lambda: switch_func("register"), - ).pack() + fg_color="transparent", + hover_color="#E5E7EB", + text_color="#2563EB" + ).pack(pady=(5, 20)) - return frame + return container diff --git a/app/frontend/components/auth/register.py b/app/frontend/components/auth/register.py index 01b1b75b4b65ee43b33c2bbea1faea42cfbcfb01..d2a635345377808e657efeeb110b660141a209e0 100644 --- a/app/frontend/components/auth/register.py +++ b/app/frontend/components/auth/register.py @@ -2,8 +2,16 @@ import customtkinter as ctk from tkinter import messagebox from utils.api_requests import register_api # Import the API function + def register_frame(parent, switch_func, API_URL): - frame = ctk.CTkFrame(parent) + # Create a full-window container with a light gray background + container = ctk.CTkFrame(parent, fg_color="#F3F4F6") + container.pack(expand=True, fill="both") + + # Center the register panel (the white card) in the middle of the container. + # Width and height are set to ensure a consistent size. + register_panel = ctk.CTkFrame(container, fg_color="white", corner_radius=20, width=400, height=500) + register_panel.place(relx=0.5, rely=0.5, anchor="center") def register(): username = entry_username.get() @@ -21,7 +29,9 @@ def register_frame(parent, switch_func, API_URL): return # Call the API function from register_api.py - status_code, response_data = register_api(username, email, phone_number, password, API_URL) + status_code, response_data = register_api( + username, email, phone_number, password, API_URL + ) if status_code == 200: messagebox.showinfo("Registration Successful", f"Welcome, {username}!") @@ -29,34 +39,64 @@ def register_frame(parent, switch_func, API_URL): else: messagebox.showerror("Registration Failed", response_data.get("detail", "Unknown error")) - ctk.CTkLabel(frame, text="Register", font=("Helvetica", 18, "bold")).pack(pady=10) + # Title for the registration form + ctk.CTkLabel( + register_panel, + text="Create Account", + font=ctk.CTkFont("Helvetica", size=26, weight="bold"), + text_color="#111827" + ).pack(pady=(20, 10)) - ctk.CTkLabel(frame, text="Username:").pack(pady=5) - entry_username = ctk.CTkEntry(frame) - entry_username.pack(pady=5) + # Username Entry + entry_username = ctk.CTkEntry( + register_panel, placeholder_text="Username", height=40, corner_radius=10, width=250 + ) + entry_username.pack(pady=10, padx=20, fill="x") - ctk.CTkLabel(frame, text="Email:").pack(pady=5) - entry_email = ctk.CTkEntry(frame) - entry_email.pack(pady=5) + # Email Entry + entry_email = ctk.CTkEntry( + register_panel, placeholder_text="Email", height=40, corner_radius=10, width=250 + ) + entry_email.pack(pady=10, padx=20, fill="x") - ctk.CTkLabel(frame, text="Phone Number:").pack(pady=5) - entry_phone = ctk.CTkEntry(frame) - entry_phone.pack(pady=5) + # Phone Number Entry + entry_phone = ctk.CTkEntry( + register_panel, placeholder_text="Phone Number", height=40, corner_radius=10, width=250 + ) + entry_phone.pack(pady=10, padx=20, fill="x") - ctk.CTkLabel(frame, text="Password:").pack(pady=5) - entry_password = ctk.CTkEntry(frame, show="*") - entry_password.pack(pady=5) + # Password Entry + entry_password = ctk.CTkEntry( + register_panel, placeholder_text="Password", show="*", height=40, corner_radius=10, width=250 + ) + entry_password.pack(pady=10, padx=20, fill="x") - ctk.CTkLabel(frame, text="Confirm Password:").pack(pady=5) - entry_confirm_password = ctk.CTkEntry(frame, show="*") - entry_confirm_password.pack(pady=5) + # Confirm Password Entry + entry_confirm_password = ctk.CTkEntry( + register_panel, placeholder_text="Confirm Password", show="*", height=40, corner_radius=10, width=250 + ) + entry_confirm_password.pack(pady=10, padx=20, fill="x") - ctk.CTkButton(frame, text="Register", command=register).pack(pady=15) + # Register Button + ctk.CTkButton( + register_panel, + text="Register", + command=register, + corner_radius=10, + height=45, + font=("Helvetica", 14), + fg_color="#2563EB", + hover_color="#1E40AF" + ).pack(pady=20, padx=20, fill="x") + # Link to Login Page ctk.CTkButton( - frame, + register_panel, text="Already have an account? Login", command=lambda: switch_func("login"), - ).pack() + fg_color="transparent", + hover_color="#E5E7EB", + text_color="#2563EB" + ).pack(pady=(5, 20)) - return frame + return container diff --git a/app/frontend/components/dashboard.py b/app/frontend/components/dashboard.py index 32a128a096bba1ff38b15fd47f128b03212bfadc..f5707c926cb655e12186dcfcccba5ebafc3a82b6 100644 --- a/app/frontend/components/dashboard.py +++ b/app/frontend/components/dashboard.py @@ -5,10 +5,29 @@ from tkinter import messagebox import io SHOPPING = "#00c1ff" +BACKEND_HOST = "http://127.0.0.1:8000" # Adjust if needed + +def fix_url(url): + """ + If the provided URL does not start with http, assume it's a relative path. + Remove any unwanted prefix (e.g., "app/static/") and prepend the public URL. + """ + if url.startswith("http"): + return url + # If the URL starts with "app/static/", remove that part. + prefix = "app/static/" + if url.startswith(prefix): + url = url[len(prefix):] + # Prepend the public URL + return f"{BACKEND_HOST}/static/{url}" def dashboard_frame(parent, switch_func, API_URL, token): - - # Main container frame + """ + Main dashboard UI: + - Header (top bar) with logo, search, user, cart + - Middle area with horizontal scroll of top shops + - Bottom area with grid layout of recommended products + """ frame = ctk.CTkFrame(parent, fg_color="transparent") # ------------- HEADER (Top Bar) ------------- @@ -20,15 +39,13 @@ def dashboard_frame(parent, switch_func, API_URL, token): header_frame, text="Shopping App", text_color="white", - font=("Helvetica", 20, "bold") + font=("Helvetica", 20, "bold"), ) logo_label.place(relx=0.01, rely=0.1, relwidth=0.15, relheight=0.8) # Search bar (Center) search_entry = ctk.CTkEntry( - header_frame, - placeholder_text="Search in Shop...", - height=30 + header_frame, placeholder_text="Search in Shop...", height=30 ) search_entry.place(relx=0.25, rely=0.25, relwidth=0.45, relheight=0.5) @@ -40,161 +57,208 @@ def dashboard_frame(parent, switch_func, API_URL, token): return try: headers = {"Authorization": f"Bearer {token}"} - resp = requests.get(f"{API_URL}/product?search={keyword}", headers=headers) + # Adjust endpoint/params as needed + resp = requests.get(f"{API_URL}/search?name={keyword}&search_type=products", headers=headers) if resp.status_code == 200: products = resp.json() display_products(products, bottom_products_frame) else: - messagebox.showerror("Error", "Failed to fetch search results.") + messagebox.showerror("Error", f"Search failed. Status code: {resp.status_code}") except Exception as e: messagebox.showerror("Error", f"Request error: {e}") - # Search button search_button = ctk.CTkButton( header_frame, text="Search", fg_color="white", text_color="black", - command=perform_search + command=perform_search, ) search_button.place(relx=0.71, rely=0.25, relwidth=0.08, relheight=0.5) + # ------------- USER & CART ICONS (Top-Right) ------------- + def open_user_details(): + switch_func("user_details") + + def open_cart_details(): + switch_func("user_orders") + + user_button = ctk.CTkButton( + header_frame, + text="User", + fg_color="white", + text_color="black", + command=open_user_details, + ) + user_button.place(relx=0.82, rely=0.25, relwidth=0.08, relheight=0.5) + + cart_button = ctk.CTkButton( + header_frame, + text="Cart", + fg_color="white", + text_color="black", + command=open_cart_details, + ) + cart_button.place(relx=0.91, rely=0.25, relwidth=0.08, relheight=0.5) + # ------------- MIDDLE (Featured/Top Shops) ------------- middle_frame = ctk.CTkFrame(frame, fg_color="transparent") middle_frame.place(relx=0, rely=0.1, relwidth=1, relheight=0.25) - # Section Title featured_label = ctk.CTkLabel( - middle_frame, - text="TOP SHOP", - font=("Helvetica", 16, "bold") + middle_frame, text="TOP SHOP", font=("Helvetica", 16, "bold") ) featured_label.pack(pady=5) - # A frame to hold the featured shops - top_shops_frame = ctk.CTkFrame(middle_frame, fg_color="transparent") + top_shops_frame = ctk.CTkScrollableFrame( + middle_frame, fg_color="transparent", width=750, height=150 + ) top_shops_frame.pack(fill="both", expand=True, padx=10, pady=5) - + + def create_shop_card(parent, shop_data): + """ + Create a card for a shop with its image on top and name below. + """ + card = ctk.CTkFrame(parent, corner_radius=5, fg_color="#2b2b2b", width=100, height=130) + card.pack_propagate(False) + + image_label = ctk.CTkLabel(card, text="") + image_label.pack(pady=5) + + # Use "image_url" as returned by the backend, then fix it if needed. + image_url = shop_data.get("image_url") + if image_url: + fixed_url = fix_url(image_url) + print(f"[DEBUG] Fetching shop image from: {fixed_url}") + try: + resp = requests.get(fixed_url) + print(f"[DEBUG] Status code for shop image: {resp.status_code}") + if resp.status_code == 200: + pil_img = Image.open(io.BytesIO(resp.content)).resize((60, 60)) + tk_img = ImageTk.PhotoImage(pil_img) + image_label.configure(image=tk_img, text="") + image_label.image = tk_img + else: + print(f"[DEBUG] Failed to fetch shop image. Status: {resp.status_code}") + except Exception as e: + print(f"[DEBUG] Shop image error: {e}") + else: + print(f"[DEBUG] No 'image_url' found for shop: {shop_data.get('name')}") + + name_text = shop_data.get("name", "No Name") + name_label = ctk.CTkLabel(card, text=name_text, font=("Helvetica", 11, "bold"), wraplength=90) + name_label.pack() + + return card + def display_shops(shops, container): - """Given a list of shop dicts, display them in the given container.""" - # Clear old widgets for widget in container.winfo_children(): widget.destroy() - + if not shops: ctk.CTkLabel(container, text="No shops found.").pack(pady=10) return - - # Display shops in a vertical list - for shop in shops: - sframe = ctk.CTkFrame(container, corner_radius=5, fg_color="#2b2b2b") - sframe.pack(fill="x", padx=5, pady=5) - # Shop image/logo on left - image_label = ctk.CTkLabel(sframe, text="") - image_label.pack(side="left", padx=5, pady=5) + inner_frame = ctk.CTkFrame(container, fg_color="transparent") + inner_frame.pack(side="top", fill="both", expand=True) - # Load shop logo if available (assumes key "logo_url") - logo_url = shop.get("logo_url") - if logo_url: - try: - resp = requests.get(logo_url) - if resp.status_code == 200: - pil_img = Image.open(io.BytesIO(resp.content)).resize((50, 50)) - tk_img = ImageTk.PhotoImage(pil_img) - image_label.configure(image=tk_img, text="") - image_label.image = tk_img - except Exception as e: - print(f"Shop image error: {e}") - - # Shop info on right - info_frame = ctk.CTkFrame(sframe, fg_color="transparent") - info_frame.pack(side="left", fill="both", expand=True, padx=10) - - ctk.CTkLabel(info_frame, text=shop.get("name", "No Name"), font=("Helvetica", 13, "bold")).pack(anchor="w") - # Optionally add more shop details, for example a rating if available: - if shop.get("rating"): - ctk.CTkLabel(info_frame, text=f"Rating: {shop['rating']}").pack(anchor="w") + for shop in shops: + shop_card = create_shop_card(inner_frame, shop) + shop_card.pack(side="left", padx=5, pady=5) - # ------------- BOTTOM (Recommendations - Products) ------------- + # ------------- BOTTOM (Products) ------------- bottom_frame = ctk.CTkFrame(frame, fg_color="transparent") bottom_frame.place(relx=0, rely=0.35, relwidth=1, relheight=0.65) - # Section Title recommend_label = ctk.CTkLabel( - bottom_frame, - text="TODAY'S RECOMMENDATIONS", - font=("Helvetica", 16, "bold") + bottom_frame, text="TODAY'S RECOMMENDATIONS", font=("Helvetica", 16, "bold") ) recommend_label.pack(pady=5) - bottom_products_frame = ctk.CTkFrame(bottom_frame, fg_color="transparent") + bottom_products_frame = ctk.CTkScrollableFrame( + bottom_frame, fg_color="transparent", width=750, height=300 + ) bottom_products_frame.pack(fill="both", expand=True, padx=10, pady=5) - def display_products(products, container): - """Given a list of product dicts, display them in the given container.""" - # Clear old widgets - for widget in container.winfo_children(): - widget.destroy() - - if not products: - ctk.CTkLabel(container, text="No products found.").pack(pady=10) - return - - # Display products in a vertical list - for product in products: - pframe = ctk.CTkFrame(container, corner_radius=5, fg_color="#2b2b2b") - pframe.pack(fill="x", padx=5, pady=5) + def create_product_card(parent, product_data): + card_width = 130 + card_height = 210 + card = ctk.CTkFrame(parent, corner_radius=5, fg_color="#2b2b2b", width=card_width, height=card_height) + card.pack_propagate(False) - # Product image on left - image_label = ctk.CTkLabel(pframe, text="") - image_label.pack(side="left", padx=5, pady=5) + image_label = ctk.CTkLabel(card, text="") + image_label.pack(pady=5) - # Load first image if available - if product.get("images"): + product_images = product_data.get("images", []) + if product_images: + img_url = product_images[0].get("image_url") + if img_url: + fixed_url = fix_url(img_url) + print(f"[DEBUG] Fetching product image from: {fixed_url}") try: - img_url = product["images"][0]["image_url"] - resp = requests.get(img_url) + resp = requests.get(fixed_url) + print(f"[DEBUG] Status code for product image: {resp.status_code}") if resp.status_code == 200: - pil_img = Image.open(io.BytesIO(resp.content)).resize((50, 50)) + pil_img = Image.open(io.BytesIO(resp.content)).resize((70, 70)) tk_img = ImageTk.PhotoImage(pil_img) image_label.configure(image=tk_img, text="") image_label.image = tk_img + else: + print(f"[DEBUG] Failed to fetch product image. Status: {resp.status_code}") except Exception as e: - print(f"Product image error: {e}") + print(f"[DEBUG] Product image error: {e}") + else: + print(f"[DEBUG] No images found for product: {product_data.get('name')}") + + name_text = product_data.get("name", "No Name") + name_label = ctk.CTkLabel(card, text=name_text, font=("Helvetica", 11, "bold"), wraplength=120, anchor="center", justify="center") + name_label.pack(padx=5, pady=3) - # Product info on right - info_frame = ctk.CTkFrame(pframe, fg_color="transparent") - info_frame.pack(side="left", fill="both", expand=True, padx=10) + price_val = product_data.get("price", 0.0) + price_label = ctk.CTkLabel(card, text=f"â‚« {price_val:,.1f}", font=("Helvetica", 10, "bold"), text_color="#ff4242") + price_label.pack(side="bottom", pady=5) - ctk.CTkLabel(info_frame, text=product.get("name", "No Name"), font=("Helvetica", 13, "bold")).pack(anchor="w") - price = product.get("price", 0.0) - ctk.CTkLabel(info_frame, text=f"Price: {price:.2f}").pack(anchor="w") + return card + + def display_products(products, container): + for widget in container.winfo_children(): + widget.destroy() + + if not products: + ctk.CTkLabel(container, text="No products found.").pack(pady=10) + return + + grid_frame = ctk.CTkFrame(container, fg_color="transparent") + grid_frame.pack(fill="both", expand=True) + + columns = 7 # 7 products per row + for idx, product in enumerate(products): + row = idx // columns + col = idx % columns + product_card = create_product_card(grid_frame, product) + product_card.grid(row=row, column=col, padx=5, pady=5) def fetch_featured_shops(): - """Fetch some 'featured' or 'top' shops for the middle section.""" headers = {"Authorization": f"Bearer {token}"} try: resp = requests.get(f"{API_URL}/shops/list", headers=headers) if resp.status_code == 200: shops = resp.json() - # Slice the list to display only a handful of top shops display_shops(shops[:5], top_shops_frame) else: - messagebox.showerror("Error", "Failed to fetch featured shops.") + messagebox.showerror("Error", f"Failed to fetch featured shops. Status: {resp.status_code}") except Exception as e: messagebox.showerror("Error", f"Request error: {e}") def fetch_recommendations(): - """Fetch recommended products for the bottom section.""" headers = {"Authorization": f"Bearer {token}"} try: resp = requests.get(f"{API_URL}/product/list", headers=headers) if resp.status_code == 200: products = resp.json() - display_products(products, bottom_products_frame) + display_products(products[:20], bottom_products_frame) else: - messagebox.showerror("Error", "Failed to fetch recommended products.") + messagebox.showerror("Error", f"Failed to fetch recommended products. Status: {resp.status_code}") except Exception as e: messagebox.showerror("Error", f"Request error: {e}") diff --git a/app/frontend/components/shop/create_shop.py b/app/frontend/components/shop/create_shop.py index ed6ba4bbee4ba60f58f4a2ad08e6289a82caec60..e73fb5b2ab57b1e15f944f00fec33be8cfbc9644 100644 --- a/app/frontend/components/shop/create_shop.py +++ b/app/frontend/components/shop/create_shop.py @@ -3,6 +3,7 @@ from tkinter import messagebox, filedialog import os from utils.api_requests import create_shop_api # Import API function + def create_shop_frame(parent, switch_func, API_URL, token): frame = ctk.CTkFrame(parent) frame.access_token = token @@ -39,9 +40,13 @@ def create_shop_frame(parent, switch_func, API_URL, token): if status_code == 200: messagebox.showinfo("Shop Created", f"Shop '{name}' created successfully!") else: - messagebox.showerror("Error", response_data.get("detail", "An error occurred")) + messagebox.showerror( + "Error", response_data.get("detail", "An error occurred") + ) - ctk.CTkLabel(frame, text="Create Shop", font=("Helvetica", 18, "bold")).pack(pady=10) + ctk.CTkLabel(frame, text="Create Shop", font=("Helvetica", 18, "bold")).pack( + pady=10 + ) ctk.CTkLabel(frame, text="Shop Name:").pack(pady=5) entry_name = ctk.CTkEntry(frame, placeholder_text="Enter shop name") @@ -55,7 +60,9 @@ def create_shop_frame(parent, switch_func, API_URL, token): file_label = ctk.CTkLabel(frame, text="No file selected") file_label.pack(pady=5) - ctk.CTkButton(frame, text="Create Shop", fg_color="green", command=create_shop).pack(pady=15) + ctk.CTkButton( + frame, text="Create Shop", fg_color="green", command=create_shop + ).pack(pady=15) ctk.CTkButton( frame, text="Back", fg_color="transparent", command=lambda: switch_func("login") diff --git a/app/frontend/components/shop/view_shop.py b/app/frontend/components/shop/view_shop.py index 2c106654d7517b5a7d2942b279a80e0331456f33..3c4ae23dd957c0efca7bff9f95d27f4e84a68e22 100644 --- a/app/frontend/components/shop/view_shop.py +++ b/app/frontend/components/shop/view_shop.py @@ -1,7 +1,12 @@ import customtkinter as ctk from tkinter import messagebox from PIL import ImageTk -from utils.api_requests import fetch_shop_details, fetch_shop_products, load_image_from_url +from utils.api_requests import ( + fetch_shop_details, + fetch_shop_products, + load_image_from_url, +) + def view_shop_frame(parent, switch_func, API_URL, token): frame = ctk.CTkFrame(parent) @@ -11,10 +16,14 @@ def view_shop_frame(parent, switch_func, API_URL, token): title_label.pack(pady=10) # Shop Details - shop_name_label = ctk.CTkLabel(frame, text="Shop Name: ", font=("Helvetica", 14, "bold")) + shop_name_label = ctk.CTkLabel( + frame, text="Shop Name: ", font=("Helvetica", 14, "bold") + ) shop_name_label.pack(pady=5) - shop_description_label = ctk.CTkLabel(frame, text="Description: ", font=("Helvetica", 12)) + shop_description_label = ctk.CTkLabel( + frame, text="Description: ", font=("Helvetica", 12) + ) shop_description_label.pack(pady=5) shop_image_label = ctk.CTkLabel(frame, text="") # Placeholder for shop image @@ -33,7 +42,9 @@ def view_shop_frame(parent, switch_func, API_URL, token): return shop_name_label.configure(text=f"Shop Name: {shop_data['name']}") - shop_description_label.configure(text=f"Description: {shop_data.get('description', 'No description')}") + shop_description_label.configure( + text=f"Description: {shop_data.get('description', 'No description')}" + ) if "image_url" in shop_data and shop_data["image_url"]: image = load_image_from_url(shop_data["image_url"]) @@ -56,7 +67,9 @@ def view_shop_frame(parent, switch_func, API_URL, token): widget.destroy() if not products: - ctk.CTkLabel(product_list_frame, text="No products found.", font=("Helvetica", 12)).pack(pady=10) + ctk.CTkLabel( + product_list_frame, text="No products found.", font=("Helvetica", 12) + ).pack(pady=10) return for product in products: @@ -79,15 +92,25 @@ def view_shop_frame(parent, switch_func, API_URL, token): details_frame = ctk.CTkFrame(product_frame) details_frame.pack(side="left", fill="x", expand=True, padx=10) - ctk.CTkLabel(details_frame, text=product["name"], font=("Helvetica", 12, "bold")).pack(anchor="w") - ctk.CTkLabel(details_frame, text=f"Price: ${product['price']:.2f}", font=("Helvetica", 12)).pack(anchor="w") + ctk.CTkLabel( + details_frame, text=product["name"], font=("Helvetica", 12, "bold") + ).pack(anchor="w") + ctk.CTkLabel( + details_frame, + text=f"Price: ${product['price']:.2f}", + font=("Helvetica", 12), + ).pack(anchor="w") # Refresh Button - refresh_button = ctk.CTkButton(frame, text="Refresh", command=fetch_and_display_shop) + refresh_button = ctk.CTkButton( + frame, text="Refresh", command=fetch_and_display_shop + ) refresh_button.pack(pady=10) # Back Button - back_button = ctk.CTkButton(frame, text="Back", command=lambda: switch_func("login")) + back_button = ctk.CTkButton( + frame, text="Back", command=lambda: switch_func("login") + ) back_button.pack(pady=10) # Fetch shop data on load diff --git a/app/frontend/components/user_details.py b/app/frontend/components/user_details.py new file mode 100644 index 0000000000000000000000000000000000000000..a6b1a8fa0cee85e51d4b7b44e45a71d71e3a4cf5 --- /dev/null +++ b/app/frontend/components/user_details.py @@ -0,0 +1,287 @@ +import customtkinter as ctk +import requests +from PIL import Image, ImageTk +from tkinter import messagebox, filedialog +import io + +SHOPPING = "#00c1ff" + +def user_details_frame(parent, switch_func, API_URL, token): + """ + A two-column user details page that matches the dashboard color scheme: + - Top bar with SHOPPING color (#00c1ff) + - Left sidebar with a dark background (#2b2b2b) + - Right content area (profile form, profile picture) + """ + + # Main container frame (transparent background, like dashboard) + frame = ctk.CTkFrame(parent, fg_color="transparent") + + # ----------------- TOP BAR ----------------- + top_bar = ctk.CTkFrame(frame, fg_color=SHOPPING, height=40) + top_bar.pack(fill="x", side="top") + + top_label = ctk.CTkLabel( + top_bar, + text="My Profile", + text_color="white", + font=("Helvetica", 16, "bold"), + ) + top_label.pack(side="left", padx=20) + + def go_back(): + switch_func("dashboard") + + back_button = ctk.CTkButton( + top_bar, + text="Back", + fg_color="white", + text_color="black", + command=go_back, + width=60, + height=30, + ) + back_button.pack(side="right", padx=20, pady=5) + + # ----------------- MAIN SECTION (Sidebar + Content) ----------------- + main_section = ctk.CTkFrame(frame, fg_color="transparent") + main_section.pack(fill="both", expand=True) + + # ----------------- LEFT SIDEBAR ----------------- + + sidebar_frame = ctk.CTkFrame(main_section, width=200, fg_color="#2b2b2b") + sidebar_frame.pack(side="left", fill="y") + + sidebar_title = ctk.CTkLabel( + sidebar_frame, + text="Menu", + font=("Helvetica", 14, "bold"), + text_color="white" + ) + sidebar_title.pack(pady=(10, 5)) + + nav_dashboard = ctk.CTkButton( + sidebar_frame, + text="Dashboard", + fg_color="#2b2b2b", + text_color="white", + hover_color="#3b3b3b", + command=go_back + ) + nav_dashboard.pack(fill="x", padx=10, pady=5) + + nav_profile = ctk.CTkButton( + sidebar_frame, + text="My Profile", + fg_color="#3b3b3b", # Active/selected state + text_color="white", + hover_color="#3b3b3b", + state="disabled" + ) + nav_profile.pack(fill="x", padx=10, pady=5) + + nav_orders = ctk.CTkButton( + sidebar_frame, + text="My Orders", + fg_color="#2b2b2b", + text_color="white", + hover_color="#3b3b3b", + command=lambda: switch_func("user_orders") + ) + nav_orders.pack(fill="x", padx=10, pady=5) + + # ----------------- RIGHT CONTENT (User Details) ----------------- + content_frame = ctk.CTkFrame(main_section, fg_color="transparent") + content_frame.pack(side="left", fill="both", expand=True, padx=20, pady=20) + + # Title / Subtitle + title_label = ctk.CTkLabel( + content_frame, + text="My Profile", + font=("Helvetica", 18, "bold"), + text_color="white" + ) + title_label.pack(anchor="w", pady=(0, 5)) + + subtitle_label = ctk.CTkLabel( + content_frame, + text="Manage your profile information to keep your account secure", + font=("Helvetica", 12), + text_color="#cccccc" + ) + subtitle_label.pack(anchor="w", pady=(0, 15)) + + # -- Split the right content into two frames: Left form & Right picture -- + right_main = ctk.CTkFrame(content_frame, fg_color="transparent") + right_main.pack(fill="both", expand=True) + + # LEFT FORM + form_frame = ctk.CTkFrame(right_main, fg_color="transparent") + form_frame.pack(side="left", fill="both", expand=True, padx=(0, 20)) + + # Username + username_label = ctk.CTkLabel(form_frame, text="Username", font=("Helvetica", 12), text_color="white") + username_label.pack(anchor="w") + username_entry = ctk.CTkEntry(form_frame, placeholder_text="Enter your username...") + username_entry.pack(anchor="w", pady=(0, 10)) + + # Name + name_label = ctk.CTkLabel(form_frame, text="Name", font=("Helvetica", 12), text_color="white") + name_label.pack(anchor="w") + name_entry = ctk.CTkEntry(form_frame, placeholder_text="Enter your name...") + name_entry.pack(anchor="w", pady=(0, 10)) + + # Email + email_label = ctk.CTkLabel(form_frame, text="Email", font=("Helvetica", 12), text_color="white") + email_label.pack(anchor="w") + email_entry = ctk.CTkEntry(form_frame, placeholder_text="Enter your email...") + email_entry.pack(anchor="w", pady=(0, 10)) + + # Phone + phone_label = ctk.CTkLabel(form_frame, text="Phone", font=("Helvetica", 12), text_color="white") + phone_label.pack(anchor="w") + phone_entry = ctk.CTkEntry(form_frame, placeholder_text="Enter your phone...") + phone_entry.pack(anchor="w", pady=(0, 10)) + + # Gender + gender_label = ctk.CTkLabel(form_frame, text="Gender", font=("Helvetica", 12), text_color="white") + gender_label.pack(anchor="w", pady=(10, 0)) + + gender_var = ctk.StringVar(value="Male") # Default + gender_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + gender_frame.pack(anchor="w", pady=5) + + male_radio = ctk.CTkRadioButton(gender_frame, text="Male", variable=gender_var, value="Male", text_color="white") + female_radio = ctk.CTkRadioButton(gender_frame, text="Female", variable=gender_var, value="Female", text_color="white") + other_radio = ctk.CTkRadioButton(gender_frame, text="Other", variable=gender_var, value="Other", text_color="white") + male_radio.pack(side="left", padx=5) + female_radio.pack(side="left", padx=5) + other_radio.pack(side="left", padx=5) + + # Birthday + birthday_label = ctk.CTkLabel(form_frame, text="Date of Birth", font=("Helvetica", 12), text_color="white") + birthday_label.pack(anchor="w", pady=(10, 0)) + birthday_entry = ctk.CTkEntry(form_frame, placeholder_text="dd/mm/yyyy") + birthday_entry.pack(anchor="w", pady=(0, 10)) + + # Save Button + def save_profile(): + """ + Example: Send a request to update user details. + Adjust the endpoint and payload for your backend. + """ + headers = {"Authorization": f"Bearer {token}"} + payload = { + "username": username_entry.get().strip(), + "name": name_entry.get().strip(), + "email": email_entry.get().strip(), + "phone": phone_entry.get().strip(), + "gender": gender_var.get(), + "birthday": birthday_entry.get().strip() + } + try: + resp = requests.put(f"{API_URL}/user/update", headers=headers, json=payload) + if resp.status_code == 200: + messagebox.showinfo("Success", "Profile updated successfully!") + else: + messagebox.showerror("Error", "Unable to update profile.") + except Exception as e: + messagebox.showerror("Error", f"Request error: {e}") + + save_button = ctk.CTkButton( + form_frame, + text="Save", + fg_color=SHOPPING, # #00c1ff + text_color="white", + command=save_profile, + width=80 + ) + save_button.pack(anchor="w", pady=(20, 10)) + + # RIGHT PICTURE SECTION + pic_frame = ctk.CTkFrame(right_main, fg_color="transparent") + pic_frame.pack(side="left", fill="both", expand=True) + + pic_label = ctk.CTkLabel(pic_frame, text="Profile Picture", font=("Helvetica", 12, "bold"), text_color="white") + pic_label.pack(anchor="w", pady=(0, 10)) + + photo_label = ctk.CTkLabel(pic_frame, text="No image", text_color="white") + photo_label.pack(anchor="w", pady=(0, 10)) + + def choose_photo(): + """ + Let the user pick a photo from their files. + Then optionally upload it or just display it. + """ + file_path = filedialog.askopenfilename( + title="Choose a Profile Picture", + filetypes=[("Image Files", "*.png *.jpg *.jpeg *.gif")] + ) + if file_path: + # Display chosen image + try: + pil_img = Image.open(file_path).resize((100, 100)) + tk_img = ImageTk.PhotoImage(pil_img) + photo_label.configure(image=tk_img, text="") + photo_label.image = tk_img + + # Optionally, upload the file to your server: + # headers = {"Authorization": f"Bearer {token}"} + # with open(file_path, 'rb') as f: + # requests.post(f'{API_URL}/user/upload_photo', files={'file': f}, headers=headers) + except Exception as e: + print(f"Image load error: {e}") + messagebox.showerror("Error", "Cannot load the image.") + + change_photo_button = ctk.CTkButton( + pic_frame, + text="Choose Image", + fg_color="white", + text_color="black", + command=choose_photo, + width=80 + ) + change_photo_button.pack(anchor="w") + + # Fetch existing user info (if needed) + def fetch_user_info(): + """ + Fetch the user's existing data to populate the form fields. + """ + headers = {"Authorization": f"Bearer {token}"} + try: + resp = requests.get(f"{API_URL}/user/profile", headers=headers) + if resp.status_code == 200: + data = resp.json() + username_entry.delete(0, "end") + username_entry.insert(0, data.get("username", "")) + name_entry.delete(0, "end") + name_entry.insert(0, data.get("name", "")) + email_entry.delete(0, "end") + email_entry.insert(0, data.get("email", "")) + phone_entry.delete(0, "end") + phone_entry.insert(0, data.get("phone", "")) + gender_var.set(data.get("gender", "Male")) + birthday_entry.delete(0, "end") + birthday_entry.insert(0, data.get("birthday", "")) + + # If there's a profile picture URL, load it + pic_url = data.get("photo_url") + if pic_url: + try: + resp_pic = requests.get(pic_url) + if resp_pic.status_code == 200: + pil_img = Image.open(io.BytesIO(resp_pic.content)).resize((100, 100)) + tk_img = ImageTk.PhotoImage(pil_img) + photo_label.configure(image=tk_img, text="") + photo_label.image = tk_img + except Exception as e: + print(f"Profile picture load error: {e}") + else: + messagebox.showerror("Error", "Unable to retrieve user information.") + except Exception as e: + messagebox.showerror("Error", f"Request error: {e}") + + fetch_user_info() + + return frame diff --git a/app/frontend/components/user_orders.py b/app/frontend/components/user_orders.py new file mode 100644 index 0000000000000000000000000000000000000000..b71b34fc7f27dd6ebfcf97eb4b3b9ad1210b57a2 --- /dev/null +++ b/app/frontend/components/user_orders.py @@ -0,0 +1,228 @@ +import customtkinter as ctk +import requests +from PIL import Image, ImageTk +from tkinter import messagebox +import io + +SHOPPING = "#00c1ff" + +def user_orders_frame(parent, switch_func, API_URL, token): + """ + A two-column user orders page that displays the products the user has purchased + along with associated shop details. The layout and color scheme match your dashboard. + """ + + # Main container frame with transparent background + frame = ctk.CTkFrame(parent, fg_color="transparent") + + # ----------------- TOP BAR ----------------- + top_bar = ctk.CTkFrame(frame, fg_color=SHOPPING, height=40) + top_bar.pack(fill="x", side="top") + + top_label = ctk.CTkLabel( + top_bar, + text="My Orders", + text_color="white", + font=("Helvetica", 16, "bold") + ) + top_label.pack(side="left", padx=20) + + def go_back(): + switch_func("dashboard") + + back_button = ctk.CTkButton( + top_bar, + text="Back", + fg_color="white", + text_color="black", + command=go_back, + width=60, + height=30 + ) + back_button.pack(side="right", padx=20, pady=5) + + # ----------------- MAIN SECTION (Sidebar + Content) ----------------- + main_section = ctk.CTkFrame(frame, fg_color="transparent") + main_section.pack(fill="both", expand=True) + + # ----------------- LEFT SIDEBAR ----------------- + sidebar_frame = ctk.CTkFrame(main_section, width=200, fg_color="#2b2b2b") + sidebar_frame.pack(side="left", fill="y") + + sidebar_title = ctk.CTkLabel( + sidebar_frame, + text="Menu", + font=("Helvetica", 14, "bold"), + text_color="white" + ) + sidebar_title.pack(pady=(10, 5)) + + def open_profile(): + switch_func("user_details") + + nav_dashboard = ctk.CTkButton( + sidebar_frame, + text="Dashboard", + fg_color="#2b2b2b", + text_color="white", + hover_color="#3b3b3b", + command=go_back + ) + nav_dashboard.pack(fill="x", padx=10, pady=5) + + nav_profile = ctk.CTkButton( + sidebar_frame, + text="My Profile", + fg_color="#2b2b2b", + text_color="white", + hover_color="#3b3b3b", + command=open_profile + ) + nav_profile.pack(fill="x", padx=10, pady=5) + + nav_orders = ctk.CTkButton( + sidebar_frame, + text="My Orders", + fg_color="#3b3b3b", # Active/selected state + text_color="white", + hover_color="#3b3b3b", + state="disabled" + ) + nav_orders.pack(fill="x", padx=10, pady=5) + + # ----------------- RIGHT CONTENT (Orders List) ----------------- + content_frame = ctk.CTkFrame(main_section, fg_color="transparent") + content_frame.pack(side="left", fill="both", expand=True, padx=20, pady=20) + + main_title_label = ctk.CTkLabel( + content_frame, + text="Your Orders", + font=("Helvetica", 18, "bold"), + text_color="white" + ) + main_title_label.pack(anchor="w", pady=(0, 5)) + + subtitle_label = ctk.CTkLabel( + content_frame, + text="Review the products you have purchased.", + font=("Helvetica", 12), + text_color="#cccccc" + ) + subtitle_label.pack(anchor="w", pady=(0, 15)) + + # A frame to hold the list of orders + orders_list_frame = ctk.CTkFrame(content_frame, fg_color="transparent") + orders_list_frame.pack(fill="both", expand=True) + + # ----------- Functions to fetch and display orders ----------- + def fetch_orders(): + """ + Fetch the list of user orders from the API. + """ + headers = {"Authorization": f"Bearer {token}"} + try: + resp = requests.get(f"{API_URL}/orders/list", headers=headers) + if resp.status_code == 200: + orders = resp.json() # Expect a list of order dicts + display_orders(orders, orders_list_frame) + else: + messagebox.showerror("Error", "Failed to fetch orders.") + except Exception as ex: + messagebox.showerror("Error", f"Request error: {ex}") + + def display_orders(orders, container): + """ + Display each order with product and shop details. + """ + # Clear previous content + for widget in container.winfo_children(): + widget.destroy() + + if not orders: + ctk.CTkLabel(container, text="No orders found.", text_color="white").pack(pady=10) + return + + for order in orders: + # Assume each order dict includes a 'product' dict and an 'order_date' + product = order.get("product", {}) + order_date = order.get("order_date", "Unknown Date") + + order_frame = ctk.CTkFrame(container, corner_radius=5, fg_color="#2b2b2b") + order_frame.pack(fill="x", padx=5, pady=5) + + # Left: Product image + image_label = ctk.CTkLabel(order_frame, text="") + image_label.pack(side="left", padx=5, pady=5) + if product.get("images"): + try: + img_url = product["images"][0]["image_url"] + img_resp = requests.get(img_url) + if img_resp.status_code == 200: + pil_img = Image.open(io.BytesIO(img_resp.content)).resize((60, 60)) + tk_img = ImageTk.PhotoImage(pil_img) + image_label.configure(image=tk_img, text="") + image_label.image = tk_img + except Exception as ex: + print(f"Product image error: {ex}") + + # Right: Order details and shop info + info_frame = ctk.CTkFrame(order_frame, fg_color="transparent") + info_frame.pack(side="left", fill="both", expand=True, padx=10) + + # Product Name + ctk.CTkLabel( + info_frame, + text=product.get("name", "No Name"), + font=("Helvetica", 13, "bold"), + text_color="white" + ).pack(anchor="w") + + # Price (and order date) + price = product.get("price", 0.0) + ctk.CTkLabel( + info_frame, + text=f"Price: {price:.2f}", + text_color="#cccccc" + ).pack(anchor="w") + ctk.CTkLabel( + info_frame, + text=f"Ordered on: {order_date}", + text_color="#cccccc" + ).pack(anchor="w") + + # Shop Name + shop_id = product.get("shop_id") + shop_name_label = ctk.CTkLabel(info_frame, text="Shop: Loading...", text_color="#cccccc") + shop_name_label.pack(anchor="w") + + def fetch_shop_and_update_label(sid, label_widget): + headers = {"Authorization": f"Bearer {token}"} + try: + sresp = requests.get(f"{API_URL}/shop/get/{sid}", headers=headers) + if sresp.status_code == 200: + shop_data = sresp.json() + label_widget.configure(text=f"Shop: {shop_data.get('name', 'No Shop Name')}") + else: + label_widget.configure(text="Shop: Not found") + except Exception: + label_widget.configure(text="Shop: Error fetching") + + fetch_shop_and_update_label(shop_id, shop_name_label) + + # "View Order" button (placeholder action) + def view_order(): + messagebox.showinfo("Order Details", f"View details for order of {product.get('name')}") + + view_button = ctk.CTkButton( + info_frame, + text="View Order", + fg_color=SHOPPING, + text_color="white", + command=view_order + ) + view_button.pack(anchor="e", pady=(5, 0)) + + # Fetch orders when the frame loads + fetch_orders() + + return frame diff --git a/app/frontend/main.py b/app/frontend/main.py index 62b98c49810c96e5b161119af7f9a733f2e5c711..65b8f0af7e3ad2c8f48b1b343ae2719166ed899e 100644 --- a/app/frontend/main.py +++ b/app/frontend/main.py @@ -1,4 +1,3 @@ -# main.py import customtkinter as ctk from components.auth.login import login_frame from components.auth.register import register_frame @@ -6,7 +5,9 @@ from components.shop.create_shop import create_shop_frame from components.shop.view_shop import view_shop_frame from components.product.create_product import product_frame from components.admin.category import category_frame -from components.dashboard import dashboard_frame # import the dashboard +from components.dashboard import dashboard_frame +from components.user_details import user_details_frame +from components.user_orders import user_orders_frame API_URL = "http://127.0.0.1:8000" access_token = None @@ -28,7 +29,7 @@ ctk.set_default_color_theme("blue") root = ctk.CTk() root.title("Shopping App") -root.geometry("900x800") +root.geometry("1000x800") # Create Frames login = login_frame(root, switch_frame, API_URL) @@ -37,9 +38,9 @@ create_shop = create_shop_frame(root, switch_frame, API_URL, access_token) view_shop = view_shop_frame(root, switch_frame, API_URL, access_token) product = product_frame(root, switch_frame, API_URL, access_token) category = category_frame(root, switch_frame, API_URL, access_token) -dashboard = dashboard_frame( - root, switch_frame, API_URL, access_token -) # new dashboard frame +dashboard = dashboard_frame(root, switch_frame, API_URL, access_token) +user_details = user_details_frame(root, switch_frame, API_URL, access_token) +user_orders = user_orders_frame(root, switch_frame, API_URL, access_token) frames = { "login": login, @@ -48,13 +49,17 @@ frames = { "create_product": product, "category": category, "view_shop": view_shop, - "dashboard": dashboard, # add dashboard here + "dashboard": dashboard, + "user_details": user_details, + "user_orders": user_orders, } for frame in frames.values(): - frame.place(relx=0, rely=0, relwidth=1, relheight=0.8) + frame.place( + relx=0, rely=0, relwidth=1, relheight=1 + ) # Adjusted height for full scaling -# Show the login frame first (or switch to dashboard as needed) -switch_frame("login") # switch to dashboard +# Show the login frame first +switch_frame("login") root.mainloop() diff --git a/app/frontend/utils/api_requests.py b/app/frontend/utils/api_requests.py index 4f50771d7fd4a12c94986dd217f248d6083931fc..d17146c43f5b66338f8acb17aa47c28740b969c0 100644 --- a/app/frontend/utils/api_requests.py +++ b/app/frontend/utils/api_requests.py @@ -2,28 +2,32 @@ import requests from PIL import Image import io -#Login API + +# Login API def login_api(email, password, api_url): """ Sends login request to the API. - + :param email: User's email :param password: User's password :param api_url: Base API URL :return: Tuple (status_code, response_data) """ try: - response = requests.post(f"{api_url}/auth/login", json={"email": email, "password": password}) + response = requests.post( + f"{api_url}/auth/login", json={"email": email, "password": password} + ) response.raise_for_status() # Raise an error for 4xx and 5xx responses return response.status_code, response.json() # Return status and response data except requests.exceptions.RequestException as e: return None, {"error": str(e)} # Return error message if request fails -#Register API + +# Register API def register_api(username, email, phone_number, password, api_url): """ Sends a registration request to the API. - + :param username: User's username :param email: User's email :param phone_number: User's phone number @@ -46,7 +50,8 @@ def register_api(username, email, phone_number, password, api_url): except requests.exceptions.RequestException as e: return None, {"detail": str(e)} # Return error message if request fails -#Create Shop + +# Create Shop def create_shop_api(name, description, file_path, api_url, access_token): """ Sends a request to create a shop with optional image upload. @@ -81,7 +86,8 @@ def create_shop_api(name, description, file_path, api_url, access_token): except requests.exceptions.RequestException as e: return None, {"detail": str(e)} -#View Shop + +# View Shop def fetch_shop_details(api_url, token, shop_id=1): """ Fetches details of the shop owned by the logged-in user. @@ -101,7 +107,8 @@ def fetch_shop_details(api_url, token, shop_id=1): except requests.exceptions.RequestException as e: return {"error": str(e)} -#Fetch Products + +# Fetch Products def fetch_shop_products(api_url, token, shop_id): """ Fetches products for a given shop. @@ -113,7 +120,9 @@ def fetch_shop_products(api_url, token, shop_id): """ headers = {"Authorization": f"Bearer {token}"} try: - response = requests.get(f"{api_url}/products?shop_id={shop_id}", headers=headers) + response = requests.get( + f"{api_url}/products?shop_id={shop_id}", headers=headers + ) if response.status_code == 200: return response.json() else: @@ -121,7 +130,8 @@ def fetch_shop_products(api_url, token, shop_id): except requests.exceptions.RequestException as e: return {"error": str(e)} -#Load Image from URL + +# Load Image from URL def load_image_from_url(image_url, size=(150, 150)): """ Loads and resizes an image from a given URL. @@ -136,4 +146,4 @@ def load_image_from_url(image_url, size=(150, 150)): image = image.resize(size) return image except Exception: - return None \ No newline at end of file + return None diff --git a/app/static/front_end_img/login.jpg b/app/static/front_end_img/login.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b8235c6d4067b453380179dda20c7c72e6da8ab5 Binary files /dev/null and b/app/static/front_end_img/login.jpg differ diff --git a/requirements.txt b/requirements.txt index 2383f12bc661c32a3b8eac0fcf9146bd60a0f3fe..1c5bb150f1acbbb5b29d84ddfede66a9ed8b7204 100644 Binary files a/requirements.txt and b/requirements.txt differ