diff --git a/app/backend/dummy_data.py b/app/backend/dummy_data.py index 80db0c79c42d2fa78c422ccf58e166113d375b2d..c1c4050ef00a1c3dcc39f31b3f09c27c024fb920 100644 --- a/app/backend/dummy_data.py +++ b/app/backend/dummy_data.py @@ -91,10 +91,37 @@ def insert_dummy_data(session: Session): ), Shop( owner_id=2, - name="Eiffel Tower", + name="famous Eiffel Tower", description="Iconic landmark in Paris", image_url="app/static/default/default_shop.png", - address="Eiffel Tower, Paris, France", + address="New York, USA", + latitude=48.8588443, + longitude=2.2943506, + ), + Shop( + owner_id=2, + name="white house", + description="Iconic landmark in Paris", + image_url="app/static/default/default_shop.png", + address="Pennsylvania Avenue NW, Washington, DC", + latitude=48.8588443, + longitude=2.2943506, + ), + Shop( + owner_id=2, + name="another Eiffel Tower", + description="Iconic landmark in Paris", + image_url="app/static/default/default_shop.png", + address="Hanoi, Vietnam", + latitude=48.8588443, + longitude=2.2943506, + ), + Shop( + owner_id=2, + name="asdf", + description="Iconic landmark in Paris", + image_url="app/static/default/default_shop.png", + address="Hangzhou, China", latitude=48.8588443, longitude=2.2943506, ), diff --git a/app/backend/routes/shop.py b/app/backend/routes/shop.py index d5a651ae41d8c67d3f84b711e2e270707f83e5a7..a545b83a89e75bcaa62f9395aed67a0caa0dc325 100644 --- a/app/backend/routes/shop.py +++ b/app/backend/routes/shop.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form from sqlmodel import Session, func from geopy.geocoders import Nominatim -from backend.models.models import Shop, User, Order +from backend.models.models import Shop, User, Order, Product from backend.schemas.shop import ShopRead from backend.database import get_session from backend.routes.auth import get_current_user @@ -81,12 +81,19 @@ def get_all_shops(order: str = "desc", session: Session = Depends(get_session)): return shops -@router.get("/get/{shop_id}", response_model=ShopRead) +@router.get("/get/{shop_id}", response_model=dict) def read_shop(shop_id: int, session: Session = Depends(get_session)): shop = session.get(Shop, shop_id) if not shop: raise HTTPException(status_code=404, detail="Shop not found") - return shop + + # Query products related to the shop + products = session.query(Product).filter(Product.shop_id == shop_id).all() + + return { + "shop": shop, + "products": products + } @router.put("/put/{shop_id}", response_model=ShopRead) diff --git a/app/frontend/components/auth/login.py b/app/frontend/components/auth/login.py index 31d2560c45f5d7f2890cbcdc6d35b9c98eed19a4..f44c06fbb19d8b6b4ead29df0d0c96c3c75ea2b1 100644 --- a/app/frontend/components/auth/login.py +++ b/app/frontend/components/auth/login.py @@ -1,28 +1,47 @@ import customtkinter as ctk +import os from tkinter import messagebox from PIL import Image from utils.api_requests import login_api +SHOPPING = "#00c1ff" +DARK_BG = "#1f1f1f" +CARD_BG = "#2b2b2b" def login_frame(parent, switch_func, API_URL): - # Overall container - container = ctk.CTkFrame(parent, fg_color="#F3F4F6") # Light gray background + # Overall container with dark background + container = ctk.CTkFrame(parent, fg_color=DARK_BG) 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") + left_frame = ctk.CTkFrame(container, fg_color=CARD_BG, corner_radius=15) + left_frame.grid(row=0, column=0, sticky="nsew", padx=(20, 10), pady=20) - 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) + # App branding + brand_frame = ctk.CTkFrame(left_frame, fg_color="transparent") + brand_frame.place(relx=0.5, rely=0.5, anchor="center") + + app_name = ctk.CTkLabel( + brand_frame, + text="Shopping App", + font=("Helvetica", 40, "bold"), + text_color=SHOPPING + ) + app_name.pack() + + app_slogan = ctk.CTkLabel( + brand_frame, + text="Your One-Stop Shopping Destination", + font=("Helvetica", 16), + text_color="white" + ) + app_slogan.pack(pady=(10, 30)) # 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 = ctk.CTkFrame(container, fg_color=CARD_BG, corner_radius=15) + right_frame.grid(row=0, column=1, sticky="nsew", padx=(10, 20), pady=20) right_frame.grid_columnconfigure(0, weight=1) # Login Function @@ -41,54 +60,107 @@ def login_frame(parent, switch_func, API_URL): messagebox.showinfo("Login Successful", f"Welcome back, {email}!") switch_func("dashboard", access_token) else: - messagebox.showerror("Login Failed", response_data.get("detail", "Invalid credentials")) + messagebox.showerror( + "Login Failed", response_data.get("detail", "Invalid credentials") + ) + + # Login form container + form_frame = ctk.CTkFrame(right_frame, fg_color="transparent") + form_frame.place(relx=0.5, rely=0.5, anchor="center") # Title ctk.CTkLabel( - right_frame, - text="Login", - font=ctk.CTkFont("Helvetica", size=26, weight="bold"), - text_color="#111827" - ).pack(pady=(20, 10)) + form_frame, + text="Welcome Back", + font=("Helvetica", 32, "bold"), + text_color=SHOPPING + ).pack(pady=(0, 10)) + + ctk.CTkLabel( + form_frame, + text="Sign in to your account", + font=("Helvetica", 14), + text_color="gray" + ).pack(pady=(0, 20)) # Email + email_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + email_frame.pack(fill="x", pady=(0, 15)) + ctk.CTkLabel( - right_frame, text="Email", font=("Helvetica", 14), text_color="#374151" - ).pack(pady=(10, 5)) + email_frame, + text="Email", + font=("Helvetica", 14), + text_color="white" + ).pack(anchor="w") + entry_email = ctk.CTkEntry( - right_frame, height=40, corner_radius=10, placeholder_text="Enter your email" + email_frame, + height=40, + corner_radius=8, + placeholder_text="Enter your email", + fg_color="#3b3b3b", + border_color=SHOPPING, + text_color="white" ) - entry_email.pack(pady=5, padx=20, fill="x") + entry_email.pack(fill="x", pady=(5, 0)) # Password + password_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + password_frame.pack(fill="x", pady=(0, 20)) + ctk.CTkLabel( - right_frame, text="Password", font=("Helvetica", 14), text_color="#374151" - ).pack(pady=(10, 5)) + password_frame, + text="Password", + font=("Helvetica", 14), + text_color="white" + ).pack(anchor="w") + entry_password = ctk.CTkEntry( - right_frame, height=40, show="*", corner_radius=10, placeholder_text="Enter your password" + password_frame, + height=40, + show="*", + corner_radius=8, + placeholder_text="Enter your password", + fg_color="#3b3b3b", + border_color=SHOPPING, + text_color="white" ) - entry_password.pack(pady=5, padx=20, fill="x") + entry_password.pack(fill="x", pady=(5, 0)) # Login Button ctk.CTkButton( - right_frame, - text="Login", + form_frame, + text="Sign In", command=login, - corner_radius=10, + corner_radius=8, height=45, - font=("Helvetica", 14), - fg_color="#2563EB", - hover_color="#1E40AF" - ).pack(pady=20, padx=20, fill="x") + font=("Helvetica", 14, "bold"), + fg_color=SHOPPING, + hover_color="#0096ff", + ).pack(fill="x", pady=(0, 15)) # Register Redirect + register_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + register_frame.pack(fill="x") + + ctk.CTkLabel( + register_frame, + text="Don't have an account?", + font=("Helvetica", 12), + text_color="gray" + ).pack(side="left", padx=(0, 5)) + ctk.CTkButton( - right_frame, - text="Don't have an account? Register", + register_frame, + text="Register", command=lambda: switch_func("register"), fg_color="transparent", - hover_color="#E5E7EB", - text_color="#2563EB" - ).pack(pady=(5, 20)) + hover_color="#3b3b3b", + text_color=SHOPPING, + font=("Helvetica", 12, "bold"), + width=70, + height=25 + ).pack(side="left") return container diff --git a/app/frontend/components/auth/register.py b/app/frontend/components/auth/register.py index d2a635345377808e657efeeb110b660141a209e0..6677b2fbd966dfb6113bc4add55f5b6806ae5e86 100644 --- a/app/frontend/components/auth/register.py +++ b/app/frontend/components/auth/register.py @@ -1,17 +1,45 @@ import customtkinter as ctk from tkinter import messagebox -from utils.api_requests import register_api # Import the API function +from utils.api_requests import register_api +SHOPPING = "#00c1ff" +DARK_BG = "#1f1f1f" +CARD_BG = "#2b2b2b" def register_frame(parent, switch_func, API_URL): - # 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") + # Create a full-window container with dark background + container = ctk.CTkFrame(parent, fg_color=DARK_BG) + container.grid_columnconfigure(0, weight=1) + container.grid_columnconfigure(1, weight=1) + container.grid_rowconfigure(0, weight=1) + + # Left branding frame + left_frame = ctk.CTkFrame(container, fg_color=CARD_BG, corner_radius=15) + left_frame.grid(row=0, column=0, sticky="nsew", padx=(20, 10), pady=20) + + # App branding + brand_frame = ctk.CTkFrame(left_frame, fg_color="transparent") + brand_frame.place(relx=0.5, rely=0.5, anchor="center") + + app_name = ctk.CTkLabel( + brand_frame, + text="Shopping App", + font=("Helvetica", 40, "bold"), + text_color=SHOPPING + ) + app_name.pack() + + app_slogan = ctk.CTkLabel( + brand_frame, + text="Join Our Shopping Community", + font=("Helvetica", 16), + text_color="white" + ) + app_slogan.pack(pady=(10, 30)) + + # Right register frame + right_frame = ctk.CTkFrame(container, fg_color=CARD_BG, corner_radius=15) + right_frame.grid(row=0, column=1, sticky="nsew", padx=(10, 20), pady=20) def register(): username = entry_username.get() @@ -20,7 +48,7 @@ def register_frame(parent, switch_func, API_URL): password = entry_password.get() confirm_password = entry_confirm_password.get() - if not username or not email or not phone_number or not password or not confirm_password: + if not all([username, email, phone_number, password, confirm_password]): messagebox.showwarning("Input Error", "All fields are required!") return @@ -28,7 +56,6 @@ def register_frame(parent, switch_func, API_URL): messagebox.showerror("Password Error", "Passwords do not match!") return - # Call the API function from register_api.py status_code, response_data = register_api( username, email, phone_number, password, API_URL ) @@ -37,66 +64,174 @@ def register_frame(parent, switch_func, API_URL): messagebox.showinfo("Registration Successful", f"Welcome, {username}!") switch_func("login") else: - messagebox.showerror("Registration Failed", response_data.get("detail", "Unknown error")) + messagebox.showerror( + "Registration Failed", response_data.get("detail", "Unknown error") + ) + + # Register form container + form_frame = ctk.CTkFrame(right_frame, fg_color="transparent") + form_frame.place(relx=0.5, rely=0.5, anchor="center") - # Title for the registration form + # Title ctk.CTkLabel( - register_panel, + form_frame, text="Create Account", - font=ctk.CTkFont("Helvetica", size=26, weight="bold"), - text_color="#111827" - ).pack(pady=(20, 10)) + font=("Helvetica", 32, "bold"), + text_color=SHOPPING + ).pack(pady=(0, 10)) + + ctk.CTkLabel( + form_frame, + text="Fill in your details to register", + font=("Helvetica", 14), + text_color="gray" + ).pack(pady=(0, 20)) + + # Username + username_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + username_frame.pack(fill="x", pady=(0, 15)) + + ctk.CTkLabel( + username_frame, + text="Username", + font=("Helvetica", 14), + text_color="white" + ).pack(anchor="w") - # Username Entry entry_username = ctk.CTkEntry( - register_panel, placeholder_text="Username", height=40, corner_radius=10, width=250 + username_frame, + height=40, + corner_radius=8, + placeholder_text="Enter your username", + fg_color="#3b3b3b", + border_color=SHOPPING, + text_color="white" ) - entry_username.pack(pady=10, padx=20, fill="x") + entry_username.pack(fill="x", pady=(5, 0)) + + # Email + email_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + email_frame.pack(fill="x", pady=(0, 15)) + + ctk.CTkLabel( + email_frame, + text="Email", + font=("Helvetica", 14), + text_color="white" + ).pack(anchor="w") - # Email Entry entry_email = ctk.CTkEntry( - register_panel, placeholder_text="Email", height=40, corner_radius=10, width=250 + email_frame, + height=40, + corner_radius=8, + placeholder_text="Enter your email", + fg_color="#3b3b3b", + border_color=SHOPPING, + text_color="white" ) - entry_email.pack(pady=10, padx=20, fill="x") + entry_email.pack(fill="x", pady=(5, 0)) + + # Phone + phone_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + phone_frame.pack(fill="x", pady=(0, 15)) + + ctk.CTkLabel( + phone_frame, + text="Phone Number", + font=("Helvetica", 14), + text_color="white" + ).pack(anchor="w") - # Phone Number Entry entry_phone = ctk.CTkEntry( - register_panel, placeholder_text="Phone Number", height=40, corner_radius=10, width=250 + phone_frame, + height=40, + corner_radius=8, + placeholder_text="Enter your phone number", + fg_color="#3b3b3b", + border_color=SHOPPING, + text_color="white" ) - entry_phone.pack(pady=10, padx=20, fill="x") + entry_phone.pack(fill="x", pady=(5, 0)) + + # Password + password_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + password_frame.pack(fill="x", pady=(0, 15)) + + ctk.CTkLabel( + password_frame, + text="Password", + font=("Helvetica", 14), + text_color="white" + ).pack(anchor="w") - # Password Entry entry_password = ctk.CTkEntry( - register_panel, placeholder_text="Password", show="*", height=40, corner_radius=10, width=250 + password_frame, + height=40, + corner_radius=8, + show="*", + placeholder_text="Enter your password", + fg_color="#3b3b3b", + border_color=SHOPPING, + text_color="white" ) - entry_password.pack(pady=10, padx=20, fill="x") + entry_password.pack(fill="x", pady=(5, 0)) + + # Confirm Password + confirm_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + confirm_frame.pack(fill="x", pady=(0, 20)) + + ctk.CTkLabel( + confirm_frame, + text="Confirm Password", + font=("Helvetica", 14), + text_color="white" + ).pack(anchor="w") - # Confirm Password Entry entry_confirm_password = ctk.CTkEntry( - register_panel, placeholder_text="Confirm Password", show="*", height=40, corner_radius=10, width=250 + confirm_frame, + height=40, + corner_radius=8, + show="*", + placeholder_text="Confirm your password", + fg_color="#3b3b3b", + border_color=SHOPPING, + text_color="white" ) - entry_confirm_password.pack(pady=10, padx=20, fill="x") + entry_confirm_password.pack(fill="x", pady=(5, 0)) # Register Button ctk.CTkButton( - register_panel, - text="Register", + form_frame, + text="Create Account", command=register, - corner_radius=10, + corner_radius=8, height=45, - font=("Helvetica", 14), - fg_color="#2563EB", - hover_color="#1E40AF" - ).pack(pady=20, padx=20, fill="x") + font=("Helvetica", 14, "bold"), + fg_color=SHOPPING, + hover_color="#0096ff", + ).pack(fill="x", pady=(0, 15)) + + # Login Redirect + login_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + login_frame.pack(fill="x") + + ctk.CTkLabel( + login_frame, + text="Already have an account?", + font=("Helvetica", 12), + text_color="gray" + ).pack(side="left", padx=(0, 5)) - # Link to Login Page ctk.CTkButton( - register_panel, - text="Already have an account? Login", + login_frame, + text="Sign In", command=lambda: switch_func("login"), fg_color="transparent", - hover_color="#E5E7EB", - text_color="#2563EB" - ).pack(pady=(5, 20)) + hover_color="#3b3b3b", + text_color=SHOPPING, + font=("Helvetica", 12, "bold"), + width=70, + height=25 + ).pack(side="left") return container diff --git a/app/frontend/components/dashboard.py b/app/frontend/components/dashboard.py index f5707c926cb655e12186dcfcccba5ebafc3a82b6..b4bc80b308d771f80d9302b14b3c249a99c695c2 100644 --- a/app/frontend/components/dashboard.py +++ b/app/frontend/components/dashboard.py @@ -3,9 +3,14 @@ import requests from PIL import Image, ImageTk from tkinter import messagebox import io +# import time +# import threading SHOPPING = "#00c1ff" -BACKEND_HOST = "http://127.0.0.1:8000" # Adjust if needed +DARK_BG = "#1f1f1f" +CARD_BG = "#2b2b2b" +BACKEND_HOST = "http://127.0.0.1:8000" + def fix_url(url): """ @@ -21,33 +26,75 @@ def fix_url(url): # Prepend the public URL return f"{BACKEND_HOST}/static/{url}" -def dashboard_frame(parent, switch_func, API_URL, token): - """ - 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) ------------- - header_frame = ctk.CTkFrame(frame, fg_color=SHOPPING) - header_frame.place(relx=0, rely=0, relwidth=1, relheight=0.1) - # App Logo (Left) - logo_label = ctk.CTkLabel( - header_frame, - text="Shopping App", +def dashboard_frame(parent, switch_func, API_URL, token): + frame = ctk.CTkFrame(parent, fg_color=DARK_BG) + + # ------------- LEFT SIDEBAR ------------- + sidebar = ctk.CTkFrame(frame, fg_color=CARD_BG, width=60) + sidebar.pack(side="left", fill="y") + sidebar.pack_propagate(False) + + # Dashboard Icon + dashboard_icon = ctk.CTkButton( + sidebar, + text="🏠", + font=("Helvetica", 20), + fg_color="transparent", text_color="white", - font=("Helvetica", 20, "bold"), + hover_color="#3b3b3b", + width=40, + height=40, + command=lambda: switch_func("dashboard") + ) + dashboard_icon.pack(pady=(20, 10)) + + # User Icon + user_icon = ctk.CTkButton( + sidebar, + text="👤", + font=("Helvetica", 20), + fg_color="transparent", + text_color="white", + hover_color="#3b3b3b", + width=40, + height=40, + command=lambda: switch_func("user_details") + ) + user_icon.pack(pady=10) + + # Cart Icon + cart_icon = ctk.CTkButton( + sidebar, + text="🛒", + font=("Helvetica", 20), + fg_color="transparent", + text_color="white", + hover_color="#3b3b3b", + width=40, + height=40, + command=lambda: switch_func("user_orders") ) - logo_label.place(relx=0.01, rely=0.1, relwidth=0.15, relheight=0.8) + cart_icon.pack(pady=10) + + # ------------- MAIN CONTENT ------------- + main_content = ctk.CTkFrame(frame, fg_color="transparent") + main_content.pack(side="left", fill="both", expand=True) + + # Search bar at top + search_frame = ctk.CTkFrame(main_content, fg_color=CARD_BG, height=60) + search_frame.pack(fill="x", padx=20, pady=10) + search_frame.pack_propagate(False) - # Search bar (Center) search_entry = ctk.CTkEntry( - header_frame, placeholder_text="Search in Shop...", height=30 + search_frame, + placeholder_text="Search for shops and products...", + height=35, + fg_color="#3b3b3b", + text_color="white", + placeholder_text_color="#888888" ) - search_entry.place(relx=0.25, rely=0.25, relwidth=0.45, relheight=0.5) + search_entry.pack(side="left", fill="x", expand=True, padx=(20, 10), pady=12) def perform_search(): """Call an endpoint to search products by keyword.""" @@ -58,165 +105,224 @@ def dashboard_frame(parent, switch_func, API_URL, token): try: headers = {"Authorization": f"Bearer {token}"} # Adjust endpoint/params as needed - resp = requests.get(f"{API_URL}/search?name={keyword}&search_type=products", headers=headers) + 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) + display_products(products, products_frame) else: - messagebox.showerror("Error", f"Search failed. Status code: {resp.status_code}") + messagebox.showerror( + "Error", f"Search failed. Status code: {resp.status_code}" + ) except Exception as e: messagebox.showerror("Error", f"Request error: {e}") search_button = ctk.CTkButton( - header_frame, + search_frame, text="Search", - fg_color="white", - text_color="black", - 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) - - featured_label = ctk.CTkLabel( - middle_frame, text="TOP SHOP", font=("Helvetica", 16, "bold") - ) - featured_label.pack(pady=5) - - top_shops_frame = ctk.CTkScrollableFrame( - middle_frame, fg_color="transparent", width=750, height=150 + fg_color=SHOPPING, + text_color="white", + width=100, + height=35, + command=perform_search ) - top_shops_frame.pack(fill="both", expand=True, padx=10, pady=5) + search_button.pack(side="right", padx=20) + + # Featured Shops Carousel + carousel_frame = ctk.CTkFrame(main_content, fg_color=CARD_BG, height=300) + carousel_frame.pack(fill="x", padx=20, pady=10) + carousel_frame.pack_propagate(False) + + # Carousel content + carousel_content = ctk.CTkFrame(carousel_frame, fg_color="transparent") + carousel_content.pack(fill="both", expand=True, padx=20, pady=20) + + featured_shops = [] + current_shop_index = 0 + + def create_shop_slide(shop_data): + slide = ctk.CTkFrame(carousel_content, fg_color="transparent") + + # Shop Image + image_frame = ctk.CTkFrame(slide, fg_color="transparent", width=400, height=200) + image_frame.pack(side="left", padx=20) + image_frame.pack_propagate(False) + + image_label = ctk.CTkLabel(image_frame, text="") + image_label.pack(expand=True) - 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)) + pil_img = Image.open(io.BytesIO(resp.content)).resize((380, 180)) 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 + # Shop Details + details_frame = ctk.CTkFrame(slide, fg_color="transparent") + details_frame.pack(side="left", fill="both", expand=True, padx=20) + + shop_name = ctk.CTkLabel( + details_frame, + text=shop_data.get("name", "Shop Name"), + font=("Helvetica", 24, "bold"), + text_color=SHOPPING + ) + shop_name.pack(anchor="w", pady=(0, 10)) + + shop_desc = ctk.CTkLabel( + details_frame, + text=shop_data.get("description", "No description available."), + font=("Helvetica", 14), + text_color="white", + wraplength=400, + justify="left" + ) + shop_desc.pack(anchor="w", pady=5) + + visit_button = ctk.CTkButton( + details_frame, + text="Visit Shop →", + fg_color=SHOPPING, + text_color="white", + command=lambda: switch_func("view_shop", shop_data.get("id")), + width=120, + height=35 + ) + visit_button.pack(anchor="w", pady=20) + + return slide + + def update_carousel(): + nonlocal current_shop_index + if not featured_shops: + return - def display_shops(shops, container): - for widget in container.winfo_children(): + # Clear current content + for widget in carousel_content.winfo_children(): widget.destroy() - if not shops: - ctk.CTkLabel(container, text="No shops found.").pack(pady=10) - return + # Create and show current slide + current_slide = create_shop_slide(featured_shops[current_shop_index]) + current_slide.pack(fill="both", expand=True) - inner_frame = ctk.CTkFrame(container, fg_color="transparent") - inner_frame.pack(side="top", fill="both", expand=True) + # Update dots + update_carousel_dots() - for shop in shops: - shop_card = create_shop_card(inner_frame, shop) - shop_card.pack(side="left", padx=5, pady=5) + # Schedule next slide + current_shop_index = (current_shop_index + 1) % len(featured_shops) + carousel_content.after(5000, update_carousel) - # ------------- BOTTOM (Products) ------------- - bottom_frame = ctk.CTkFrame(frame, fg_color="transparent") - bottom_frame.place(relx=0, rely=0.35, relwidth=1, relheight=0.65) + # Carousel navigation dots + dots_frame = ctk.CTkFrame(carousel_frame, fg_color="transparent", height=30) + dots_frame.pack(side="bottom", fill="x", pady=10) + + def update_carousel_dots(): + for widget in dots_frame.winfo_children(): + widget.destroy() - recommend_label = ctk.CTkLabel( - bottom_frame, text="TODAY'S RECOMMENDATIONS", font=("Helvetica", 16, "bold") + for i in range(len(featured_shops)): + color = SHOPPING if i == current_shop_index else "gray" + dot = ctk.CTkLabel( + dots_frame, + text="●", + text_color=color, + font=("Helvetica", 16) + ) + dot.pack(side="left", padx=2) + + # Products Section + products_section = ctk.CTkFrame(main_content, fg_color="transparent") + products_section.pack(fill="both", expand=True, padx=20, pady=10) + + products_header = ctk.CTkFrame(products_section, fg_color="transparent") + products_header.pack(fill="x", pady=(0, 10)) + + products_title = ctk.CTkLabel( + products_header, + text="Featured Products", + font=("Helvetica", 20, "bold"), + text_color="white" ) - recommend_label.pack(pady=5) + products_title.pack(side="left") - bottom_products_frame = ctk.CTkScrollableFrame( - bottom_frame, fg_color="transparent", width=750, height=300 + products_frame = ctk.CTkScrollableFrame( + products_section, + fg_color="transparent", + height=400 ) - bottom_products_frame.pack(fill="both", expand=True, padx=10, pady=5) + products_frame.pack(fill="both", expand=True) 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 = ctk.CTkFrame( + parent, + fg_color=CARD_BG, + corner_radius=10, + width=200, + height=280 + ) card.pack_propagate(False) - image_label = ctk.CTkLabel(card, text="") - image_label.pack(pady=5) + # Product Image + image_frame = ctk.CTkFrame(card, fg_color="transparent", height=150) + image_frame.pack(fill="x", padx=10, pady=10) + image_frame.pack_propagate(False) + + image_label = ctk.CTkLabel(image_frame, text="") + image_label.pack(expand=True) 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: 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((70, 70)) + pil_img = Image.open(io.BytesIO(resp.content)).resize((120, 120)) 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"[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) - 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) + # Product Details + details_frame = ctk.CTkFrame(card, fg_color="transparent") + details_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10)) + + name_label = ctk.CTkLabel( + details_frame, + text=product_data.get("name", "No Name"), + font=("Helvetica", 14, "bold"), + wraplength=180, + justify="left" + ) + name_label.pack(anchor="w") + + price_label = ctk.CTkLabel( + details_frame, + text=f"₫ {product_data.get('price', 0.0):,.1f}", + font=("Helvetica", 14, "bold"), + text_color="#ff4242" + ) + price_label.pack(anchor="w", pady=5) + + view_button = ctk.CTkButton( + details_frame, + text="View Details", + command=lambda: switch_func("view_product", product_data), + fg_color=SHOPPING, + text_color="white", + height=30 + ) + view_button.pack(side="bottom", pady=5) return card @@ -225,28 +331,47 @@ def dashboard_frame(parent, switch_func, API_URL, token): widget.destroy() if not products: - ctk.CTkLabel(container, text="No products found.").pack(pady=10) + no_products_label = ctk.CTkLabel( + container, + text="No products available.", + font=("Helvetica", 14), + text_color="gray" + ) + no_products_label.pack(pady=20) 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 + max_cols = 4 + row = 0 + col = 0 + + for product in products: product_card = create_product_card(grid_frame, product) - product_card.grid(row=row, column=col, padx=5, pady=5) + product_card.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") + + col += 1 + if col >= max_cols: + col = 0 + row += 1 + + for i in range(max_cols): + grid_frame.grid_columnconfigure(i, weight=1) def fetch_featured_shops(): headers = {"Authorization": f"Bearer {token}"} try: resp = requests.get(f"{API_URL}/shops/list", headers=headers) if resp.status_code == 200: - shops = resp.json() - display_shops(shops[:5], top_shops_frame) + nonlocal featured_shops + featured_shops = resp.json()[:5] # Get top 5 shops for carousel + update_carousel() else: - messagebox.showerror("Error", f"Failed to fetch featured shops. Status: {resp.status_code}") + messagebox.showerror( + "Error", + f"Failed to fetch featured shops. Status: {resp.status_code}", + ) except Exception as e: messagebox.showerror("Error", f"Request error: {e}") @@ -256,9 +381,12 @@ def dashboard_frame(parent, switch_func, API_URL, token): resp = requests.get(f"{API_URL}/product/list", headers=headers) if resp.status_code == 200: products = resp.json() - display_products(products[:20], bottom_products_frame) + display_products(products[:12], products_frame) # Show top 12 products else: - messagebox.showerror("Error", f"Failed to fetch recommended products. Status: {resp.status_code}") + 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/product/create_product.py b/app/frontend/components/product/create_product.py index b95400c653b1672004fa9f84d811daf2acee967a..97e6b2c6aceb1c84d893cbac8bd11955cf2d174c 100644 --- a/app/frontend/components/product/create_product.py +++ b/app/frontend/components/product/create_product.py @@ -1,69 +1,99 @@ import customtkinter as ctk +from tkinter import filedialog, messagebox import requests -from tkinter import messagebox +import os -def product_frame(parent, switch_func, API_URL, token): +def create_product_frame(parent, switch_func, API_URL, token): + """ + Frame to create a new product. + Fields: Title, Price, Description, Stock, and Product Image. + On success, navigates back to the shop view. + """ frame = ctk.CTkFrame(parent) - - ctk.CTkLabel(frame, text="Product Management", font=("Helvetica", 18, "bold")).pack( - pady=10 + title = ctk.CTkLabel( + frame, text="Create New Product", font=("Helvetica", 18, "bold") ) + title.pack(pady=10) - ctk.CTkLabel(frame, text="Product Name:").pack(pady=5) - entry_name = ctk.CTkEntry(frame) - entry_name.pack(pady=5) + # Product Title + prod_title_label = ctk.CTkLabel(frame, text="Product Title:") + prod_title_label.pack(pady=5) + prod_title_entry = ctk.CTkEntry(frame, width=300) + prod_title_entry.pack(pady=5) - ctk.CTkLabel(frame, text="Price:").pack(pady=5) - entry_price = ctk.CTkEntry(frame) - entry_price.pack(pady=5) + # Price + price_label = ctk.CTkLabel(frame, text="Price:") + price_label.pack(pady=5) + price_entry = ctk.CTkEntry(frame, width=300) + price_entry.pack(pady=5) - ctk.CTkLabel(frame, text="Stock:").pack(pady=5) - entry_stock = ctk.CTkEntry(frame) - entry_stock.pack(pady=5) + # Description + desc_label = ctk.CTkLabel(frame, text="Description:") + desc_label.pack(pady=5) + desc_entry = ctk.CTkEntry(frame, width=300) + desc_entry.pack(pady=5) - def create_product(): - name = entry_name.get().strip() - price = entry_price.get().strip() - stock = entry_stock.get().strip() + # Stock Quantity + stock_label = ctk.CTkLabel(frame, text="Stock Quantity:") + stock_label.pack(pady=5) + stock_entry = ctk.CTkEntry(frame, width=300) + stock_entry.pack(pady=5) - if not name or not price or not stock: - messagebox.showwarning("Input Error", "All fields are required!") - return + # Product Image Upload + image_path = ctk.StringVar(value="No file selected") - try: - price = float(price) - stock = int(stock) - except ValueError: - messagebox.showwarning( - "Input Error", "Price must be a number and Stock must be an integer." - ) + def browse_image(): + filename = filedialog.askopenfilename( + title="Select Product Image", + filetypes=[("Image Files", "*.png;*.jpg;*.jpeg;*.gif")], + ) + if filename: + image_path.set(filename) + + browse_button = ctk.CTkButton(frame, text="Browse Image", command=browse_image) + browse_button.pack(pady=5) + image_label = ctk.CTkLabel(frame, textvariable=image_path) + image_label.pack(pady=5) + + def submit_product(): + title_val = prod_title_entry.get().strip() + price_val = price_entry.get().strip() + desc_val = desc_entry.get().strip() + stock_val = stock_entry.get().strip() + if not title_val or not price_val or not stock_val: + messagebox.showerror("Error", "Title, Price, and Stock are required") return + data = { + "name": title_val, + "price": price_val, + "description": desc_val, + "stock": stock_val, + } + files = {} + if os.path.isfile(image_path.get()): + files["images"] = open(image_path.get(), "rb") headers = {"Authorization": f"Bearer {token}"} - data = {"name": name, "price": price, "stock": stock} - try: - response = requests.post(f"{API_URL}/products", json=data, headers=headers) - - if response.status_code == 200: - messagebox.showinfo( - "Success", f"Product '{name}' created successfully!" - ) - entry_name.delete(0, "end") - entry_price.delete(0, "end") - entry_stock.delete(0, "end") + resp = requests.post( + f"{API_URL}/product/create", data=data, files=files, headers=headers + ) + if resp.status_code == 200: + messagebox.showinfo("Success", "Product created successfully!") + switch_func("view_shop") else: - error_message = response.json().get( - "detail", "Failed to create product" + messagebox.showerror( + "Error", + f"Failed to create product. Status: {resp.status_code}\n{resp.text}", ) - messagebox.showerror("Error", error_message) - except requests.exceptions.RequestException as e: - messagebox.showerror("Error", f"Failed to connect to server: {e}") + except Exception as e: + messagebox.showerror("Error", f"Request error: {e}") + finally: + if files: + files["images"].close() - ctk.CTkButton(frame, text="Create Product", command=create_product).pack(pady=15) - ctk.CTkButton(frame, text="Back", command=lambda: switch_func("view_shop")).pack( - pady=5 - ) + submit_button = ctk.CTkButton(frame, text="Create Product", command=submit_product) + submit_button.pack(pady=10) return frame diff --git a/app/frontend/components/product/view_product.py b/app/frontend/components/product/view_product.py new file mode 100644 index 0000000000000000000000000000000000000000..5fdca7de7839395162ba535846c1ab8b59ec744c --- /dev/null +++ b/app/frontend/components/product/view_product.py @@ -0,0 +1,180 @@ +import customtkinter as ctk +import requests +from PIL import Image, ImageTk +from tkinter import messagebox +import io + +BACKEND_HOST = "http://127.0.0.1:8000" # Adjust if needed + + +def fix_url(url): + """ + Fix the URL to ensure it points to the correct static file location. + """ + if url.startswith("http"): + return url + return f"{BACKEND_HOST}/static/{url}" + + +def view_shop_frame(parent, switch_func, API_URL, token, shop_id): + """ + CustomTkinter-based frame to display shop details and products. + """ + frame = ctk.CTkFrame(parent, fg_color="transparent") + + # Fetch shop details and products from the backend + headers = {"Authorization": f"Bearer {token}"} + try: + response = requests.get(f"{API_URL}/shops/get/{shop_id}", headers=headers) + if response.status_code == 200: + data = response.json() + shop_data = data.get("shop", {}) + products = data.get("products", []) + else: + messagebox.showerror("Error", f"Failed to fetch shop data. Status: {response.status_code}") + return frame + except Exception as e: + messagebox.showerror("Error", f"Request error: {e}") + return frame + + # ------------- TOP LAYER (Shop Image and Details) ------------- + top_layer = ctk.CTkFrame(frame, fg_color="transparent") + top_layer.place(relx=0, rely=0, relwidth=1, relheight=0.4) + + # Shop Image + shop_image_label = ctk.CTkLabel(top_layer, text="") + shop_image_label.place(relx=0.05, rely=0.1, relwidth=0.3, relheight=0.8) + + shop_image_url = shop_data.get("image_url") + if shop_image_url: + fixed_url = fix_url(shop_image_url) + try: + resp = requests.get(fixed_url) + if resp.status_code == 200: + pil_img = Image.open(io.BytesIO(resp.content)).resize((200, 200)) + tk_img = ImageTk.PhotoImage(pil_img) + shop_image_label.configure(image=tk_img, text="") + shop_image_label.image = tk_img + except Exception as e: + print(f"[DEBUG] Failed to load shop image: {e}") + + # Shop Details + shop_details_frame = ctk.CTkFrame(top_layer, fg_color="transparent") + shop_details_frame.place(relx=0.4, rely=0.1, relwidth=0.55, relheight=0.8) + + shop_name_label = ctk.CTkLabel( + shop_details_frame, + text=shop_data.get("name", "Shop Name"), + font=("Helvetica", 20, "bold"), + ) + shop_name_label.pack(pady=10) + + shop_description_label = ctk.CTkLabel( + shop_details_frame, + text=shop_data.get("description", "No description available."), + font=("Helvetica", 14), + wraplength=400, + justify="left", + ) + shop_description_label.pack(pady=5) + + shop_address_label = ctk.CTkLabel( + shop_details_frame, + text=f"Address: {shop_data.get('address', 'No address provided')}", + font=("Helvetica", 12), + ) + shop_address_label.pack(pady=5) + + # ------------- BOTTOM LAYER (Products) ------------- + bottom_layer = ctk.CTkFrame(frame, fg_color="transparent") + bottom_layer.place(relx=0, rely=0.4, relwidth=1, relheight=0.6) + + products_label = ctk.CTkLabel( + bottom_layer, text="Products", font=("Helvetica", 16, "bold") + ) + products_label.pack(pady=10) + + products_frame = ctk.CTkScrollableFrame( + bottom_layer, fg_color="transparent", width=750, height=300 + ) + products_frame.pack(fill="both", expand=True, padx=10, 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) + + image_label = ctk.CTkLabel(card, text="") + image_label.pack(pady=5) + + 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) + try: + resp = requests.get(fixed_url) + if resp.status_code == 200: + 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 + except Exception as e: + print(f"[DEBUG] Product image error: {e}") + + 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) + + 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) + + # Bind click event on card and its children. + def on_click(event, product=product_data): + switch_func("view_product", product) + + card.bind("<Button-1>", on_click) + 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 = 5 # 5 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) + + # Display products + display_products(products, products_frame) + + return frame \ No newline at end of file diff --git a/app/frontend/components/shop/create_shop.py b/app/frontend/components/shop/create_shop.py index e73fb5b2ab57b1e15f944f00fec33be8cfbc9644..6d5a991bb4b207be811c2b820be9725523b8b97b 100644 --- a/app/frontend/components/shop/create_shop.py +++ b/app/frontend/components/shop/create_shop.py @@ -1,71 +1,240 @@ import customtkinter as ctk +import requests +from PIL import Image, ImageTk from tkinter import messagebox, filedialog -import os -from utils.api_requests import create_shop_api # Import API function +# import io +SHOPPING = "#00c1ff" +DARK_BG = "#1f1f1f" +CARD_BG = "#2b2b2b" def create_shop_frame(parent, switch_func, API_URL, token): - frame = ctk.CTkFrame(parent) - frame.access_token = token + """ + Create shop page with a modern dark theme and improved layout. + """ + # Main container frame + frame = ctk.CTkFrame(parent, fg_color=DARK_BG) + + # --- TOP BAR --- + top_bar = ctk.CTkFrame(frame, fg_color=SHOPPING, height=50) + top_bar.pack(fill="x", side="top") + + top_label = ctk.CTkLabel( + top_bar, + text="Create Shop", + text_color="white", + font=("Helvetica", 20, "bold") + ) + top_label.pack(side="left", padx=25) + + def go_back(): + switch_func("dashboard") + + back_button = ctk.CTkButton( + top_bar, + text="← Back", + fg_color="transparent", + text_color="white", + hover_color="#0096ff", + command=go_back, + width=80, + height=35, + font=("Helvetica", 14) + ) + back_button.pack(side="right", padx=20, pady=7) + + # --- 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=250, fg_color=CARD_BG, corner_radius=15) + sidebar_frame.pack(side="left", fill="y", padx=20, pady=20) + sidebar_frame.pack_propagate(False) + + sidebar_title = ctk.CTkLabel( + sidebar_frame, + text="Menu", + font=("Helvetica", 18, "bold"), + text_color=SHOPPING + ) + sidebar_title.pack(pady=(20, 10)) + + def create_nav_button(text, command=None, is_active=False): + return ctk.CTkButton( + sidebar_frame, + text=text, + fg_color="transparent", + text_color="white", + hover_color="#3b3b3b", + command=command, + height=40, + font=("Helvetica", 14), + state="disabled" if is_active else "normal" + ) - def update_token(new_token): - frame.access_token = new_token + nav_dashboard = create_nav_button("Dashboard", go_back) + nav_dashboard.pack(fill="x", padx=15, pady=5) - frame.update_token = update_token + def open_profile(): + switch_func("user_details") - selected_file_path = [None] + nav_profile = create_nav_button("My Profile", open_profile) + nav_profile.pack(fill="x", padx=15, pady=5) - def select_file(): - file_path = filedialog.askopenfilename(title="Select Shop Image") - if file_path: - selected_file_path[0] = file_path - file_label.configure(text=os.path.basename(file_path)) - else: - selected_file_path[0] = None - file_label.configure(text="No file selected") + nav_orders = create_nav_button("My Orders", lambda: switch_func("user_orders")) + nav_orders.pack(fill="x", padx=15, pady=5) - def create_shop(): - name = entry_name.get() - description = entry_description.get() + become_owner = create_nav_button("Become Shop Owner", is_active=True) + become_owner.pack(fill="x", padx=15, pady=5) - if not name: - messagebox.showwarning("Input Error", "Shop name is required!") - return + # RIGHT CONTENT (Create Shop Form) + content_frame = ctk.CTkFrame(main_section, fg_color=CARD_BG, corner_radius=15) + content_frame.pack(side="left", fill="both", expand=True, padx=(0, 20), pady=20) - # Call the API function from shop_api.py - status_code, response_data = create_shop_api( - name, description, selected_file_path[0], API_URL, frame.access_token - ) + # Header + header_frame = ctk.CTkFrame(content_frame, fg_color="transparent") + header_frame.pack(fill="x", padx=30, pady=20) + + title_label = ctk.CTkLabel( + header_frame, + text="Create Your Shop", + font=("Helvetica", 24, "bold"), + text_color=SHOPPING + ) + title_label.pack(anchor="w") + + subtitle_label = ctk.CTkLabel( + header_frame, + text="Fill in your shop details to get started", + font=("Helvetica", 14), + text_color="gray" + ) + subtitle_label.pack(anchor="w", pady=(5, 0)) - if status_code == 200: - messagebox.showinfo("Shop Created", f"Shop '{name}' created successfully!") - else: - messagebox.showerror( - "Error", response_data.get("detail", "An error occurred") - ) + # Main Content + main_content = ctk.CTkFrame(content_frame, fg_color="transparent") + main_content.pack(fill="both", expand=True, padx=30, pady=(0, 20)) - ctk.CTkLabel(frame, text="Create Shop", font=("Helvetica", 18, "bold")).pack( - pady=10 + # LEFT FORM + form_frame = ctk.CTkFrame(main_content, fg_color="transparent") + form_frame.pack(side="left", fill="both", expand=True, padx=(0, 20)) + + def create_form_field(label_text, placeholder_text): + field_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + field_frame.pack(fill="x", pady=(0, 15)) + + label = ctk.CTkLabel( + field_frame, + text=label_text, + font=("Helvetica", 14), + text_color="white" + ) + label.pack(anchor="w") + + entry = ctk.CTkEntry( + field_frame, + height=40, + corner_radius=8, + placeholder_text=placeholder_text, + fg_color="#3b3b3b", + border_color=SHOPPING, + text_color="white" + ) + entry.pack(fill="x", pady=(5, 0)) + return entry + + shop_name_entry = create_form_field("Shop Name", "Enter your shop name...") + shop_description_entry = create_form_field("Shop Description", "Enter your shop description...") + shop_address_entry = create_form_field("Shop Address", "Enter your shop address...") + + # RIGHT PICTURE SECTION + pic_frame = ctk.CTkFrame(main_content, fg_color="transparent") + pic_frame.pack(side="left", fill="both", expand=True) + + pic_label = ctk.CTkLabel( + pic_frame, + text="Shop Logo", + font=("Helvetica", 18, "bold"), + text_color=SHOPPING ) + pic_label.pack(anchor="w", pady=(0, 10)) + + photo_frame = ctk.CTkFrame(pic_frame, fg_color="#3b3b3b", corner_radius=10) + photo_frame.pack(fill="x", pady=(0, 20)) - ctk.CTkLabel(frame, text="Shop Name:").pack(pady=5) - entry_name = ctk.CTkEntry(frame, placeholder_text="Enter shop name") - entry_name.pack(pady=5) + photo_label = ctk.CTkLabel( + photo_frame, + text="No image", + text_color="gray", + font=("Helvetica", 14) + ) + photo_label.pack(pady=20) - ctk.CTkLabel(frame, text="Description:").pack(pady=5) - entry_description = ctk.CTkEntry(frame, placeholder_text="Enter shop description") - entry_description.pack(pady=5) + def choose_photo(): + file_path = filedialog.askopenfilename( + title="Choose a Shop Logo", + filetypes=[("Image Files", "*.png *.jpg *.jpeg *.gif")], + ) + if file_path: + try: + pil_img = Image.open(file_path).resize((150, 150)) + tk_img = ImageTk.PhotoImage(pil_img) + photo_label.configure(image=tk_img, text="") + photo_label.image = tk_img + 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", + command=choose_photo, + corner_radius=8, + height=40, + font=("Helvetica", 14), + fg_color=SHOPPING, + hover_color="#0096ff", + ) + change_photo_button.pack(fill="x") - ctk.CTkButton(frame, text="Select Image", command=select_file).pack(pady=5) - file_label = ctk.CTkLabel(frame, text="No file selected") - file_label.pack(pady=5) + # Create Shop Button + def create_shop(): + shop_name = shop_name_entry.get().strip() + shop_description = shop_description_entry.get().strip() + shop_address = shop_address_entry.get().strip() - ctk.CTkButton( - frame, text="Create Shop", fg_color="green", command=create_shop - ).pack(pady=15) + if not all([shop_name, shop_description, shop_address]): + messagebox.showwarning("Input Error", "All fields are required!") + return - ctk.CTkButton( - frame, text="Back", fg_color="transparent", command=lambda: switch_func("login") - ).pack(pady=5) + headers = {"Authorization": f"Bearer {token}"} + payload = { + "name": shop_name, + "description": shop_description, + "address": shop_address, + } + + try: + resp = requests.post(f"{API_URL}/shops/create", headers=headers, json=payload) + if resp.status_code == 200: + messagebox.showinfo("Success", "Shop created successfully!") + switch_func("dashboard") + else: + messagebox.showerror("Error", "Failed to create shop.") + except Exception as e: + messagebox.showerror("Error", f"Request error: {e}") + + create_button = ctk.CTkButton( + form_frame, + text="Create Shop", + command=create_shop, + corner_radius=8, + height=45, + font=("Helvetica", 14, "bold"), + fg_color=SHOPPING, + hover_color="#0096ff", + ) + create_button.pack(fill="x", pady=(20, 0)) return frame diff --git a/app/frontend/components/shop/view_shop.py b/app/frontend/components/shop/view_shop.py index 3c4ae23dd957c0efca7bff9f95d27f4e84a68e22..2c951fa37be0d0151bc0df47aa6b5b4a7773f1fc 100644 --- a/app/frontend/components/shop/view_shop.py +++ b/app/frontend/components/shop/view_shop.py @@ -1,119 +1,279 @@ import customtkinter as ctk +import requests +from PIL import Image, ImageTk from tkinter import messagebox -from PIL import ImageTk -from utils.api_requests import ( - fetch_shop_details, - fetch_shop_products, - load_image_from_url, -) +import io +SHOPPING = "#00c1ff" # Adding theme color +BACKEND_HOST = "http://127.0.0.1:8000" # Adjust if needed -def view_shop_frame(parent, switch_func, API_URL, token): - frame = ctk.CTkFrame(parent) - # Title - title_label = ctk.CTkLabel(frame, text="Your Shop", font=("Helvetica", 18, "bold")) - title_label.pack(pady=10) +def fix_url(url): + """ + Fix the URL to ensure it points to the correct static file location. + """ + 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):] + return f"{BACKEND_HOST}/static/{url}" - # Shop Details - 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.pack(pady=5) +def view_shop_frame(parent, switch_func, API_URL, token, shop_id): + """ + CustomTkinter-based frame to display shop details and products. + """ + # Main frame + main_frame = ctk.CTkFrame(parent, fg_color="transparent") + main_frame.pack(fill="both", expand=True) - shop_image_label = ctk.CTkLabel(frame, text="") # Placeholder for shop image - shop_image_label.pack(pady=10) + # Create a container frame for content that can be refreshed + container_frame = ctk.CTkFrame(main_frame, fg_color="transparent") + container_frame.pack(fill="both", expand=True) - # Product List Section - product_list_frame = ctk.CTkFrame(frame) - product_list_frame.pack(fill="both", expand=True, padx=10, pady=10) + def refresh_data(new_shop_id=None, new_token=None): + """Refresh the shop view with new data""" + nonlocal token, shop_id + if new_token: + token = new_token + if new_shop_id: + shop_id = new_shop_id - def fetch_and_display_shop(): - """Fetch shop details and update UI""" - shop_data = fetch_shop_details(API_URL, token) + # Clear the container + for widget in container_frame.winfo_children(): + widget.destroy() - if "error" in shop_data: - messagebox.showerror("Error", shop_data["error"]) + if not shop_id: + print("[DEBUG] No shop ID provided") + no_shop_label = ctk.CTkLabel( + container_frame, + text="No shop selected", + font=("Helvetica", 16) + ) + no_shop_label.pack(pady=20) 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')}" + # Back button at the top + back_button = ctk.CTkButton( + container_frame, + text="← Back to Dashboard", + command=lambda: switch_func("dashboard"), + fg_color=SHOPPING, + text_color="white", + width=150 ) + back_button.pack(anchor="nw", padx=20, pady=10) - if "image_url" in shop_data and shop_data["image_url"]: - image = load_image_from_url(shop_data["image_url"]) - if image: - img_tk = ImageTk.PhotoImage(image) - shop_image_label.configure(image=img_tk, text="") - shop_image_label.image = img_tk # Prevent garbage collection + # Create a scrollable frame for all content + content_frame = ctk.CTkScrollableFrame(container_frame, fg_color="transparent") + content_frame.pack(fill="both", expand=True, padx=20, pady=10) - fetch_and_display_products(shop_data["id"]) + # Fetch shop details and products from the backend + headers = {"Authorization": f"Bearer {token}"} + try: + print(f"[DEBUG] Fetching shop data for ID: {shop_id}") + response = requests.get(f"{API_URL}/shops/get/{shop_id}", headers=headers) + print(f"[DEBUG] Response status: {response.status_code}") + + if response.status_code == 200: + data = response.json() + shop_data = data.get("shop", {}) + products = data.get("products", []) + print(f"[DEBUG] Shop data: {shop_data}") + print(f"[DEBUG] Number of products: {len(products)}") + else: + print(f"[DEBUG] Error response: {response.text}") + messagebox.showerror("Error", f"Failed to fetch shop data. Status: {response.status_code}") + return + except Exception as e: + print(f"[DEBUG] Error fetching shop data: {e}") + messagebox.showerror("Error", f"Request error: {e}") + return - def fetch_and_display_products(shop_id): - """Fetch products and display them in the UI""" - products = fetch_shop_products(API_URL, token, shop_id) + # Shop Header Section + shop_header = ctk.CTkFrame(content_frame, fg_color="#2b2b2b", corner_radius=10) + shop_header.pack(fill="x", padx=10, pady=10) - if "error" in products: - messagebox.showerror("Error", products["error"]) - return + # Shop Image and Details in header + header_left = ctk.CTkFrame(shop_header, fg_color="transparent") + header_left.pack(side="left", padx=20, pady=20) - for widget in product_list_frame.winfo_children(): - widget.destroy() + shop_image_label = ctk.CTkLabel(header_left, text="") + shop_image_label.pack(pady=5) - if not products: - ctk.CTkLabel( - product_list_frame, text="No products found.", font=("Helvetica", 12) - ).pack(pady=10) - return + # Load shop image + shop_image_url = shop_data.get("image_url") + if shop_image_url: + fixed_url = fix_url(shop_image_url) + try: + print(f"[DEBUG] Loading shop image from: {fixed_url}") + resp = requests.get(fixed_url) + if resp.status_code == 200: + pil_img = Image.open(io.BytesIO(resp.content)).resize((150, 150)) + tk_img = ImageTk.PhotoImage(pil_img) + shop_image_label.configure(image=tk_img, text="") + shop_image_label.image = tk_img + else: + print(f"[DEBUG] Failed to load shop image. Status: {resp.status_code}") + except Exception as e: + print(f"[DEBUG] Shop image error: {e}") + + # Shop Details + header_right = ctk.CTkFrame(shop_header, fg_color="transparent") + header_right.pack(side="left", fill="both", expand=True, padx=20, pady=20) - for product in products: - product_frame = ctk.CTkFrame(product_list_frame) - product_frame.pack(fill="x", padx=5, pady=5) + shop_name = ctk.CTkLabel( + header_right, + text=shop_data.get("name", "Shop Name"), + font=("Helvetica", 24, "bold"), + text_color=SHOPPING + ) + shop_name.pack(anchor="w") + + shop_description = ctk.CTkLabel( + header_right, + text=shop_data.get("description", "No description available."), + font=("Helvetica", 14), + wraplength=500, + justify="left" + ) + shop_description.pack(anchor="w", pady=10) + + shop_address = ctk.CTkLabel( + header_right, + text=f"📍 {shop_data.get('address', 'No address provided')}", + font=("Helvetica", 12), + justify="left" + ) + shop_address.pack(anchor="w") + + # Products Section + products_section = ctk.CTkFrame(content_frame, fg_color="transparent") + products_section.pack(fill="both", expand=True, padx=10, pady=10) + + # Products Header + products_header = ctk.CTkFrame(products_section, fg_color="transparent") + products_header.pack(fill="x", pady=10) + + products_title = ctk.CTkLabel( + products_header, + text="Products", + font=("Helvetica", 20, "bold"), + text_color=SHOPPING + ) + products_title.pack(side="left") + + products_count = ctk.CTkLabel( + products_header, + text=f"({len(products)} items)", + font=("Helvetica", 14) + ) + products_count.pack(side="left", padx=10) + + # Products Grid + products_grid = ctk.CTkFrame(products_section, fg_color="transparent") + products_grid.pack(fill="both", expand=True) + + def create_product_card(parent, product_data): + card = ctk.CTkFrame( + parent, + fg_color="#2b2b2b", + corner_radius=10, + width=200, + height=280 + ) + card.pack_propagate(False) # Product Image - img_label = ctk.CTkLabel(product_frame, text="") # Placeholder - img_label.pack(side="left", padx=5) + image_frame = ctk.CTkFrame(card, fg_color="transparent", height=150) + image_frame.pack(fill="x", padx=10, pady=10) + image_frame.pack_propagate(False) + + image_label = ctk.CTkLabel(image_frame, text="") + image_label.pack(expand=True) - if "images" in product and product["images"]: - image_url = product["images"][0]["image_url"] - image = load_image_from_url(image_url, size=(50, 50)) - if image: - img_tk = ImageTk.PhotoImage(image) - img_label.configure(image=img_tk, text="") - img_label.image = img_tk # Prevent garbage collection + # Load product image + 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) + try: + resp = requests.get(fixed_url) + if resp.status_code == 200: + pil_img = Image.open(io.BytesIO(resp.content)).resize((120, 120)) + tk_img = ImageTk.PhotoImage(pil_img) + image_label.configure(image=tk_img, text="") + image_label.image = tk_img + except Exception as e: + print(f"[DEBUG] Product image error: {e}") # Product Details - details_frame = ctk.CTkFrame(product_frame) - details_frame.pack(side="left", fill="x", expand=True, padx=10) + details_frame = ctk.CTkFrame(card, fg_color="transparent") + details_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10)) + + name_label = ctk.CTkLabel( + details_frame, + text=product_data.get("name", "No Name"), + font=("Helvetica", 14, "bold"), + wraplength=180, + justify="left" + ) + name_label.pack(anchor="w") + + price_label = ctk.CTkLabel( + details_frame, + text=f"₫ {product_data.get('price', 0.0):,.1f}", + font=("Helvetica", 14, "bold"), + text_color="#ff4242" + ) + price_label.pack(anchor="w", pady=5) - ctk.CTkLabel( - details_frame, text=product["name"], font=("Helvetica", 12, "bold") - ).pack(anchor="w") - ctk.CTkLabel( + # View Product Button + view_button = ctk.CTkButton( 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.pack(pady=10) - - # Back Button - back_button = ctk.CTkButton( - frame, text="Back", command=lambda: switch_func("login") - ) - back_button.pack(pady=10) - - # Fetch shop data on load - fetch_and_display_shop() - - return frame + text="View Details", + command=lambda p=product_data: switch_func("view_product", p), + fg_color=SHOPPING, + text_color="white", + height=30 + ) + view_button.pack(side="bottom", pady=5) + + return card + + if not products: + no_products_label = ctk.CTkLabel( + products_grid, + text="No products available in this shop.", + font=("Helvetica", 14), + text_color="gray" + ) + no_products_label.pack(pady=20) + else: + row = 0 + col = 0 + max_cols = 4 # Number of products per row + + for product in products: + product_card = create_product_card(products_grid, product) + product_card.grid(row=row, column=col, padx=10, pady=10, sticky="nsew") + + col += 1 + if col >= max_cols: + col = 0 + row += 1 + + # Configure grid columns to be equal width + for i in range(max_cols): + products_grid.grid_columnconfigure(i, weight=1) + + # Add the refresh_data method to the frame + main_frame.refresh_data = refresh_data + + # Initial load with empty shop_id + refresh_data() + + return main_frame \ No newline at end of file diff --git a/app/frontend/components/user_details.py b/app/frontend/components/user_details.py index a6b1a8fa0cee85e51d4b7b44e45a71d71e3a4cf5..7adf510e1d7a5f027a9ece8feb9d04486cea3727 100644 --- a/app/frontend/components/user_details.py +++ b/app/frontend/components/user_details.py @@ -5,171 +5,178 @@ from tkinter import messagebox, filedialog import io SHOPPING = "#00c1ff" +DARK_BG = "#1f1f1f" +CARD_BG = "#2b2b2b" + 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) + User details page with a modern dark theme and improved layout. """ + # Main container frame + frame = ctk.CTkFrame(parent, fg_color=DARK_BG) - # 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 --- + top_bar = ctk.CTkFrame(frame, fg_color=SHOPPING, height=50) top_bar.pack(fill="x", side="top") top_label = ctk.CTkLabel( - top_bar, - text="My Profile", - text_color="white", - font=("Helvetica", 16, "bold"), + top_bar, + text="My Profile", + text_color="white", + font=("Helvetica", 20, "bold") ) - top_label.pack(side="left", padx=20) + top_label.pack(side="left", padx=25) def go_back(): switch_func("dashboard") back_button = ctk.CTkButton( top_bar, - text="Back", - fg_color="white", - text_color="black", + text="← Back", + fg_color="transparent", + text_color="white", + hover_color="#0096ff", command=go_back, - width=60, - height=30, + width=80, + height=35, + font=("Helvetica", 14) ) - back_button.pack(side="right", padx=20, pady=5) + back_button.pack(side="right", padx=20, pady=7) - # ----------------- MAIN SECTION (Sidebar + Content) ----------------- + # --- 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") + # LEFT SIDEBAR + sidebar_frame = ctk.CTkFrame(main_section, width=250, fg_color=CARD_BG, corner_radius=15) + sidebar_frame.pack(side="left", fill="y", padx=20, pady=20) + sidebar_frame.pack_propagate(False) sidebar_title = ctk.CTkLabel( sidebar_frame, text="Menu", - font=("Helvetica", 14, "bold"), - text_color="white" + font=("Helvetica", 18, "bold"), + text_color=SHOPPING ) - sidebar_title.pack(pady=(10, 5)) + sidebar_title.pack(pady=(20, 10)) + + def create_nav_button(text, command=None, is_active=False): + return ctk.CTkButton( + sidebar_frame, + text=text, + fg_color="transparent", + text_color="white", + hover_color="#3b3b3b", + command=command, + height=40, + font=("Helvetica", 14), + state="disabled" if is_active else "normal" + ) - 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_dashboard = create_nav_button("Dashboard", go_back) + nav_dashboard.pack(fill="x", padx=15, 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_profile = create_nav_button("My Profile", is_active=True) + nav_profile.pack(fill="x", padx=15, 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) + nav_orders = create_nav_button("My Orders", lambda: switch_func("user_orders")) + nav_orders.pack(fill="x", padx=15, pady=5) + + become_owner = create_nav_button("Become Shop Owner", lambda: switch_func("create_shop")) + become_owner.pack(fill="x", padx=15, 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) + # RIGHT CONTENT (User Details Form) + content_frame = ctk.CTkFrame(main_section, fg_color=CARD_BG, corner_radius=15) + content_frame.pack(side="left", fill="both", expand=True, padx=(0, 20), pady=20) + + # Header + header_frame = ctk.CTkFrame(content_frame, fg_color="transparent") + header_frame.pack(fill="x", padx=30, pady=20) - # Title / Subtitle title_label = ctk.CTkLabel( - content_frame, - text="My Profile", - font=("Helvetica", 18, "bold"), - text_color="white" + header_frame, + text="Profile Information", + font=("Helvetica", 24, "bold"), + text_color=SHOPPING ) - title_label.pack(anchor="w", pady=(0, 5)) + title_label.pack(anchor="w") subtitle_label = ctk.CTkLabel( - content_frame, + header_frame, text="Manage your profile information to keep your account secure", - font=("Helvetica", 12), - text_color="#cccccc" + font=("Helvetica", 14), + text_color="gray" ) - subtitle_label.pack(anchor="w", pady=(0, 15)) + subtitle_label.pack(anchor="w", pady=(5, 0)) - # -- 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) + # Main Content + main_content = ctk.CTkFrame(content_frame, fg_color="transparent") + main_content.pack(fill="both", expand=True, padx=30, pady=(0, 20)) # LEFT FORM - form_frame = ctk.CTkFrame(right_main, fg_color="transparent") + form_frame = ctk.CTkFrame(main_content, 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 + def create_form_field(label_text, placeholder_text): + field_frame = ctk.CTkFrame(form_frame, fg_color="transparent") + field_frame.pack(fill="x", pady=(0, 15)) + + label = ctk.CTkLabel( + field_frame, + text=label_text, + font=("Helvetica", 14), + text_color="white" + ) + label.pack(anchor="w") + + entry = ctk.CTkEntry( + field_frame, + height=40, + corner_radius=8, + placeholder_text=placeholder_text, + fg_color="#3b3b3b", + border_color=SHOPPING, + text_color="white" + ) + entry.pack(fill="x", pady=(5, 0)) + return entry + + username_entry = create_form_field("Username", "Enter your username...") + name_entry = create_form_field("Name", "Enter your name...") + email_entry = create_form_field("Email", "Enter your email...") + phone_entry = create_form_field("Phone", "Enter your phone number...") + + # Gender Selection gender_frame = ctk.CTkFrame(form_frame, fg_color="transparent") - gender_frame.pack(anchor="w", pady=5) + gender_frame.pack(fill="x", pady=(0, 15)) + + ctk.CTkLabel( + gender_frame, + text="Gender", + font=("Helvetica", 14), + text_color="white" + ).pack(anchor="w") + + gender_var = ctk.StringVar(value="Male") + gender_options = ctk.CTkFrame(gender_frame, fg_color="transparent") + gender_options.pack(fill="x", pady=(5, 0)) - 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) + for gender in ["Male", "Female", "Other"]: + ctk.CTkRadioButton( + gender_options, + text=gender, + variable=gender_var, + value=gender, + text_color="white", + fg_color=SHOPPING, + hover_color="#0096ff" + ).pack(side="left", padx=(0, 20)) - # 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)) + birthday_entry = create_form_field("Date of Birth", "dd/mm/yyyy") # 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(), @@ -177,7 +184,7 @@ def user_details_frame(parent, switch_func, API_URL, token): "email": email_entry.get().strip(), "phone": phone_entry.get().strip(), "gender": gender_var.get(), - "birthday": birthday_entry.get().strip() + "birthday": birthday_entry.get().strip(), } try: resp = requests.put(f"{API_URL}/user/update", headers=headers, json=payload) @@ -190,45 +197,50 @@ def user_details_frame(parent, switch_func, API_URL, token): save_button = ctk.CTkButton( form_frame, - text="Save", - fg_color=SHOPPING, # #00c1ff - text_color="white", + text="Save Changes", command=save_profile, - width=80 + corner_radius=8, + height=45, + font=("Helvetica", 14, "bold"), + fg_color=SHOPPING, + hover_color="#0096ff", ) - save_button.pack(anchor="w", pady=(20, 10)) + save_button.pack(fill="x", pady=(20, 0)) # RIGHT PICTURE SECTION - pic_frame = ctk.CTkFrame(right_main, fg_color="transparent") + pic_frame = ctk.CTkFrame(main_content, 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 = ctk.CTkLabel( + pic_frame, + text="Profile Picture", + font=("Helvetica", 18, "bold"), + text_color=SHOPPING + ) 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)) + photo_frame = ctk.CTkFrame(pic_frame, fg_color="#3b3b3b", corner_radius=10) + photo_frame.pack(fill="x", pady=(0, 20)) + + photo_label = ctk.CTkLabel( + photo_frame, + text="No image", + text_color="gray", + font=("Helvetica", 14) + ) + photo_label.pack(pady=20) 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")] + filetypes=[("Image Files", "*.png *.jpg *.jpeg *.gif")], ) if file_path: - # Display chosen image try: - pil_img = Image.open(file_path).resize((100, 100)) + pil_img = Image.open(file_path).resize((150, 150)) 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.") @@ -236,18 +248,16 @@ def user_details_frame(parent, switch_func, API_URL, token): change_photo_button = ctk.CTkButton( pic_frame, text="Choose Image", - fg_color="white", - text_color="black", command=choose_photo, - width=80 + corner_radius=8, + height=40, + font=("Helvetica", 14), + fg_color=SHOPPING, + hover_color="#0096ff", ) - change_photo_button.pack(anchor="w") + change_photo_button.pack(fill="x") - # 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) @@ -264,14 +274,12 @@ def user_details_frame(parent, switch_func, API_URL, token): 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)) + pil_img = Image.open(io.BytesIO(resp_pic.content)).resize((150, 150)) tk_img = ImageTk.PhotoImage(pil_img) photo_label.configure(image=tk_img, text="") photo_label.image = tk_img diff --git a/app/frontend/components/user_orders.py b/app/frontend/components/user_orders.py index b71b34fc7f27dd6ebfcf97eb4b3b9ad1210b57a2..cbb83ea355c633cde9e52ff090bfb88d15f1ee5a 100644 --- a/app/frontend/components/user_orders.py +++ b/app/frontend/components/user_orders.py @@ -5,116 +5,220 @@ from tkinter import messagebox import io SHOPPING = "#00c1ff" +DARK_BG = "#1f1f1f" +CARD_BG = "#2b2b2b" + 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. + A modern user orders page that displays the products the user has purchased + along with associated shop details. """ - - # Main container frame with transparent background - frame = ctk.CTkFrame(parent, fg_color="transparent") + # Main container frame with dark background + frame = ctk.CTkFrame(parent, fg_color=DARK_BG) # ----------------- TOP BAR ----------------- - top_bar = ctk.CTkFrame(frame, fg_color=SHOPPING, height=40) + top_bar = ctk.CTkFrame(frame, fg_color=SHOPPING, height=50) top_bar.pack(fill="x", side="top") top_label = ctk.CTkLabel( - top_bar, - text="My Orders", - text_color="white", - font=("Helvetica", 16, "bold") + top_bar, + text="My Orders", + text_color="white", + font=("Helvetica", 20, "bold") ) - top_label.pack(side="left", padx=20) + top_label.pack(side="left", padx=25) def go_back(): switch_func("dashboard") back_button = ctk.CTkButton( top_bar, - text="Back", - fg_color="white", - text_color="black", + text="← Back", + fg_color="transparent", + text_color="white", + hover_color="#0096ff", command=go_back, - width=60, - height=30 + width=80, + height=35, + font=("Helvetica", 14) ) - back_button.pack(side="right", padx=20, pady=5) + back_button.pack(side="right", padx=20, pady=7) # ----------------- 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_frame = ctk.CTkFrame(main_section, width=250, fg_color=CARD_BG, corner_radius=15) + sidebar_frame.pack(side="left", fill="y", padx=20, pady=20) + sidebar_frame.pack_propagate(False) sidebar_title = ctk.CTkLabel( - sidebar_frame, - text="Menu", - font=("Helvetica", 14, "bold"), - text_color="white" + sidebar_frame, + text="Menu", + font=("Helvetica", 18, "bold"), + text_color=SHOPPING ) - sidebar_title.pack(pady=(10, 5)) + sidebar_title.pack(pady=(20, 10)) + + def create_nav_button(text, command=None, is_active=False): + return ctk.CTkButton( + sidebar_frame, + text=text, + fg_color="transparent", + text_color="white", + hover_color="#3b3b3b", + command=command, + height=40, + font=("Helvetica", 14), + state="disabled" if is_active else "normal" + ) + + nav_dashboard = create_nav_button("Dashboard", go_back) + nav_dashboard.pack(fill="x", padx=15, pady=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_profile = create_nav_button("My Profile", open_profile) + nav_profile.pack(fill="x", padx=15, 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) + nav_orders = create_nav_button("My Orders", is_active=True) + nav_orders.pack(fill="x", padx=15, 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) + content_frame = ctk.CTkFrame(main_section, fg_color=CARD_BG, corner_radius=15) + content_frame.pack(side="left", fill="both", expand=True, padx=(0, 20), pady=20) + + # Header + header_frame = ctk.CTkFrame(content_frame, fg_color="transparent") + header_frame.pack(fill="x", padx=30, pady=20) main_title_label = ctk.CTkLabel( - content_frame, - text="Your Orders", - font=("Helvetica", 18, "bold"), - text_color="white" + header_frame, + text="Order History", + font=("Helvetica", 24, "bold"), + text_color=SHOPPING ) - main_title_label.pack(anchor="w", pady=(0, 5)) + main_title_label.pack(anchor="w") subtitle_label = ctk.CTkLabel( - content_frame, - text="Review the products you have purchased.", - font=("Helvetica", 12), - text_color="#cccccc" + header_frame, + text="Review the products you have purchased", + font=("Helvetica", 14), + text_color="gray" + ) + subtitle_label.pack(anchor="w", pady=(5, 0)) + + # Orders List Container + orders_list_frame = ctk.CTkScrollableFrame( + content_frame, + fg_color="transparent", + corner_radius=0 ) - subtitle_label.pack(anchor="w", pady=(0, 15)) + orders_list_frame.pack(fill="both", expand=True, padx=30, pady=(0, 20)) + + def create_order_card(order_data): + product = order_data.get("product", {}) + order_date = order_data.get("order_date", "Unknown Date") + + card = ctk.CTkFrame(orders_list_frame, fg_color="#3b3b3b", corner_radius=10) + card.pack(fill="x", pady=5) + + # Left: Product image + image_frame = ctk.CTkFrame(card, fg_color="transparent", width=100, height=100) + image_frame.pack(side="left", padx=15, pady=15) + image_frame.pack_propagate(False) + + image_label = ctk.CTkLabel(image_frame, text="") + image_label.pack(expand=True) + + 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((80, 80)) + 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 + info_frame = ctk.CTkFrame(card, fg_color="transparent") + info_frame.pack(side="left", fill="both", expand=True, padx=15, pady=15) + + # Product Name + ctk.CTkLabel( + info_frame, + text=product.get("name", "No Name"), + font=("Helvetica", 16, "bold"), + text_color="white" + ).pack(anchor="w") + + # Price and Date + price = product.get("price", 0.0) + ctk.CTkLabel( + info_frame, + text=f"₫ {price:,.1f}", + font=("Helvetica", 14), + text_color=SHOPPING + ).pack(anchor="w", pady=(5, 0)) + + ctk.CTkLabel( + info_frame, + text=f"Ordered on: {order_date}", + font=("Helvetica", 12), + text_color="gray" + ).pack(anchor="w", pady=(5, 0)) + + # Shop Name + shop_id = product.get("shop_id") + shop_name_label = ctk.CTkLabel( + info_frame, + text="Shop: Loading...", + font=("Helvetica", 12), + text_color="gray" + ) + shop_name_label.pack(anchor="w", pady=(5, 0)) + + 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 + def view_order(): + messagebox.showinfo( + "Order Details", + f"View details for order of {product.get('name')}" + ) - # 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) + view_button = ctk.CTkButton( + info_frame, + text="View Order", + command=view_order, + corner_radius=8, + height=35, + font=("Helvetica", 12), + fg_color=SHOPPING, + hover_color="#0096ff", + ) + view_button.pack(anchor="e", pady=(10, 0)) - # ----------- Functions to fetch and display orders ----------- def fetch_orders(): """ Fetch the list of user orders from the API. @@ -123,104 +227,46 @@ def user_orders_frame(parent, switch_func, API_URL, 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) + orders = resp.json() + display_orders(orders) else: messagebox.showerror("Error", "Failed to fetch orders.") except Exception as ex: messagebox.showerror("Error", f"Request error: {ex}") - def display_orders(orders, container): + def display_orders(orders): """ Display each order with product and shop details. """ # Clear previous content - for widget in container.winfo_children(): + for widget in orders_list_frame.winfo_children(): widget.destroy() if not orders: - ctk.CTkLabel(container, text="No orders found.", text_color="white").pack(pady=10) - return + no_orders_frame = ctk.CTkFrame(orders_list_frame, fg_color="transparent") + no_orders_frame.pack(expand=True, pady=20) - 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", + no_orders_frame, + text="No orders found", + font=("Helvetica", 16), + text_color="gray" + ).pack() + + ctk.CTkButton( + no_orders_frame, + text="Start Shopping", + command=go_back, + corner_radius=8, + height=40, + font=("Helvetica", 14), fg_color=SHOPPING, - text_color="white", - command=view_order - ) - view_button.pack(anchor="e", pady=(5, 0)) + hover_color="#0096ff", + ).pack(pady=(20, 0)) + return + + for order in orders: + create_order_card(order) # Fetch orders when the frame loads fetch_orders() diff --git a/app/frontend/main.py b/app/frontend/main.py index 65b8f0af7e3ad2c8f48b1b343ae2719166ed899e..e53b707183752a43c62a1a7a4aad15a87447e778 100644 --- a/app/frontend/main.py +++ b/app/frontend/main.py @@ -3,24 +3,35 @@ from components.auth.login import login_frame from components.auth.register import register_frame 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.product.create_product import create_product_frame from components.admin.category import category_frame 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 +access_token = None # Global token -def switch_frame(frame_name, token=None): +def switch_frame(frame_name, *args): + """ + Switch between frames. + Optionally, additional data (like shop_data) can be passed as extra arguments. + """ global access_token - if token: - access_token = token + if args and args[0] and isinstance(args[0], str): + access_token = args[0] + + frame = frames.get(frame_name) + if frame is None: + print(f"Frame {frame_name} not found!") + return - frame = frames.get(frame_name, login) if hasattr(frame, "update_token"): frame.update_token(access_token) + if hasattr(frame, "refresh_data") and len(args) > 0: + frame.refresh_data(*args) + frame.tkraise() @@ -31,12 +42,15 @@ root = ctk.CTk() root.title("Shopping App") root.geometry("1000x800") -# Create Frames +# Create Frames with the token parameter where needed. login = login_frame(root, switch_frame, API_URL) register = register_frame(root, switch_frame, API_URL) -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) +create_shop = create_shop_frame( + root, switch_frame, API_URL, access_token +) # Accepts token +# Fix the parameter order for view_shop_frame +view_shop = view_shop_frame(root, switch_frame, API_URL, access_token, None) # Pass shop_id as None initially +product = create_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) user_details = user_details_frame(root, switch_frame, API_URL, access_token) @@ -55,11 +69,7 @@ frames = { } for frame in frames.values(): - frame.place( - relx=0, rely=0, relwidth=1, relheight=1 - ) # Adjusted height for full scaling + frame.place(relx=0, rely=0, relwidth=1, relheight=1) -# 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 d17146c43f5b66338f8acb17aa47c28740b969c0..59aa0806c59531d6ce2b760712be7a3adec561b5 100644 --- a/app/frontend/utils/api_requests.py +++ b/app/frontend/utils/api_requests.py @@ -88,13 +88,13 @@ def create_shop_api(name, description, file_path, api_url, access_token): # View Shop -def fetch_shop_details(api_url, token, shop_id=1): +def fetch_shop_details(api_url, token, shop_id): """ Fetches details of the shop owned by the logged-in user. :param api_url: Base API URL :param token: Authorization token - :param shop_id: ID of the shop (default = 1) + :param shop_id: ID of the shop :return: Dictionary containing shop details or error message """ headers = {"Authorization": f"Bearer {token}"}