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