Compare commits

..

12 Commits

9 changed files with 743 additions and 1019 deletions

View File

@@ -1,6 +1,5 @@
import type { NextConfig } from "next"; /** @type {import('next').NextConfig} */
const nextConfig = {
const nextConfig: NextConfig = {
webpack: (config) => { webpack: (config) => {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
config.module.rules.push({ config.module.rules.push({

View File

@@ -40,18 +40,20 @@
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@supabase/auth-ui-react": "^0.4.7", "@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8", "@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.93.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"geist": "^1.5.1",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.511.0", "lucide-react": "^0.511.0",
"next": "15.3.8", "next": "14.2.3",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.2.1", "react": "^18.2.0",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^19.2.1", "react-dom": "^18.2.0",
"react-hook-form": "^7.56.4", "react-hook-form": "^7.56.4",
"react-resizable-panels": "^3.0.2", "react-resizable-panels": "^3.0.2",
"recharts": "^2.15.3", "recharts": "^2.15.3",
@@ -64,8 +66,8 @@
"devDependencies": { "devDependencies": {
"@dyad-sh/nextjs-webpack-component-tagger": "^0.8.0", "@dyad-sh/nextjs-webpack-component-tagger": "^0.8.0",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^18.2.0",
"@types/react-dom": "^19", "@types/react-dom": "^18.2.0",
"postcss": "^8", "postcss": "^8",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5" "typescript": "^5"

1639
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,8 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { GeistSans } from "geist/font/sans";
import "./globals.css"; import "./globals.css";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL("https://clips.linxweiler.xyz"), metadataBase: new URL("https://clips.linxweiler.xyz"),
title: "Video Clip Cutter", title: "Video Clip Cutter",
@@ -25,9 +15,9 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en" className="dark"> <html lang="en" className={`${GeistSans.variable} dark`}>
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className="antialiased"
> >
<Header /> <Header />
{children} {children}

View File

@@ -7,12 +7,13 @@ import { useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { KeyRound } from "lucide-react"; import { KeyRound } from "lucide-react";
import type { AuthChangeEvent, Session, Provider } from "@supabase/supabase-js";
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { const { data: { subscription } } = supabase.auth.onAuthStateChange((_event: AuthChangeEvent, session: Session | null) => {
if (session) { if (session) {
router.push('/'); router.push('/');
} }
@@ -23,7 +24,7 @@ export default function LoginPage() {
const handleOidcSignIn = async () => { const handleOidcSignIn = async () => {
await supabase.auth.signInWithOAuth({ await supabase.auth.signInWithOAuth({
provider: 'pocket_id', provider: 'pocket_id' as Provider,
}); });
}; };
@@ -40,7 +41,7 @@ export default function LoginPage() {
providers={[]} providers={[]}
theme="dark" theme="dark"
view="sign_in" view="sign_in"
showLinks={false} showLinks={true}
/> />
<div className="relative"> <div className="relative">
<div className="absolute inset-0 flex items-center"> <div className="absolute inset-0 flex items-center">

View File

@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { VideoEditor } from "@/components/video-editor"; import { VideoEditor } from "@/components/video-editor";
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import type { AuthChangeEvent, Session } from '@supabase/supabase-js';
export default function Home() { export default function Home() {
const router = useRouter(); const router = useRouter();
@@ -22,7 +23,7 @@ export default function Home() {
checkSession(); checkSession();
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, _session: Session | null) => {
if (event === 'SIGNED_OUT') { if (event === 'SIGNED_OUT') {
router.push('/login'); router.push('/login');
} }
@@ -40,7 +41,7 @@ export default function Home() {
} }
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen bg-background p-4 sm:p-6 md:p-8 font-[family-name:var(--font-geist-sans)]"> <div className="flex flex-col items-center justify-center min-h-screen bg-background p-4 sm:p-6 md:p-8">
<main className="w-full max-w-4xl"> <main className="w-full max-w-4xl">
<VideoEditor /> <VideoEditor />
</main> </main>

View File

@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { User as SupabaseUser } from '@supabase/supabase-js'; import { User as SupabaseUser, AuthChangeEvent, Session } from '@supabase/supabase-js';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { LogOut, User } from 'lucide-react'; import { LogOut, User } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -21,7 +21,7 @@ export function Header() {
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { const { data: { subscription } } = supabase.auth.onAuthStateChange((_event: AuthChangeEvent, session: Session | null) => {
setUser(session?.user ?? null); setUser(session?.user ?? null);
}); });

View File

@@ -6,8 +6,8 @@ import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/componen
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter, DialogClose } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from '@/components/ui/dialog';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Toaster } from '@/components/ui/sonner'; import { Toaster } from '@/components/ui/sonner';
@@ -25,6 +25,7 @@ type UserClipsProps = {
export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProps) { export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProps) {
const [clipToEdit, setClipToEdit] = useState<Clip | null>(null); const [clipToEdit, setClipToEdit] = useState<Clip | null>(null);
const [clipToDelete, setClipToDelete] = useState<Clip | null>(null);
const [newTitle, setNewTitle] = useState(''); const [newTitle, setNewTitle] = useState('');
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
@@ -74,6 +75,7 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
onClipDeleted(clip.id); onClipDeleted(clip.id);
toast.success('Clip deleted successfully!'); toast.success('Clip deleted successfully!');
setClipToDelete(null);
} catch (error) { } catch (error) {
toast.error('Failed to delete clip.'); toast.error('Failed to delete clip.');
console.error(error); console.error(error);
@@ -112,18 +114,17 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DialogTrigger asChild onClick={() => handleEditClick(clip)}> <DropdownMenuItem onSelect={() => handleEditClick(clip)}>
<DropdownMenuItem>
<Edit className="mr-2 h-4 w-4" /> <Edit className="mr-2 h-4 w-4" />
<span>Edit Title</span> <span>Edit Title</span>
</DropdownMenuItem> </DropdownMenuItem>
</DialogTrigger> <DropdownMenuItem
<AlertDialogTrigger asChild> className="text-destructive focus:text-destructive"
<DropdownMenuItem className="text-destructive focus:text-destructive"> onSelect={() => setClipToDelete(clip)}
>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span> <span>Delete</span>
</DropdownMenuItem> </DropdownMenuItem>
</AlertDialogTrigger>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</CardTitle> </CardTitle>
@@ -154,25 +155,6 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
{hasCopied === clip.short_id ? 'Copied!' : 'Copy Link'} {hasCopied === clip.short_id ? 'Copied!' : 'Copy Link'}
</Button> </Button>
</CardFooter> </CardFooter>
{/* Delete Confirmation Dialog */}
<AlertDialog>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your clip and its thumbnail from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => handleDeleteClip(clip)} disabled={isDeleting} className="bg-destructive hover:bg-destructive/90">
{isDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card> </Card>
))} ))}
</div> </div>
@@ -209,6 +191,25 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Delete Confirmation Dialog */}
<AlertDialog open={!!clipToDelete} onOpenChange={(isOpen) => !isOpen && setClipToDelete(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your clip and its thumbnail from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => clipToDelete && handleDeleteClip(clipToDelete)} disabled={isDeleting} className="bg-destructive hover:bg-destructive/90">
{isDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</> </>
); );
} }

View File

@@ -9,6 +9,9 @@ export default {
], ],
theme: { theme: {
extend: { extend: {
fontFamily: {
sans: ["var(--font-geist-sans)"],
},
colors: { colors: {
background: 'hsl(var(--background))', background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))', foreground: 'hsl(var(--foreground))',