Skip to content

Commit 739fd6b

Browse files
committed
Implement Movie Creator Web App MVP
- Install required dependencies (Remotion, Zustand, Dexie.js, dnd-kit, etc.) - Create TypeScript types and data models (Project, Timeline, Asset, etc.) - Set up IndexedDB with Dexie.js for data persistence - Create Zustand stores (project, timeline, playback, asset stores) - Build Dashboard page with project CRUD functionality - Create shadcn/ui components (dialog, slider, input, etc.) - Implement asset upload and management system - Build Editor page layout (toolbar, canvas, timeline structure) - Create Remotion composition and player integration - Implement Timeline component with drag & drop functionality - Build playback controls and preview canvas - Update home page to redirect to dashboard Features implemented: - Project creation, editing, and deletion - Asset library with file uploads (video, audio, images) - Timeline editor with multiple tracks - Drag and drop assets from library to timeline - Remotion-powered video preview with playback controls - Timeline element manipulation (move, resize) - Real-time preview synchronization with timeline MVP is now functional with basic video editing capabilities.
1 parent 52d6950 commit 739fd6b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+6058
-253
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { useRouter } from 'next/navigation';
5+
import { useProjectStore } from '@/lib/stores/project-store';
6+
import { ProjectCard } from '@/components/dashboard/project-card';
7+
import { CreateProjectDialog } from '@/components/dashboard/create-project-dialog';
8+
import { RenameProjectDialog } from '@/components/dashboard/rename-project-dialog';
9+
import { Button } from '@/components/ui/button';
10+
import { Plus, Film } from 'lucide-react';
11+
12+
export default function DashboardPage() {
13+
const router = useRouter();
14+
const { projects, loadProjects, createProject, updateProject, deleteProject, setActiveProject } =
15+
useProjectStore();
16+
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
17+
const [renameProjectId, setRenameProjectId] = useState<string | null>(null);
18+
const [isLoading, setIsLoading] = useState(true);
19+
20+
useEffect(() => {
21+
const loadData = async () => {
22+
try {
23+
await loadProjects();
24+
} finally {
25+
setIsLoading(false);
26+
}
27+
};
28+
loadData();
29+
}, [loadProjects]);
30+
31+
const handleCreateProject = async (name: string) => {
32+
const project = await createProject(name);
33+
setActiveProject(project.id);
34+
router.push(`/editor/${project.id}`);
35+
};
36+
37+
const handleDeleteProject = async (id: string) => {
38+
if (confirm('Are you sure you want to delete this project? This cannot be undone.')) {
39+
await deleteProject(id);
40+
}
41+
};
42+
43+
const handleRenameProject = async (name: string) => {
44+
if (renameProjectId) {
45+
await updateProject(renameProjectId, { name });
46+
setRenameProjectId(null);
47+
}
48+
};
49+
50+
const renameProject = projects.find((p) => p.id === renameProjectId);
51+
52+
if (isLoading) {
53+
return (
54+
<div className="min-h-screen flex items-center justify-center">
55+
<div className="text-center">
56+
<Film className="w-16 h-16 mx-auto mb-4 animate-pulse" />
57+
<p className="text-lg text-muted-foreground">Loading projects...</p>
58+
</div>
59+
</div>
60+
);
61+
}
62+
63+
return (
64+
<div className="min-h-screen bg-background">
65+
<div className="container mx-auto px-4 py-8">
66+
<div className="flex items-center justify-between mb-8">
67+
<div>
68+
<h1 className="text-4xl font-bold mb-2">My Projects</h1>
69+
<p className="text-muted-foreground">
70+
Create and manage your video projects
71+
</p>
72+
</div>
73+
<Button size="lg" onClick={() => setIsCreateDialogOpen(true)}>
74+
<Plus className="w-5 h-5 mr-2" />
75+
New Project
76+
</Button>
77+
</div>
78+
79+
{projects.length === 0 ? (
80+
<div className="text-center py-16">
81+
<Film className="w-24 h-24 mx-auto mb-6 text-muted-foreground" />
82+
<h2 className="text-2xl font-semibold mb-2">No projects yet</h2>
83+
<p className="text-muted-foreground mb-6">
84+
Create your first video project to get started
85+
</p>
86+
<Button size="lg" onClick={() => setIsCreateDialogOpen(true)}>
87+
<Plus className="w-5 h-5 mr-2" />
88+
Create Your First Project
89+
</Button>
90+
</div>
91+
) : (
92+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
93+
{projects.map((project) => (
94+
<ProjectCard
95+
key={project.id}
96+
project={project}
97+
onDelete={handleDeleteProject}
98+
onRename={(id) => setRenameProjectId(id)}
99+
/>
100+
))}
101+
</div>
102+
)}
103+
</div>
104+
105+
<CreateProjectDialog
106+
open={isCreateDialogOpen}
107+
onOpenChange={setIsCreateDialogOpen}
108+
onSubmit={handleCreateProject}
109+
/>
110+
111+
{renameProject && (
112+
<RenameProjectDialog
113+
open={!!renameProjectId}
114+
currentName={renameProject.name}
115+
onOpenChange={(open) => !open && setRenameProjectId(null)}
116+
onSubmit={handleRenameProject}
117+
/>
118+
)}
119+
</div>
120+
);
121+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use client';
2+
3+
import { useEffect } from 'react';
4+
import { useParams } from 'next/navigation';
5+
import { useProjectStore } from '@/lib/stores/project-store';
6+
import { useAssetStore } from '@/lib/stores/asset-store';
7+
import { EditorLayout } from '@/components/editor/editor-layout';
8+
import { Film } from 'lucide-react';
9+
10+
export default function EditorPage() {
11+
const params = useParams();
12+
const projectId = params.projectId as string;
13+
const { projects, loadProjects, setActiveProject, getActiveProject } = useProjectStore();
14+
const { loadAssets } = useAssetStore();
15+
16+
useEffect(() => {
17+
const loadData = async () => {
18+
await loadProjects();
19+
setActiveProject(projectId);
20+
await loadAssets(projectId);
21+
};
22+
loadData();
23+
}, [projectId, loadProjects, setActiveProject, loadAssets]);
24+
25+
const project = getActiveProject();
26+
27+
if (!project) {
28+
return (
29+
<div className="min-h-screen flex items-center justify-center bg-black">
30+
<div className="text-center text-white">
31+
<Film className="w-16 h-16 mx-auto mb-4 animate-pulse" />
32+
<p className="text-lg">Loading editor...</p>
33+
</div>
34+
</div>
35+
);
36+
}
37+
38+
return <EditorLayout project={project} />;
39+
}

MyApp.Client/app/page.tsx

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,5 @@
1-
import Container from "@/components/container"
2-
import MoreStories from "@/components/more-stories"
3-
import HeroPost from "@/components/hero-post"
4-
import Intro from "@/components/intro"
5-
import Layout from "@/components/layout"
6-
import { getAllPosts } from "@/lib/api"
7-
import { CMS_NAME } from "@/lib/constants"
8-
import Post from "@/types/post"
9-
import GettingStarted from "@/components/getting-started"
10-
import BuiltInUis from "@/components/builtin-uis"
11-
import type { Metadata } from 'next'
12-
13-
export const metadata: Metadata = {
14-
title: `Next.js Example with ${CMS_NAME}`,
15-
}
1+
import { redirect } from 'next/navigation'
162

173
export default function Index() {
18-
const allPosts = getAllPosts([
19-
'title',
20-
'date',
21-
'slug',
22-
'author',
23-
'coverImage',
24-
'excerpt',
25-
]) as unknown as Post[]
26-
27-
const heroPost = allPosts[0]
28-
const morePosts = allPosts.slice(1)
29-
30-
return (
31-
<Layout>
32-
<Container>
33-
<Intro />
34-
<div className="mb-32 flex justify-center">
35-
<GettingStarted template="nextjs" />
36-
</div>
37-
38-
39-
<div className="flex justify-center my-20 py-20 bg-slate-100 dark:bg-slate-800">
40-
<div className="text-center">
41-
<svg className="text-link-dark w-36 h-36 inline-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="m8.58 17.25l.92-3.89l-3-2.58l3.95-.37L12 6.8l1.55 3.65l3.95.33l-3 2.58l.92 3.89L12 15.19zM12 2a10 10 0 0 1 10 10a10 10 0 0 1-10 10A10 10 0 0 1 2 12A10 10 0 0 1 12 2m0 2a8 8 0 0 0-8 8a8 8 0 0 0 8 8a8 8 0 0 0 8-8a8 8 0 0 0-8-8"/></svg>
42-
<h1 className="text-6xl md:text-7xl lg:text-8xl font-bold tracking-tighter leading-tight md:leading-none mb-12 text-center md:text-left">
43-
Built-in UIs
44-
</h1>
45-
</div>
46-
</div>
47-
48-
<div className="mb-40">
49-
<p className="mt-4 mb-10 text-xl text-gray-600 dark:text-gray-400">
50-
Manage your ServiceStack App and explore, discover, query and call APIs instantly with
51-
built-in Auto UIs dynamically generated from the rich metadata of your App's typed C# APIs &amp; DTOs
52-
</p>
53-
54-
<BuiltInUis />
55-
</div>
56-
57-
{heroPost && (
58-
<HeroPost
59-
title={heroPost.title}
60-
coverImage={heroPost.coverImage}
61-
date={heroPost.date}
62-
author={heroPost.author}
63-
slug={heroPost.slug}
64-
excerpt={heroPost.excerpt}
65-
/>
66-
)}
67-
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
68-
</Container>
69-
</Layout>
70-
)
4+
redirect('/dashboard')
715
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogDescription,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTitle,
11+
} from '@/components/ui/dialog';
12+
import { Input } from '@/components/ui/input';
13+
import { Label } from '@/components/ui/label';
14+
import { Button } from '@/components/ui/button';
15+
16+
interface CreateProjectDialogProps {
17+
open: boolean;
18+
onOpenChange: (open: boolean) => void;
19+
onSubmit: (name: string) => void;
20+
}
21+
22+
export function CreateProjectDialog({
23+
open,
24+
onOpenChange,
25+
onSubmit,
26+
}: CreateProjectDialogProps) {
27+
const [name, setName] = useState('');
28+
29+
const handleSubmit = (e: React.FormEvent) => {
30+
e.preventDefault();
31+
if (name.trim()) {
32+
onSubmit(name.trim());
33+
setName('');
34+
onOpenChange(false);
35+
}
36+
};
37+
38+
return (
39+
<Dialog open={open} onOpenChange={onOpenChange}>
40+
<DialogContent>
41+
<form onSubmit={handleSubmit}>
42+
<DialogHeader>
43+
<DialogTitle>Create New Project</DialogTitle>
44+
<DialogDescription>
45+
Give your video project a name to get started.
46+
</DialogDescription>
47+
</DialogHeader>
48+
<div className="grid gap-4 py-4">
49+
<div className="grid gap-2">
50+
<Label htmlFor="name">Project Name</Label>
51+
<Input
52+
id="name"
53+
placeholder="My Awesome Video"
54+
value={name}
55+
onChange={(e) => setName(e.target.value)}
56+
autoFocus
57+
/>
58+
</div>
59+
</div>
60+
<DialogFooter>
61+
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
62+
Cancel
63+
</Button>
64+
<Button type="submit" disabled={!name.trim()}>
65+
Create Project
66+
</Button>
67+
</DialogFooter>
68+
</form>
69+
</DialogContent>
70+
</Dialog>
71+
);
72+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use client';
2+
3+
import { Project } from '@/lib/types/project';
4+
import { Card, CardContent } from '@/components/ui/card';
5+
import { Button } from '@/components/ui/button';
6+
import { Trash2, Edit, Play } from 'lucide-react';
7+
import { formatDistanceToNow } from 'date-fns';
8+
import Link from 'next/link';
9+
10+
interface ProjectCardProps {
11+
project: Project;
12+
onDelete: (id: string) => void;
13+
onRename: (id: string) => void;
14+
}
15+
16+
export function ProjectCard({ project, onDelete, onRename }: ProjectCardProps) {
17+
const formattedDate = formatDistanceToNow(new Date(project.updatedAt), {
18+
addSuffix: true,
19+
});
20+
21+
const duration = Math.floor(project.settings.durationInFrames / project.settings.fps);
22+
const minutes = Math.floor(duration / 60);
23+
const seconds = duration % 60;
24+
25+
return (
26+
<Card className="group overflow-hidden hover:shadow-lg transition-shadow">
27+
<div className="relative aspect-video bg-black">
28+
{project.thumbnail ? (
29+
<img
30+
src={project.thumbnail}
31+
alt={project.name}
32+
className="w-full h-full object-cover"
33+
/>
34+
) : (
35+
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600">
36+
<Play className="w-16 h-16 text-white opacity-50" />
37+
</div>
38+
)}
39+
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
40+
<Link href={`/editor/${project.id}`}>
41+
<Button size="sm" variant="secondary">
42+
<Play className="w-4 h-4 mr-1" />
43+
Open
44+
</Button>
45+
</Link>
46+
<Button size="sm" variant="secondary" onClick={() => onRename(project.id)}>
47+
<Edit className="w-4 h-4" />
48+
</Button>
49+
<Button
50+
size="sm"
51+
variant="destructive"
52+
onClick={() => onDelete(project.id)}
53+
>
54+
<Trash2 className="w-4 h-4" />
55+
</Button>
56+
</div>
57+
</div>
58+
<CardContent className="p-4">
59+
<h3 className="font-semibold text-lg mb-1 truncate">{project.name}</h3>
60+
<div className="text-sm text-muted-foreground space-y-1">
61+
<p>
62+
{project.settings.width}x{project.settings.height}{project.settings.fps}fps
63+
</p>
64+
<p>
65+
{minutes}:{seconds.toString().padStart(2, '0')}{project.metadata.assetCount} assets
66+
</p>
67+
<p>Updated {formattedDate}</p>
68+
</div>
69+
</CardContent>
70+
</Card>
71+
);
72+
}

0 commit comments

Comments
 (0)