diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx new file mode 100644 index 0000000..fc34864 --- /dev/null +++ b/src/app/account/page.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { supabase } from '@/integrations/supabase/client'; +import { Loader2, Video } from 'lucide-react'; +import { UserClips } from '@/components/user-clips'; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; + +export type Clip = { + id: string; + short_id: string; + title: string; + storage_path: string; + created_at: string; +}; + +export default function AccountPage() { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(true); + const [clips, setClips] = useState([]); + + useEffect(() => { + const fetchUserAndClips = async () => { + const { data: { user } } = await supabase.auth.getUser(); + if (!user) { + router.push('/login'); + return; + } + + const { data, error } = await supabase + .from('clips') + .select('id, short_id, title, storage_path, created_at') + .eq('user_id', user.id) + .order('created_at', { ascending: false }); + + if (error) { + console.error('Error fetching clips:', error); + } else { + setClips(data as Clip[]); + } + setIsLoading(false); + }; + + fetchUserAndClips(); + }, [router]); + + const onClipDeleted = (clipId: string) => { + setClips(prevClips => prevClips.filter(clip => clip.id !== clipId)); + }; + + const onClipUpdated = (updatedClip: Clip) => { + setClips(prevClips => prevClips.map(clip => clip.id === updatedClip.id ? updatedClip : clip)); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+

My Clips

+ {clips.length > 0 ? ( + + ) : ( +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/header.tsx b/src/components/header.tsx index f2f7c44..e7e4f68 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -2,13 +2,22 @@ import { useEffect, useState } from 'react'; import { supabase } from '@/integrations/supabase/client'; -import { User } from '@supabase/supabase-js'; +import { User as SupabaseUser } from '@supabase/supabase-js'; import { Button } from './ui/button'; -import { LogOut } from 'lucide-react'; +import { LogOut, User } from 'lucide-react'; import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" export function Header() { - const [user, setUser] = useState(null); + const [user, setUser] = useState(null); const router = useRouter(); useEffect(() => { @@ -34,12 +43,34 @@ export function Header() { return (
-
- {user.email} - -
+ + + + + + +
+

My Account

+

+ {user.email} +

+
+
+ + + + My Clips + + + + + + Log out + +
+
); } \ No newline at end of file diff --git a/src/components/user-clips.tsx b/src/components/user-clips.tsx new file mode 100644 index 0000000..dc866d9 --- /dev/null +++ b/src/components/user-clips.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { useState } from 'react'; +import { Clip } from '@/app/account/page'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter, DialogClose } from '@/components/ui/dialog'; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; +import { supabase } from '@/integrations/supabase/client'; +import { toast } from 'sonner'; +import { Toaster } from '@/components/ui/sonner'; +import { formatDistanceToNow } from 'date-fns'; +import { MoreVertical, Edit, Trash2, Loader2, Copy, Check } from 'lucide-react'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import Link from 'next/link'; + +type UserClipsProps = { + clips: Clip[]; + onClipDeleted: (clipId: string) => void; + onClipUpdated: (clip: Clip) => void; +}; + +export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProps) { + const [clipToEdit, setClipToEdit] = useState(null); + const [newTitle, setNewTitle] = useState(''); + const [isSaving, setIsSaving] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [hasCopied, setHasCopied] = useState(null); + + const handleEditClick = (clip: Clip) => { + setClipToEdit(clip); + setNewTitle(clip.title); + }; + + const handleSaveChanges = async () => { + if (!clipToEdit || !newTitle.trim()) return; + setIsSaving(true); + try { + const { data, error } = await supabase + .from('clips') + .update({ title: newTitle.trim() }) + .eq('id', clipToEdit.id) + .select() + .single(); + + if (error) throw error; + + onClipUpdated(data as Clip); + toast.success('Title updated successfully!'); + setClipToEdit(null); + } catch (error) { + toast.error('Failed to update title.'); + console.error(error); + } finally { + setIsSaving(false); + } + }; + + const handleDeleteClip = async (clip: Clip) => { + setIsDeleting(true); + try { + const { error: storageError } = await supabase.storage.from('clips').remove([clip.storage_path]); + if (storageError) throw storageError; + + const { error: dbError } = await supabase.from('clips').delete().eq('id', clip.id); + if (dbError) throw dbError; + + onClipDeleted(clip.id); + toast.success('Clip deleted successfully!'); + } catch (error) { + toast.error('Failed to delete clip.'); + console.error(error); + } finally { + setIsDeleting(false); + } + }; + + const copyToClipboard = (shortId: string) => { + const link = `${window.location.origin}/clips/${shortId}`; + navigator.clipboard.writeText(link); + setHasCopied(shortId); + setTimeout(() => setHasCopied(null), 2000); + }; + + return ( + <> + +
+ {clips.map((clip) => ( + + + + + {clip.title || 'Untitled Clip'} + + + + + + + handleEditClick(clip)}> + + + Edit Title + + + + + + Delete + + + + + + + + +
+ Click to view +
+ +
+ + {formatDistanceToNow(new Date(clip.created_at), { addSuffix: true })} + + + + {/* Delete Confirmation Dialog */} + + + + Are you sure? + + This action cannot be undone. This will permanently delete your clip and remove its data from our servers. + + + + Cancel + handleDeleteClip(clip)} disabled={isDeleting} className="bg-destructive hover:bg-destructive/90"> + {isDeleting && } + Delete + + + + +
+ ))} +
+ + {/* Edit Title Dialog */} + !isOpen && setClipToEdit(null)}> + + + Edit Clip Title + +
+
+ + setNewTitle(e.target.value)} + className="col-span-3" + /> +
+
+ + + + + + +
+
+ + ); +} \ No newline at end of file