From b02b661896dc99282382b7e6e49a22be969f902f Mon Sep 17 00:00:00 2001
From: nn2-minh <Nguyen12.Minh@live.uwe.ac.uk>
Date: Fri, 28 Mar 2025 22:30:40 +0700
Subject: [PATCH] Make change for product and shop also main to show img for
 frontend and make change for dashboard for prettier appearance

---
 app/backend/main.py                   |   9 +-
 app/backend/routes/product.py         |  69 +++---
 app/backend/routes/shop.py            |  36 ++--
 app/frontend/components/auth/login.py |   2 +-
 app/frontend/components/dashboard.py  | 290 +++++++++++++-------------
 5 files changed, 201 insertions(+), 205 deletions(-)

diff --git a/app/backend/main.py b/app/backend/main.py
index 658f0ca..9606ccc 100644
--- a/app/backend/main.py
+++ b/app/backend/main.py
@@ -4,13 +4,20 @@ import os
 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 
 from fastapi import FastAPI
+from fastapi.staticfiles import StaticFiles
 from backend.routes import auth, shop, product, category, search, order
 from backend.database import init_db
 from core.config import settings
 
 app = FastAPI(title="Shopping App", version="1.0.0", debug=settings.debug)
 
-# initialize database
+# ------------------- NEW: MOUNT STATIC FILES -------------------
+# Suppose your static files are located in "app/static"
+# Adjust the path if needed.
+static_dir_path = os.path.join(os.path.dirname(__file__), "..", "static")
+app.mount("/static", StaticFiles(directory=static_dir_path), name="static")
+
+# Initialize database
 init_db()
 
 # Include API routes
diff --git a/app/backend/routes/product.py b/app/backend/routes/product.py
index fcad951..30ca0e9 100644
--- a/app/backend/routes/product.py
+++ b/app/backend/routes/product.py
@@ -11,9 +11,7 @@ import os
 
 router = APIRouter()
 
