Files
OpenFrame/app/(dashboard)/projects/[projectId]/videos/new/page.tsx
T
Yusuf İpek 264392c2ec chore(init): scaffold OpenFrame — Next.js + Bun + shadcn
Initialize OpenFrame project with core scaffold and UI foundation.

- Project scaffold: Next.js 16.1 (App Router) + Bun runtime
- UI: shadcn/ui + TailwindCSS; landing, dashboard, project, and auth UIs
- Video support: provider abstraction (YouTube first), video player page with timestamped comments and custom timeline
- Auth & DB: NextAuth skeleton and Prisma schema (Postgres) included
- UX: dark-mode toggle, full-width layouts, comments sidebar, video route moved to /watch/[videoId]
- Dev: added YouTube iframe integration, custom controls, and TypeScript types
- Next steps: DB migrations, API routes (CRUD), real data wiring, voice-recording & sharing
2026-02-05 22:11:20 +03:00

192 lines
6.3 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useRouter, useParams } from 'next/navigation';
import Link from 'next/link';
import { ArrowLeft, Loader2, Link as LinkIcon, AlertCircle, CheckCircle2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { parseVideoUrl, getThumbnailUrl, type VideoSource } from '@/lib/video-providers';
export default function NewVideoPage() {
const router = useRouter();
const params = useParams();
const projectId = params.projectId as string;
const [isLoading, setIsLoading] = useState(false);
const [videoUrl, setVideoUrl] = useState('');
const [videoSource, setVideoSource] = useState<VideoSource | null>(null);
const [urlError, setUrlError] = useState('');
const [formData, setFormData] = useState({
title: '',
description: '',
});
const handleUrlChange = (url: string) => {
setVideoUrl(url);
setUrlError('');
if (!url.trim()) {
setVideoSource(null);
return;
}
const source = parseVideoUrl(url);
if (source) {
setVideoSource(source);
} else {
setVideoSource(null);
if (url.length > 10) {
setUrlError('Could not recognize this video URL. Currently supported: YouTube, Vimeo');
}
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!videoSource) {
setUrlError('Please enter a valid video URL');
return;
}
setIsLoading(true);
try {
// TODO: Implement actual video creation
// const response = await fetch(`/api/projects/${projectId}/videos`, {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({
// ...formData,
// ...videoSource,
// }),
// });
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 500));
router.push(`/projects/${projectId}`);
} catch (error) {
console.error('Failed to add video:', error);
} finally {
setIsLoading(false);
}
};
const thumbnailUrl = videoSource ? getThumbnailUrl(videoSource, 'large') : null;
return (
<div className="container max-w-2xl py-8">
<div className="mb-6">
<Link
href={`/projects/${projectId}`}
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="h-4 w-4 mr-1" />
Back to Project
</Link>
</div>
<Card>
<CardHeader>
<CardTitle>Add Video</CardTitle>
<CardDescription>
Paste a video link to add it to your project. Currently supports YouTube and Vimeo.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Video URL Input */}
<div className="space-y-2">
<Label htmlFor="url">Video URL</Label>
<div className="relative">
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
id="url"
placeholder="https://youtube.com/watch?v=..."
value={videoUrl}
onChange={(e) => handleUrlChange(e.target.value)}
className="pl-10"
required
disabled={isLoading}
/>
</div>
{/* URL validation feedback */}
{urlError && (
<p className="text-sm text-destructive flex items-center gap-1">
<AlertCircle className="h-4 w-4" />
{urlError}
</p>
)}
{videoSource && (
<p className="text-sm text-green-600 flex items-center gap-1">
<CheckCircle2 className="h-4 w-4" />
{videoSource.providerId.charAt(0).toUpperCase() + videoSource.providerId.slice(1)} video detected
</p>
)}
</div>
{/* Video Preview */}
{thumbnailUrl && videoSource && (
<div className="space-y-2">
<Label>Preview</Label>
<div className="relative aspect-video rounded-lg overflow-hidden bg-muted">
<img
src={thumbnailUrl}
alt="Video thumbnail"
className="object-cover w-full h-full"
/>
</div>
</div>
)}
{/* Title */}
<div className="space-y-2">
<Label htmlFor="title">Title</Label>
<Input
id="title"
placeholder="Video title (will auto-fill from video if empty)"
value={formData.title}
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
disabled={isLoading}
/>
<p className="text-xs text-muted-foreground">
Leave empty to use the original video title
</p>
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="description">Description (optional)</Label>
<Textarea
id="description"
placeholder="Add context about this video..."
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
rows={3}
disabled={isLoading}
/>
</div>
<div className="flex gap-3">
<Button type="submit" disabled={isLoading || !videoSource}>
{isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Add Video
</Button>
<Button type="button" variant="outline" onClick={() => router.back()}>
Cancel
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
);
}