Files
thrilltrack-explorer/src/components/lists/UserListManager.tsx
2025-11-03 20:04:11 +00:00

331 lines
11 KiB
TypeScript

import { useState, useEffect } from "react";
import { useAuth } from "@/hooks/useAuth";
import { supabase } from "@/integrations/supabase/client";
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>
);
}