Compare commits

...

12 Commits

9 changed files with 743 additions and 1019 deletions

View File

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

View File

@@ -40,18 +40,20 @@
"@radix-ui/react-tooltip": "^1.2.7",
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.93.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.6.0",
"geist": "^1.5.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.511.0",
"next": "15.3.8",
"next": "14.2.3",
"next-themes": "^0.4.6",
"react": "^19.2.1",
"react": "^18.2.0",
"react-day-picker": "^8.10.1",
"react-dom": "^19.2.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.56.4",
"react-resizable-panels": "^3.0.2",
"recharts": "^2.15.3",
@@ -64,8 +66,8 @@
"devDependencies": {
"@dyad-sh/nextjs-webpack-component-tagger": "^0.8.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"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 { Geist, Geist_Mono } from "next/font/google";
import { GeistSans } from "geist/font/sans";
import "./globals.css";
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 = {
metadataBase: new URL("https://clips.linxweiler.xyz"),
title: "Video Clip Cutter",
@@ -25,9 +15,9 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en" className="dark">
<html lang="en" className={`${GeistSans.variable} dark`}>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className="antialiased"
>
<Header />
{children}

View File

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

View File

@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react';
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 { LogOut, User } from 'lucide-react';
import { useRouter } from 'next/navigation';
@@ -21,7 +21,7 @@ export function Header() {
const router = useRouter();
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event: AuthChangeEvent, session: Session | 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 { 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from '@/components/ui/dialog';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { Toaster } from '@/components/ui/sonner';
@@ -25,6 +25,7 @@ type UserClipsProps = {
export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProps) {
const [clipToEdit, setClipToEdit] = useState<Clip | null>(null);
const [clipToDelete, setClipToDelete] = useState<Clip | null>(null);
const [newTitle, setNewTitle] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
@@ -74,6 +75,7 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
onClipDeleted(clip.id);
toast.success('Clip deleted successfully!');
setClipToDelete(null);
} catch (error) {
toast.error('Failed to delete clip.');
console.error(error);
@@ -112,18 +114,17 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DialogTrigger asChild onClick={() => handleEditClick(clip)}>
<DropdownMenuItem>
<DropdownMenuItem onSelect={() => handleEditClick(clip)}>
<Edit className="mr-2 h-4 w-4" />
<span>Edit Title</span>
</DropdownMenuItem>
</DialogTrigger>
<AlertDialogTrigger asChild>
<DropdownMenuItem className="text-destructive focus:text-destructive">
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onSelect={() => setClipToDelete(clip)}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
</AlertDialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
</CardTitle>
@@ -154,25 +155,6 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
{hasCopied === clip.short_id ? 'Copied!' : 'Copy Link'}
</Button>
</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>
))}
</div>
@@ -209,6 +191,25 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
</DialogFooter>
</DialogContent>
</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: {
extend: {
fontFamily: {
sans: ["var(--font-geist-sans)"],
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',