From 8ec0d59e756c3285d53f4582d23b6c2a55d6a023 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 03:38:55 +0000 Subject: [PATCH] Refactor user lists --- src/components/lists/ListDisplay.tsx | 154 +++++++++ src/components/lists/ListItemEditor.tsx | 221 +++++++++++++ src/components/lists/ListSearch.tsx | 177 ++++++++++ src/components/lists/UserListManager.tsx | 310 ++++++++++++++++++ src/integrations/supabase/types.ts | 45 +++ src/types/database.ts | 25 ++ ...6_549a9c8b-5621-4c5e-921a-b4d9f37d7e60.sql | 109 ++++++ 7 files changed, 1041 insertions(+) create mode 100644 src/components/lists/ListDisplay.tsx create mode 100644 src/components/lists/ListItemEditor.tsx create mode 100644 src/components/lists/ListSearch.tsx create mode 100644 src/components/lists/UserListManager.tsx create mode 100644 supabase/migrations/20251002033636_549a9c8b-5621-4c5e-921a-b4d9f37d7e60.sql diff --git a/src/components/lists/ListDisplay.tsx b/src/components/lists/ListDisplay.tsx new file mode 100644 index 00000000..b698f652 --- /dev/null +++ b/src/components/lists/ListDisplay.tsx @@ -0,0 +1,154 @@ +import { useState, useEffect } from "react"; +import { UserTopList, UserTopListItem, Park, Ride, Company } from "@/types/database"; +import { supabase } from "@/integrations/supabase/client"; +import { Link } from "react-router-dom"; +import { Badge } from "@/components/ui/badge"; + +interface ListDisplayProps { + list: UserTopList; +} + +interface EnrichedListItem extends UserTopListItem { + entity?: Park | Ride | Company; +} + +export function ListDisplay({ list }: ListDisplayProps) { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchItemsWithEntities(); + }, [list.id]); + + const fetchItemsWithEntities = async () => { + setLoading(true); + + // First, get the list items + const { data: itemsData, error: itemsError } = await supabase + .from("user_top_list_items") + .select("*") + .eq("list_id", list.id) + .order("position", { ascending: true }); + + if (itemsError) { + console.error("Error fetching items:", itemsError); + setLoading(false); + return; + } + + // Then, fetch the entities for each item + const enrichedItems = await Promise.all( + (itemsData as UserTopListItem[]).map(async (item) => { + let entity = null; + + if (item.entity_type === "park") { + const { data } = await supabase + .from("parks") + .select("id, name, slug, park_type, location_id") + .eq("id", item.entity_id) + .single(); + entity = data; + } else if (item.entity_type === "ride") { + const { data } = await supabase + .from("rides") + .select("id, name, slug, category, park_id") + .eq("id", item.entity_id) + .single(); + entity = data; + } else if (item.entity_type === "company") { + const { data } = await supabase + .from("companies") + .select("id, name, slug, company_type") + .eq("id", item.entity_id) + .single(); + entity = data; + } + + return { ...item, entity }; + }) + ); + + setItems(enrichedItems); + setLoading(false); + }; + + const getEntityUrl = (item: EnrichedListItem) => { + if (!item.entity) return "#"; + + const entity = item.entity as any; + + if (item.entity_type === "park") { + return `/parks/${entity.slug}`; + } else if (item.entity_type === "ride") { + // We need park slug for rides + return `/rides/${entity.slug}`; + } else if (item.entity_type === "company") { + return `/companies/${entity.slug}`; + } + + return "#"; + }; + + if (loading) { + return
Loading...
; + } + + if (items.length === 0) { + return ( +
+ This list is empty. Click "Edit" to add items. +
+ ); + } + + return ( +
    + {items.map((item, index) => ( +
  1. + + {index + 1}. + +
    + {item.entity ? ( + + {(item.entity as any).name} + + ) : ( + + [Deleted or unavailable] + + )} +
    + + {item.entity_type} + + {item.entity && item.entity_type === "park" && ( + + {(item.entity as Park).park_type} + + )} + {item.entity && item.entity_type === "ride" && ( + + {(item.entity as Ride).category} + + )} + {item.entity && item.entity_type === "company" && ( + + {(item.entity as Company).company_type} + + )} +
    + {item.notes && ( +

    + "{item.notes}" +

    + )} +
    +
  2. + ))} +
+ ); +} diff --git a/src/components/lists/ListItemEditor.tsx b/src/components/lists/ListItemEditor.tsx new file mode 100644 index 00000000..68daf859 --- /dev/null +++ b/src/components/lists/ListItemEditor.tsx @@ -0,0 +1,221 @@ +import { useState, useEffect } from "react"; +import { UserTopList, UserTopListItem } from "@/types/database"; +import { supabase } from "@/integrations/supabase/client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { GripVertical, Trash2, Plus } from "lucide-react"; +import { toast } from "sonner"; +import { ListSearch } from "./ListSearch"; + +interface ListItemEditorProps { + list: UserTopList; + onUpdate: () => void; + onClose: () => void; +} + +export function ListItemEditor({ list, onUpdate, onClose }: ListItemEditorProps) { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [showSearch, setShowSearch] = useState(false); + + useEffect(() => { + fetchItems(); + }, [list.id]); + + const fetchItems = async () => { + setLoading(true); + const { data, error } = await supabase + .from("user_top_list_items") + .select("*") + .eq("list_id", list.id) + .order("position", { ascending: true }); + + if (error) { + toast.error("Failed to load list items"); + console.error(error); + } else { + setItems(data as UserTopListItem[]); + } + setLoading(false); + }; + + const handleAddItem = async (entityType: string, entityId: string, entityName: string) => { + const newPosition = items.length + 1; + + const { error } = await supabase + .from("user_top_list_items") + .insert({ + list_id: list.id, + entity_type: entityType, + entity_id: entityId, + position: newPosition, + }); + + if (error) { + if (error.code === "23505") { + toast.error("This item is already in your list"); + } else { + toast.error("Failed to add item"); + console.error(error); + } + } else { + toast.success(`Added ${entityName} to list`); + fetchItems(); + onUpdate(); + setShowSearch(false); + } + }; + + const handleRemoveItem = async (itemId: string) => { + const { error } = await supabase + .from("user_top_list_items") + .delete() + .eq("id", itemId); + + if (error) { + toast.error("Failed to remove item"); + console.error(error); + } else { + toast.success("Item removed"); + // Reorder remaining items + const remainingItems = items.filter(i => i.id !== itemId); + await reorderItems(remainingItems); + fetchItems(); + onUpdate(); + } + }; + + const handleUpdateNotes = async (itemId: string, notes: string) => { + const { error } = await supabase + .from("user_top_list_items") + .update({ notes }) + .eq("id", itemId); + + if (error) { + toast.error("Failed to update notes"); + console.error(error); + } else { + setItems(items.map(i => i.id === itemId ? { ...i, notes } : i)); + } + }; + + const handleMoveItem = async (itemId: string, direction: "up" | "down") => { + const currentIndex = items.findIndex(i => i.id === itemId); + if ( + (direction === "up" && currentIndex === 0) || + (direction === "down" && currentIndex === items.length - 1) + ) { + return; + } + + const newItems = [...items]; + const swapIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1; + [newItems[currentIndex], newItems[swapIndex]] = [newItems[swapIndex], newItems[currentIndex]]; + + await reorderItems(newItems); + setItems(newItems); + onUpdate(); + }; + + const reorderItems = async (orderedItems: UserTopListItem[]) => { + const updates = orderedItems.map((item, index) => ({ + id: item.id, + position: index + 1, + })); + + for (const update of updates) { + await supabase + .from("user_top_list_items") + .update({ position: update.position }) + .eq("id", update.id); + } + }; + + if (loading) { + return
Loading items...
; + } + + return ( +
+
+

Edit List Items

+
+ + +
+
+ + {showSearch && ( + setShowSearch(false)} + /> + )} + + {items.length === 0 ? ( +
+ No items in this list yet. Click "Add Item" to get started. +
+ ) : ( +
+ {items.map((item, index) => ( +
+
+ + + {index + 1} + +
+
+
+
+

{item.entity_type} - {item.entity_id}

+
+ +
+
+ +