mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 18:31:13 -05:00
331 lines
11 KiB
TypeScript
331 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|