-static_dir = os.path.join("app", "static")
-os.makedirs(static_dir, exist_ok=True)
-
+BACKEND_HOST = "http://127.0.0.1:8000"  # Or use settings.backend_url if available
 
 @router.post("/create", response_model=ProductRead)
 def create_product(
@@ -27,6 +25,12 @@ def create_product(
     session: Session = Depends(get_session),
     current_user: User = Depends(get_current_user),
 ):
+    shop = session.get(Shop, shop_id)
+    if not shop or shop.owner_id != current_user.id:
+        raise HTTPException(
+            status_code=403, detail="Unauthorized to create product for this shop"
+        )
+
     product = Product(
         name=name,
         description=description,
@@ -40,35 +44,31 @@ def create_product(
     session.commit()
     session.refresh(product)
 
-    shop = session.get(Shop, shop_id)
-    if not shop or shop.owner_id != current_user.id:
-        raise HTTPException(
-            status_code=403, detail="Unauthorized to create product for this shop"
-        )
-
-    # Directory structure: static/shop_{shop_name}/product_{product_name}/
+    # Directory: static/shop_{shop.name}/product_{product.name}/
     shop_dir = os.path.join(settings.static_dir, f"shop_{shop.name}")
     os.makedirs(shop_dir, exist_ok=True)
     product_dir = os.path.join(shop_dir, f"product_{product.name}")
     os.makedirs(product_dir, exist_ok=True)
 
-    # Handling multiple image uploads
     if images:
         for image in images:
             if image.filename:
                 file_location = os.path.join(product_dir, image.filename)
                 with open(file_location, "wb") as buffer:
                     shutil.copyfileobj(image.file, buffer)
-
-                # Save image record in ProductImage
-                product_image = ProductImage(
-                    product_id=product.id, image_url=file_location
-                )
+                relative_path = os.path.relpath(file_location, settings.static_dir)
+                relative_path = relative_path.replace("\\", "/")
+                public_url = f"{BACKEND_HOST}/static/{relative_path}"
+                product_image = ProductImage(product_id=product.id, image_url=public_url)
                 session.add(product_image)
                 session.commit()
-    else:  # Save default image
-        file_location = os.path.join(settings.static_dir, "default/default_product.png")
-        product_image = ProductImage(product_id=product.id, image_url=file_location)
+    else:
+        # Use default product image from static/default folder
+        default_path = os.path.join(settings.static_dir, "default", "default_product.png")
+        relative_path = os.path.relpath(default_path, settings.static_dir)
+        relative_path = relative_path.replace("\\", "/")
+        public_url = f"{BACKEND_HOST}/static/{relative_path}"
+        product_image = ProductImage(product_id=product.id, image_url=public_url)
         session.add(product_image)
         session.commit()
 
@@ -82,7 +82,6 @@ def read_all_products(order: str = "desc", session: Session = Depends(get_sessio
         if order == "desc"
         else func.count(OrderItem.id).asc()
     )
-
     products = (
         session.query(Product)
         .outerjoin(OrderItem, Product.id == OrderItem.product_id)
@@ -90,7 +89,6 @@ def read_all_products(order: str = "desc", session: Session = Depends(get_sessio
         .order_by(order_by)
         .all()
     )
-
     return products
 
 
@@ -120,28 +118,16 @@ def update_product(
             status_code=403, detail="Unauthorized to update this product"
         )
 
-    if name:
-        product.name = name
-    if description:
-        product.description = description
-    if price is not None:
-        product.price = price
-    if stock is not None:
-        product.stock = stock
-    if category_id is not None:
-        product.category_id = category_id
+    product.name = name
+    product.description = description
+    product.price = price
+    product.stock = stock
+    product.category_id = category_id
 
     session.add(product)
     session.commit()
     session.refresh(product)
 
-    product = session.get(Product, product_id)
-    if not product or product.shop.owner_id != current_user.id:
-        raise HTTPException(
-            status_code=403, detail="Unauthorized to update this product"
-        )
-
-    # Directory structure: static/shop_{shop_name}/product_{product_name}/
     shop = session.get(Shop, product.shop_id)
     shop_dir = os.path.join(settings.static_dir, f"shop_{shop.name}")
     os.makedirs(shop_dir, exist_ok=True)
@@ -154,8 +140,10 @@ def update_product(
                 file_location = os.path.join(product_dir, file.filename)
                 with open(file_location, "wb") as buffer:
                     shutil.copyfileobj(file.file, buffer)
-
-                image = ProductImage(product_id=product.id, image_url=file_location)
+                relative_path = os.path.relpath(file_location, settings.static_dir)
+                relative_path = relative_path.replace("\\", "/")
+                public_url = f"{BACKEND_HOST}/static/{relative_path}"
+                image = ProductImage(product_id=product.id, image_url=public_url)
                 session.add(image)
                 session.commit()
 
@@ -173,7 +161,6 @@ def delete_product(
         raise HTTPException(
             status_code=403, detail="Unauthorized to delete this product"
         )
-
     session.delete(product)
     session.commit()
     return {"message": "Product deleted successfully"}
diff --git a/app/backend/routes/shop.py b/app/backend/routes/shop.py
index a240d1b..d5a651a 100644
--- a/app/backend/routes/shop.py
+++ b/app/backend/routes/shop.py
@@ -11,6 +11,8 @@ import os
 
 router = APIRouter()
 
+# Define your backend host URL.
+BACKEND_HOST = "http://127.0.0.1:8000"  # Or use settings.backend_url if available
 
 @router.post("/create", response_model=ShopRead)
 def create_shop(
@@ -39,6 +41,7 @@ def create_shop(
     session.commit()
     session.refresh(shop)
 
+    # Create a folder in the static directory for the shop
     shop_dir = os.path.join(settings.static_dir, f"shop_{shop.name}")
     os.makedirs(shop_dir, exist_ok=True)
 
@@ -46,13 +49,20 @@ def create_shop(
         file_location = os.path.join(shop_dir, file.filename)
         with open(file_location, "wb") as buffer:
             shutil.copyfileobj(file.file, buffer)
-        shop.image_url = file_location
+        relative_path = os.path.relpath(file_location, settings.static_dir)
+        relative_path = relative_path.replace("\\", "/")
+        public_url = f"{BACKEND_HOST}/static/{relative_path}"
+        shop.image_url = public_url
     else:
-        shop.image_url = os.path.join(settings.static_dir, "default/default_shop.png")
+        # Use default shop image from static/default folder
+        default_path = os.path.join(settings.static_dir, "default", "default_shop.png")
+        relative_path = os.path.relpath(default_path, settings.static_dir)
+        relative_path = relative_path.replace("\\", "/")
+        public_url = f"{BACKEND_HOST}/static/{relative_path}"
+        shop.image_url = public_url
 
     session.commit()
     session.refresh(shop)
-
     return shop
 
 
@@ -61,7 +71,6 @@ def get_all_shops(order: str = "desc", session: Session = Depends(get_session)):
     order_by = (
         func.count(Order.id).desc() if order == "desc" else func.count(Order.id).asc()
     )
-
     shops = (
         session.query(Shop)
         .outerjoin(Order, Shop.id == Order.shop_id)
@@ -69,7 +78,6 @@ def get_all_shops(order: str = "desc", session: Session = Depends(get_session)):
         .order_by(order_by)
         .all()
     )
-
     return shops
 
 
@@ -94,10 +102,9 @@ def update_shop(
     shop = session.get(Shop, shop_id)
     if not shop:
         raise HTTPException(status_code=404, detail="Shop not found")
-
-    # Ensure the current user is the shop owner
     if shop.owner_id != current_user.id:
         raise HTTPException(status_code=403, detail="Unauthorized to update this shop")
+    
     if name:
         shop.name = name
     if description:
@@ -118,14 +125,20 @@ def update_shop(
         file_location = os.path.join(shop_dir, file.filename)
         with open(file_location, "wb") as buffer:
             shutil.copyfileobj(file.file, buffer)
-        shop.image_url = file_location
+        relative_path = os.path.relpath(file_location, settings.static_dir)
+        relative_path = relative_path.replace("\\", "/")
+        public_url = f"{BACKEND_HOST}/static/{relative_path}"
+        shop.image_url = public_url
     else:
-        shop.image_url = os.path.join(settings.static_dir, "default/default_shop.png")
+        default_path = os.path.join(settings.static_dir, "default", "default_shop.png")
+        relative_path = os.path.relpath(default_path, settings.static_dir)
+        relative_path = relative_path.replace("\\", "/")
+        public_url = f"{BACKEND_HOST}/static/{relative_path}"
+        shop.image_url = public_url
 
     session.add(shop)
     session.commit()
     session.refresh(shop)
-
     return shop
 
 
@@ -138,11 +151,8 @@ def delete_shop(
     shop = session.get(Shop, shop_id)
     if not shop:
         raise HTTPException(status_code=404, detail="Shop not found")
-
-    # Ensure the current user is the shop owner
     if shop.owner_id != current_user.id:
         raise HTTPException(status_code=403, detail="Unauthorized to delete this shop")
-
     session.delete(shop)
     session.commit()
     return {"message": "Shop deleted successfully"}
diff --git a/app/frontend/components/auth/login.py b/app/frontend/components/auth/login.py
index dc2d30d..31d2560 100644
--- a/app/frontend/components/auth/login.py
+++ b/app/frontend/components/auth/login.py
@@ -15,7 +15,7 @@ def login_frame(parent, switch_func, API_URL):
     left_frame = ctk.CTkFrame(container, fg_color="transparent")
     left_frame.grid(row=0, column=0, sticky="nsew")
 
-    image_path = "app/static/front_end_img/login.jpg"
+    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)
diff --git a/app/frontend/components/dashboard.py b/app/frontend/components/dashboard.py
index f6c1af4..f5707c9 100644
--- a/app/frontend/components/dashboard.py
+++ b/app/frontend/components/dashboard.py
@@ -5,10 +5,29 @@ from tkinter import messagebox
 import io
 
 SHOPPING = "#00c1ff"
-
+BACKEND_HOST = "http://127.0.0.1:8000"  # Adjust if needed
+
+def fix_url(url):
+    """
+    If the provided URL does not start with http, assume it's a relative path.
+    Remove any unwanted prefix (e.g., "app/static/") and prepend the public URL.
+    """
+    if url.startswith("http"):
+        return url
+    # If the URL starts with "app/static/", remove that part.
+    prefix = "app/static/"
+    if url.startswith(prefix):
+        url = url[len(prefix):]
+    # Prepend the public URL
+    return f"{BACKEND_HOST}/static/{url}"
 
 def dashboard_frame(parent, switch_func, API_URL, token):
-    # Main container frame
+    """
+    Main dashboard UI:
+      - Header (top bar) with logo, search, user, cart
+      - Middle area with horizontal scroll of top shops
+      - Bottom area with grid layout of recommended products
+    """
     frame = ctk.CTkFrame(parent, fg_color="transparent")
 
     # ------------- HEADER (Top Bar) -------------
@@ -38,16 +57,16 @@ def dashboard_frame(parent, switch_func, API_URL, token):
             return
         try:
             headers = {"Authorization": f"Bearer {token}"}
-            resp = requests.get(f"{API_URL}/product?search={keyword}", headers=headers)
+            # Adjust endpoint/params as needed
+            resp = requests.get(f"{API_URL}/search?name={keyword}&search_type=products", headers=headers)
             if resp.status_code == 200:
                 products = resp.json()
                 display_products(products, bottom_products_frame)
             else:
-                messagebox.showerror("Error", "Failed to fetch search results.")
+                messagebox.showerror("Error", f"Search failed. Status code: {resp.status_code}")
         except Exception as e:
             messagebox.showerror("Error", f"Request error: {e}")
 
-    # Search button
     search_button = ctk.CTkButton(
         header_frame,
         text="Search",
@@ -59,89 +78,80 @@ def dashboard_frame(parent, switch_func, API_URL, token):
 
     # ------------- USER & CART ICONS (Top-Right) -------------
     def open_user_details():
-        # Switch to user_details.py screen or run it
         switch_func("user_details")
-        # Alternatively, use os.system("python user_details.py")
 
     def open_cart_details():
-        # Switch to cart_shopping.py screen or run it
-        switch_func("user_orders")
-        # Alternatively, use os.system("python cart_shopping.py")
-
-    # Try loading icon images; update paths as needed.
-    try:
-        user_image = Image.open("path/to/user_icon.png").resize((30, 30))
-        user_icon = ImageTk.PhotoImage(user_image)
-    except Exception as e:
-        print(f"User icon load error: {e}")
-        user_icon = None
-
-    try:
-        cart_image = Image.open("path/to/cart_icon.png").resize((30, 30))
-        cart_icon = ImageTk.PhotoImage(cart_image)
-    except Exception as e:
-        print(f"Cart icon load error: {e}")
-        cart_icon = None
-
-    if user_icon:
-        user_button = ctk.CTkButton(
-            header_frame,
-            image=user_icon,
-            text="",
-            fg_color="transparent",
-            command=open_user_details,
-        )
-        switch_func("user_details")
-        user_button.image = user_icon
-        user_button.place(relx=0.82, rely=0.25, relwidth=0.08, relheight=0.5)
-    else:
-        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)
-
-    if cart_icon:
-        cart_button = ctk.CTkButton(
-            header_frame,
-            image=cart_icon,
-            text="",
-            fg_color="transparent",
-            command=open_cart_details,
-        )
         switch_func("user_orders")
-        cart_button.image = cart_icon
-        cart_button.place(relx=0.91, rely=0.25, relwidth=0.08, relheight=0.5)
-    else:
-        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)
+
+    user_button = ctk.CTkButton(
+        header_frame,
+        text="User",
+        fg_color="white",
+        text_color="black",
+        command=open_user_details,
+    )
+    user_button.place(relx=0.82, rely=0.25, relwidth=0.08, relheight=0.5)
+
+    cart_button = ctk.CTkButton(
+        header_frame,
+        text="Cart",
+        fg_color="white",
+        text_color="black",
+        command=open_cart_details,
+    )
+    cart_button.place(relx=0.91, rely=0.25, relwidth=0.08, relheight=0.5)
 
     # ------------- MIDDLE (Featured/Top Shops) -------------
     middle_frame = ctk.CTkFrame(frame, fg_color="transparent")
     middle_frame.place(relx=0, rely=0.1, relwidth=1, relheight=0.25)
 
-    # Section Title
     featured_label = ctk.CTkLabel(
         middle_frame, text="TOP SHOP", font=("Helvetica", 16, "bold")
     )
     featured_label.pack(pady=5)
 
-    # A frame to hold the featured shops
-    top_shops_frame = ctk.CTkFrame(middle_frame, fg_color="transparent")
+    top_shops_frame = ctk.CTkScrollableFrame(
+        middle_frame, fg_color="transparent", width=750, height=150
+    )
     top_shops_frame.pack(fill="both", expand=True, padx=10, pady=5)
 
+    def create_shop_card(parent, shop_data):
+        """
+        Create a card for a shop with its image on top and name below.
+        """
+        card = ctk.CTkFrame(parent, corner_radius=5, fg_color="#2b2b2b", width=100, height=130)
+        card.pack_propagate(False)
+
+        image_label = ctk.CTkLabel(card, text="")
+        image_label.pack(pady=5)
+
+        # Use "image_url" as returned by the backend, then fix it if needed.
+        image_url = shop_data.get("image_url")
+        if image_url:
+            fixed_url = fix_url(image_url)
+            print(f"[DEBUG] Fetching shop image from: {fixed_url}")
+            try:
+                resp = requests.get(fixed_url)
+                print(f"[DEBUG] Status code for shop image: {resp.status_code}")
+                if resp.status_code == 200:
+                    pil_img = Image.open(io.BytesIO(resp.content)).resize((60, 60))
+                    tk_img = ImageTk.PhotoImage(pil_img)
+                    image_label.configure(image=tk_img, text="")
+                    image_label.image = tk_img
+                else:
+                    print(f"[DEBUG] Failed to fetch shop image. Status: {resp.status_code}")
+            except Exception as e:
+                print(f"[DEBUG] Shop image error: {e}")
+        else:
+            print(f"[DEBUG] No 'image_url' found for shop: {shop_data.get('name')}")
+
+        name_text = shop_data.get("name", "No Name")
+        name_label = ctk.CTkLabel(card, text=name_text, font=("Helvetica", 11, "bold"), wraplength=90)
+        name_label.pack()
+
+        return card
+
     def display_shops(shops, container):
-        """Given a list of shop dicts, display them in the given container."""
-        # Clear old widgets
         for widget in container.winfo_children():
             widget.destroy()
 
@@ -149,59 +159,68 @@ def dashboard_frame(parent, switch_func, API_URL, token):
             ctk.CTkLabel(container, text="No shops found.").pack(pady=10)
             return
 
-        # Display shops in a vertical list
-        for shop in shops:
-            sframe = ctk.CTkFrame(container, corner_radius=5, fg_color="#2b2b2b")
-            sframe.pack(fill="x", padx=5, pady=5)
+        inner_frame = ctk.CTkFrame(container, fg_color="transparent")
+        inner_frame.pack(side="top", fill="both", expand=True)
 
-            # Shop image/logo on left
-            image_label = ctk.CTkLabel(sframe, text="")
-            image_label.pack(side="left", padx=5, pady=5)
+        for shop in shops:
+            shop_card = create_shop_card(inner_frame, shop)
+            shop_card.pack(side="left", padx=5, pady=5)
 
-            # Load shop logo if available (assumes key "logo_url")
-            logo_url = shop.get("logo_url")
-            if logo_url:
-                try:
-                    resp = requests.get(logo_url)
-                    if resp.status_code == 200:
-                        pil_img = Image.open(io.BytesIO(resp.content)).resize((50, 50))
-                        tk_img = ImageTk.PhotoImage(pil_img)
-                        image_label.configure(image=tk_img, text="")
-                        image_label.image = tk_img
-                except Exception as e:
-                    print(f"Shop image error: {e}")
-
-            # Shop info on right
-            info_frame = ctk.CTkFrame(sframe, fg_color="transparent")
-            info_frame.pack(side="left", fill="both", expand=True, padx=10)
-
-            ctk.CTkLabel(
-                info_frame,
-                text=shop.get("name", "No Name"),
-                font=("Helvetica", 13, "bold"),
-            ).pack(anchor="w")
-            # Optionally add more shop details, for example a rating if available:
-            if shop.get("rating"):
-                ctk.CTkLabel(info_frame, text=f"Rating: {shop['rating']}").pack(
-                    anchor="w"
-                )
-
-    # ------------- BOTTOM (Recommendations - Products) -------------
+    # ------------- BOTTOM (Products) -------------
     bottom_frame = ctk.CTkFrame(frame, fg_color="transparent")
     bottom_frame.place(relx=0, rely=0.35, relwidth=1, relheight=0.65)
 
-    # Section Title
     recommend_label = ctk.CTkLabel(
         bottom_frame, text="TODAY'S RECOMMENDATIONS", font=("Helvetica", 16, "bold")
     )
     recommend_label.pack(pady=5)
 
-    bottom_products_frame = ctk.CTkFrame(bottom_frame, fg_color="transparent")
+    bottom_products_frame = ctk.CTkScrollableFrame(
+        bottom_frame, fg_color="transparent", width=750, height=300
+    )
     bottom_products_frame.pack(fill="both", expand=True, padx=10, pady=5)
 
+    def 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)
+                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))
+                        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)
+
+        return card
+
     def display_products(products, container):
-        """Given a list of product dicts, display them in the given container."""
-        # Clear old widgets
         for widget in container.winfo_children():
             widget.destroy()
 
@@ -209,64 +228,37 @@ def dashboard_frame(parent, switch_func, API_URL, token):
             ctk.CTkLabel(container, text="No products found.").pack(pady=10)
             return
 
-        # Display products in a vertical list
-        for product in products:
-            pframe = ctk.CTkFrame(container, corner_radius=5, fg_color="#2b2b2b")
-            pframe.pack(fill="x", padx=5, pady=5)
+        grid_frame = ctk.CTkFrame(container, fg_color="transparent")
+        grid_frame.pack(fill="both", expand=True)
 
-            # Product image on left
-            image_label = ctk.CTkLabel(pframe, text="")
-            image_label.pack(side="left", padx=5, pady=5)
-
-            # Load first image if available
-            if product.get("images"):
-                try:
-                    img_url = product["images"][0]["image_url"]
-                    resp = requests.get(img_url)
-                    if resp.status_code == 200:
-                        pil_img = Image.open(io.BytesIO(resp.content)).resize((50, 50))
-                        tk_img = ImageTk.PhotoImage(pil_img)
-                        image_label.configure(image=tk_img, text="")
-                        image_label.image = tk_img
-                except Exception as e:
-                    print(f"Product image error: {e}")
-
-            # Product info on right
-            info_frame = ctk.CTkFrame(pframe, fg_color="transparent")
-            info_frame.pack(side="left", fill="both", expand=True, padx=10)
-
-            ctk.CTkLabel(
-                info_frame,
-                text=product.get("name", "No Name"),
-                font=("Helvetica", 13, "bold"),
-            ).pack(anchor="w")
-            price = product.get("price", 0.0)
-            ctk.CTkLabel(info_frame, text=f"Price: {price:.2f}").pack(anchor="w")
+        columns = 7  # 7 products per row
+        for idx, product in enumerate(products):
+            row = idx // columns
+            col = idx % columns
+            product_card = create_product_card(grid_frame, product)
+            product_card.grid(row=row, column=col, padx=5, pady=5)
 
     def fetch_featured_shops():
-        """Fetch some 'featured' or 'top' shops for the middle section."""
         headers = {"Authorization": f"Bearer {token}"}
         try:
             resp = requests.get(f"{API_URL}/shops/list", headers=headers)
             if resp.status_code == 200:
                 shops = resp.json()
-                # Slice the list to display only a handful of top shops
                 display_shops(shops[:5], top_shops_frame)
             else:
-                messagebox.showerror("Error", "Failed to fetch featured shops.")
+                messagebox.showerror("Error", f"Failed to fetch featured shops. Status: {resp.status_code}")
         except Exception as e:
             messagebox.showerror("Error", f"Request error: {e}")
 
     def fetch_recommendations():
-        """Fetch recommended products for the bottom section."""
         headers = {"Authorization": f"Bearer {token}"}
         try:
             resp = requests.get(f"{API_URL}/product/list", headers=headers)
             if resp.status_code == 200:
                 products = resp.json()
-                display_products(products, bottom_products_frame)
+                display_products(products[:20], bottom_products_frame)
             else:
-                messagebox.showerror("Error", "Failed to fetch recommended products.")
+                messagebox.showerror("Error", f"Failed to fetch recommended products. Status: {resp.status_code}")
         except Exception as e:
             messagebox.showerror("Error", f"Request error: {e}")
 
-- 
GitLab