mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 17:31:15 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
158
src-old/components/lists/ListDisplay.tsx
Normal file
158
src-old/components/lists/ListDisplay.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { UserTopList, UserTopListItem, Park, Ride, Company } from "@/types/database";
|
||||
import { supabase } from "@/lib/supabaseClient";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { handleError } from "@/lib/errorHandler";
|
||||
|
||||
interface ListDisplayProps {
|
||||
list: UserTopList;
|
||||
}
|
||||
|
||||
interface EnrichedListItem extends UserTopListItem {
|
||||
entity?: Park | Ride | Company;
|
||||
}
|
||||
|
||||
export function ListDisplay({ list }: ListDisplayProps) {
|
||||
const [items, setItems] = useState<EnrichedListItem[]>([]);
|
||||
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) {
|
||||
handleError(itemsError, {
|
||||
action: 'Fetch List Items',
|
||||
metadata: { listId: list.id }
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then, fetch the entities for each item
|
||||
const enrichedItems = await Promise.all(
|
||||
(itemsData as UserTopListItem[]).map(async (item) => {
|
||||
let entity: Park | Ride | Company | null = 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 as Park | null;
|
||||
} 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 as Ride | null;
|
||||
} 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 as Company | null;
|
||||
}
|
||||
|
||||
return { ...item, entity };
|
||||
})
|
||||
);
|
||||
|
||||
setItems(enrichedItems as EnrichedListItem[]);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const getEntityUrl = (item: EnrichedListItem) => {
|
||||
if (!item.entity) return "#";
|
||||
|
||||
const entity = item.entity as { slug?: string };
|
||||
|
||||
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 <div className="text-center py-4 text-muted-foreground">Loading...</div>;
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
This list is empty. Click "Edit" to add items.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ol className="space-y-2">
|
||||
{items.map((item, index) => (
|
||||
<li key={item.id} className="flex items-start gap-3 p-3 rounded-lg hover:bg-muted/50 transition-colors">
|
||||
<span className="font-bold text-lg text-muted-foreground min-w-[2rem]">
|
||||
{index + 1}.
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
{item.entity ? (
|
||||
<Link
|
||||
to={getEntityUrl(item)}
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{(item.entity as { name?: string }).name || 'Unknown'}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="font-medium text-muted-foreground">
|
||||
[Deleted or unavailable]
|
||||
</span>
|
||||
)}
|
||||
<div className="flex gap-2 mt-1">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{item.entity_type}
|
||||
</Badge>
|
||||
{item.entity && item.entity_type === "park" && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{(item.entity as Park).park_type}
|
||||
</Badge>
|
||||
)}
|
||||
{item.entity && item.entity_type === "ride" && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{(item.entity as Ride).category}
|
||||
</Badge>
|
||||
)}
|
||||
{item.entity && item.entity_type === "company" && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{(item.entity as Company).company_type}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{item.notes && (
|
||||
<p className="text-sm text-muted-foreground mt-2 italic">
|
||||
"{item.notes}"
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
230
src-old/components/lists/ListItemEditor.tsx
Normal file
230
src-old/components/lists/ListItemEditor.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { UserTopList, UserTopListItem } from "@/types/database";
|
||||
import { supabase } from "@/lib/supabaseClient";
|
||||
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";
|
||||
import { getErrorMessage } from "@/lib/errorHandler";
|
||||
|
||||
interface ListItemEditorProps {
|
||||
list: UserTopList;
|
||||
onUpdate: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ListItemEditor({ list, onUpdate, onClose }: ListItemEditorProps) {
|
||||
const [items, setItems] = useState<UserTopListItem[]>([]);
|
||||
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) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
toast.error("Failed to load list items", {
|
||||
description: errorMessage
|
||||
});
|
||||
} 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 {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
toast.error("Failed to add item", {
|
||||
description: errorMessage
|
||||
});
|
||||
}
|
||||
} 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) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
toast.error("Failed to remove item", {
|
||||
description: errorMessage
|
||||
});
|
||||
} 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) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
toast.error("Failed to update notes", {
|
||||
description: errorMessage
|
||||
});
|
||||
} 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 <div className="text-center py-4">Loading items...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="font-semibold">Edit List Items</h3>
|
||||
<div className="space-x-2">
|
||||
<Button size="sm" onClick={() => setShowSearch(!showSearch)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Item
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={onClose}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showSearch && (
|
||||
<ListSearch
|
||||
listType={list.list_type}
|
||||
onSelect={handleAddItem}
|
||||
onClose={() => setShowSearch(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No items in this list yet. Click "Add Item" to get started.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-start gap-2 p-3 border rounded-lg bg-card"
|
||||
>
|
||||
<div className="flex flex-col gap-1 mt-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => handleMoveItem(item.id, "up")}
|
||||
disabled={index === 0}
|
||||
>
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground text-center">
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="font-medium">{item.entity_type} - {item.entity_id}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => handleRemoveItem(item.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor={`notes-${item.id}`} className="text-xs">
|
||||
Notes (optional)
|
||||
</Label>
|
||||
<Textarea
|
||||
id={`notes-${item.id}`}
|
||||
value={item.notes || ""}
|
||||
onChange={(e) => handleUpdateNotes(item.id, e.target.value)}
|
||||
placeholder="Add personal notes about this item..."
|
||||
className="h-16 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
184
src-old/components/lists/ListSearch.tsx
Normal file
184
src-old/components/lists/ListSearch.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { supabase } from "@/lib/supabaseClient";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search, Plus, X } from "lucide-react";
|
||||
import { useDebounce } from "@/hooks/useDebounce";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface ListSearchProps {
|
||||
listType: string;
|
||||
onSelect: (entityType: string, entityId: string, entityName: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "park" | "ride" | "company";
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export function ListSearch({ listType, onSelect, onClose }: ListSearchProps) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [results, setResults] = useState<SearchResult[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const debouncedQuery = useDebounce(query, 300);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedQuery.length >= 2) {
|
||||
searchEntities();
|
||||
} else {
|
||||
setResults([]);
|
||||
}
|
||||
}, [debouncedQuery, listType]);
|
||||
|
||||
const searchEntities = async () => {
|
||||
setLoading(true);
|
||||
const searchResults: SearchResult[] = [];
|
||||
|
||||
// Determine which entity types to search based on list type
|
||||
const shouldSearchParks = listType === "parks" || listType === "mixed";
|
||||
const shouldSearchRides = listType === "rides" || listType === "coasters" || listType === "mixed";
|
||||
const shouldSearchCompanies = listType === "companies" || listType === "mixed";
|
||||
|
||||
// Search parks
|
||||
if (shouldSearchParks) {
|
||||
const { data: parks } = await supabase
|
||||
.from("parks")
|
||||
.select("id, name, park_type, location_id")
|
||||
.ilike("name", `%${debouncedQuery}%`)
|
||||
.limit(10);
|
||||
|
||||
if (parks) {
|
||||
searchResults.push(
|
||||
...parks.map((park) => ({
|
||||
id: park.id,
|
||||
name: park.name,
|
||||
type: "park" as const,
|
||||
subtitle: park.park_type,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Search rides
|
||||
if (shouldSearchRides) {
|
||||
const { data: rides } = await supabase
|
||||
.from("rides")
|
||||
.select("id, name, category, park:parks(name)")
|
||||
.ilike("name", `%${debouncedQuery}%`)
|
||||
.limit(10);
|
||||
|
||||
if (rides) {
|
||||
interface RideSearchResult {
|
||||
id: string;
|
||||
name: string;
|
||||
park?: { name: string } | null;
|
||||
category?: string | null;
|
||||
}
|
||||
|
||||
searchResults.push(
|
||||
...rides.map((ride: RideSearchResult) => ({
|
||||
id: ride.id,
|
||||
name: ride.name,
|
||||
type: "ride" as const,
|
||||
subtitle: ride.park?.name || ride.category || 'Unknown',
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Search companies
|
||||
if (shouldSearchCompanies) {
|
||||
const { data: companies } = await supabase
|
||||
.from("companies")
|
||||
.select("id, name, company_type")
|
||||
.ilike("name", `%${debouncedQuery}%`)
|
||||
.limit(10);
|
||||
|
||||
if (companies) {
|
||||
searchResults.push(
|
||||
...companies.map((company) => ({
|
||||
id: company.id,
|
||||
name: company.name,
|
||||
type: "company" as const,
|
||||
subtitle: company.company_type,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setResults(searchResults);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg p-4 bg-card space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search for parks, rides, or companies..."
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="text-center py-4 text-muted-foreground">
|
||||
Searching...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && results.length === 0 && debouncedQuery.length >= 2 && (
|
||||
<div className="text-center py-4 text-muted-foreground">
|
||||
No results found. Try a different search term.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && results.length > 0 && (
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{results.map((result) => (
|
||||
<div
|
||||
key={`${result.type}-${result.id}`}
|
||||
className="flex items-center justify-between p-2 rounded hover:bg-muted transition-colors"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{result.name}</p>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{result.type}
|
||||
</Badge>
|
||||
{result.subtitle && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{result.subtitle}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onSelect(result.type, result.id, result.name)}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{debouncedQuery.length < 2 && (
|
||||
<div className="text-center py-4 text-muted-foreground text-sm">
|
||||
Type at least 2 characters to search
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
330
src-old/components/lists/UserListManager.tsx
Normal file
330
src-old/components/lists/UserListManager.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { supabase } from "@/lib/supabaseClient";
|
||||
import { UserTopList, UserTopListItem } from "@/types/database";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Plus, Trash2, Edit, Eye, EyeOff } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { ListItemEditor } from "./ListItemEditor";
|
||||
import { ListDisplay } from "./ListDisplay";
|
||||
import { handleError } from "@/lib/errorHandler";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
export function UserListManager() {
|
||||
const { user } = useAuth();
|
||||
const [lists, setLists] = useState<UserTopList[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingList, setEditingList] = useState<UserTopList | null>(null);
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
|
||||
const [newListTitle, setNewListTitle] = useState("");
|
||||
const [newListDescription, setNewListDescription] = useState("");
|
||||
const [newListType, setNewListType] = useState<string>("mixed");
|
||||
const [newListIsPublic, setNewListIsPublic] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetchLists();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const fetchLists = async () => {
|
||||
if (!user) return;
|
||||
|
||||
setLoading(true);
|
||||
const { data, error } = await supabase
|
||||
.from("user_top_lists")
|
||||
.select(`
|
||||
*,
|
||||
list_items (
|
||||
id,
|
||||
entity_type,
|
||||
entity_id,
|
||||
position,
|
||||
notes,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
`)
|
||||
.eq("user_id", user.id)
|
||||
.order("created_at", { ascending: false });
|
||||
|
||||
if (error) {
|
||||
handleError(error, {
|
||||
action: 'Load User Lists',
|
||||
userId: user.id
|
||||
});
|
||||
} else {
|
||||
// Map Supabase data to UserTopList interface
|
||||
const mappedLists: UserTopList[] = (data || []).map(list => ({
|
||||
id: list.id,
|
||||
user_id: list.user_id,
|
||||
title: list.title,
|
||||
description: list.description,
|
||||
list_type: list.list_type as 'parks' | 'rides' | 'coasters' | 'companies' | 'mixed',
|
||||
is_public: list.is_public,
|
||||
created_at: list.created_at,
|
||||
updated_at: list.updated_at,
|
||||
items: (list.list_items || []).map(item => ({
|
||||
id: item.id,
|
||||
list_id: list.id, // Add the parent list ID
|
||||
entity_type: item.entity_type as 'park' | 'ride' | 'company',
|
||||
entity_id: item.entity_id,
|
||||
position: item.position,
|
||||
notes: item.notes,
|
||||
created_at: item.created_at || new Date().toISOString(),
|
||||
updated_at: item.updated_at || new Date().toISOString(),
|
||||
})),
|
||||
}));
|
||||
setLists(mappedLists);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleCreateList = async () => {
|
||||
if (!user || !newListTitle.trim()) {
|
||||
toast.error("Please enter a list title");
|
||||
return;
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("user_top_lists")
|
||||
.insert([{
|
||||
user_id: user.id,
|
||||
title: newListTitle,
|
||||
description: newListDescription || null,
|
||||
list_type: newListType,
|
||||
is_public: newListIsPublic,
|
||||
}])
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
handleError(error, {
|
||||
action: 'Create List',
|
||||
userId: user.id,
|
||||
metadata: { title: newListTitle }
|
||||
});
|
||||
} else {
|
||||
toast.success("List created successfully");
|
||||
const newList: UserTopList = {
|
||||
id: data.id,
|
||||
user_id: data.user_id,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
list_type: data.list_type as 'parks' | 'rides' | 'coasters' | 'companies' | 'mixed',
|
||||
is_public: data.is_public,
|
||||
created_at: data.created_at,
|
||||
updated_at: data.updated_at,
|
||||
items: [],
|
||||
};
|
||||
setLists([newList, ...lists]);
|
||||
setIsCreateDialogOpen(false);
|
||||
setNewListTitle("");
|
||||
setNewListDescription("");
|
||||
setNewListType("mixed");
|
||||
setNewListIsPublic(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteList = async (listId: string) => {
|
||||
const { error } = await supabase
|
||||
.from("user_top_lists")
|
||||
.delete()
|
||||
.eq("id", listId);
|
||||
|
||||
if (error) {
|
||||
handleError(error, {
|
||||
action: 'Delete List',
|
||||
userId: user?.id,
|
||||
metadata: { listId }
|
||||
});
|
||||
} else {
|
||||
toast.success("List deleted");
|
||||
setLists(lists.filter(l => l.id !== listId));
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleVisibility = async (list: UserTopList) => {
|
||||
const { error } = await supabase
|
||||
.from("user_top_lists")
|
||||
.update({ is_public: !list.is_public })
|
||||
.eq("id", list.id);
|
||||
|
||||
if (error) {
|
||||
handleError(error, {
|
||||
action: 'Toggle List Visibility',
|
||||
userId: user?.id,
|
||||
metadata: { listId: list.id }
|
||||
});
|
||||
} else {
|
||||
toast.success(`List is now ${!list.is_public ? "public" : "private"}`);
|
||||
setLists(lists.map(l =>
|
||||
l.id === list.id ? { ...l, is_public: !l.is_public } : l
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center py-8">Loading lists...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold">My Lists</h2>
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create List
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New List</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new list to organize your favorite parks, rides, or companies.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="title">Title</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={newListTitle}
|
||||
onChange={(e) => setNewListTitle(e.target.value)}
|
||||
placeholder="My Top 10 Coasters"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="description">Description (optional)</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={newListDescription}
|
||||
onChange={(e) => setNewListDescription(e.target.value)}
|
||||
placeholder="A list of my all-time favorite roller coasters"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="type">List Type</Label>
|
||||
<Select value={newListType} onValueChange={setNewListType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="parks">Parks</SelectItem>
|
||||
<SelectItem value="rides">Rides</SelectItem>
|
||||
<SelectItem value="coasters">Coasters</SelectItem>
|
||||
<SelectItem value="companies">Companies</SelectItem>
|
||||
<SelectItem value="mixed">Mixed</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="public"
|
||||
checked={newListIsPublic}
|
||||
onCheckedChange={setNewListIsPublic}
|
||||
/>
|
||||
<Label htmlFor="public">Make this list public</Label>
|
||||
</div>
|
||||
<Button onClick={handleCreateList} className="w-full">
|
||||
Create List
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{lists.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center">
|
||||
<p className="text-muted-foreground mb-4">You haven't created any lists yet.</p>
|
||||
<Button onClick={() => setIsCreateDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create Your First List
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{lists.map((list) => (
|
||||
<Card key={list.id}>
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<CardTitle>{list.title}</CardTitle>
|
||||
{list.description && (
|
||||
<CardDescription>{list.description}</CardDescription>
|
||||
)}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<span className="text-xs bg-secondary px-2 py-1 rounded">
|
||||
{list.list_type}
|
||||
</span>
|
||||
<span className="text-xs bg-secondary px-2 py-1 rounded">
|
||||
{list.items?.length || 0} items
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleToggleVisibility(list)}
|
||||
title={list.is_public ? "Make private" : "Make public"}
|
||||
>
|
||||
{list.is_public ? (
|
||||
<Eye className="h-4 w-4" />
|
||||
) : (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setEditingList(list)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteList(list.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{editingList?.id === list.id ? (
|
||||
<ListItemEditor
|
||||
list={list}
|
||||
onUpdate={fetchLists}
|
||||
onClose={() => setEditingList(null)}
|
||||
/>
|
||||
) : (
|
||||
<ListDisplay list={list} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user