Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions MyApp.Client/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use client';

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useProjectStore } from '@/lib/stores/project-store';
import { ProjectCard } from '@/components/dashboard/project-card';
import { CreateProjectDialog } from '@/components/dashboard/create-project-dialog';
import { RenameProjectDialog } from '@/components/dashboard/rename-project-dialog';
import { Button } from '@/components/ui/button';
import { Plus, Film } from 'lucide-react';

export default function DashboardPage() {
const router = useRouter();
const { projects, loadProjects, createProject, updateProject, deleteProject, setActiveProject } =
useProjectStore();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [renameProjectId, setRenameProjectId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const loadData = async () => {
try {
await loadProjects();
} finally {
setIsLoading(false);
}
};
loadData();
}, [loadProjects]);

const handleCreateProject = async (name: string) => {
const project = await createProject(name);
setActiveProject(project.id);
router.push(`/editor/${project.id}`);
};

const handleDeleteProject = async (id: string) => {
if (confirm('Are you sure you want to delete this project? This cannot be undone.')) {
await deleteProject(id);
}
};

const handleRenameProject = async (name: string) => {
if (renameProjectId) {
await updateProject(renameProjectId, { name });
setRenameProjectId(null);
}
};

const renameProject = projects.find((p) => p.id === renameProjectId);

if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<Film className="w-16 h-16 mx-auto mb-4 animate-pulse" />
<p className="text-lg text-muted-foreground">Loading projects...</p>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-4xl font-bold mb-2">My Projects</h1>
<p className="text-muted-foreground">
Create and manage your video projects
</p>
</div>
<Button size="lg" onClick={() => setIsCreateDialogOpen(true)}>
<Plus className="w-5 h-5 mr-2" />
New Project
</Button>
</div>

{projects.length === 0 ? (
<div className="text-center py-16">
<Film className="w-24 h-24 mx-auto mb-6 text-muted-foreground" />
<h2 className="text-2xl font-semibold mb-2">No projects yet</h2>
<p className="text-muted-foreground mb-6">
Create your first video project to get started
</p>
<Button size="lg" onClick={() => setIsCreateDialogOpen(true)}>
<Plus className="w-5 h-5 mr-2" />
Create Your First Project
</Button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{projects.map((project) => (
<ProjectCard
key={project.id}
project={project}
onDelete={handleDeleteProject}
onRename={(id) => setRenameProjectId(id)}
/>
))}
</div>
)}
</div>

<CreateProjectDialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
onSubmit={handleCreateProject}
/>

{renameProject && (
<RenameProjectDialog
open={!!renameProjectId}
currentName={renameProject.name}
onOpenChange={(open) => !open && setRenameProjectId(null)}
onSubmit={handleRenameProject}
/>
)}
</div>
);
}
39 changes: 39 additions & 0 deletions MyApp.Client/app/editor/[projectId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';

import { useEffect } from 'react';
import { useParams } from 'next/navigation';
import { useProjectStore } from '@/lib/stores/project-store';
import { useAssetStore } from '@/lib/stores/asset-store';
import { EditorLayout } from '@/components/editor/editor-layout';
import { Film } from 'lucide-react';

export default function EditorPage() {
const params = useParams();
const projectId = params.projectId as string;
const { projects, loadProjects, setActiveProject, getActiveProject } = useProjectStore();
const { loadAssets } = useAssetStore();

useEffect(() => {
const loadData = async () => {
await loadProjects();
setActiveProject(projectId);
await loadAssets(projectId);
};
loadData();
}, [projectId, loadProjects, setActiveProject, loadAssets]);

const project = getActiveProject();

if (!project) {
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="text-center text-white">
<Film className="w-16 h-16 mx-auto mb-4 animate-pulse" />
<p className="text-lg">Loading editor...</p>
</div>
</div>
);
}

return <EditorLayout project={project} />;
}
70 changes: 2 additions & 68 deletions MyApp.Client/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,5 @@
import Container from "@/components/container"
import MoreStories from "@/components/more-stories"
import HeroPost from "@/components/hero-post"
import Intro from "@/components/intro"
import Layout from "@/components/layout"
import { getAllPosts } from "@/lib/api"
import { CMS_NAME } from "@/lib/constants"
import Post from "@/types/post"
import GettingStarted from "@/components/getting-started"
import BuiltInUis from "@/components/builtin-uis"
import type { Metadata } from 'next'

export const metadata: Metadata = {
title: `Next.js Example with ${CMS_NAME}`,
}
import { redirect } from 'next/navigation'

export default function Index() {
const allPosts = getAllPosts([
'title',
'date',
'slug',
'author',
'coverImage',
'excerpt',
]) as unknown as Post[]

const heroPost = allPosts[0]
const morePosts = allPosts.slice(1)

return (
<Layout>
<Container>
<Intro />
<div className="mb-32 flex justify-center">
<GettingStarted template="nextjs" />
</div>


<div className="flex justify-center my-20 py-20 bg-slate-100 dark:bg-slate-800">
<div className="text-center">
<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>
<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">
Built-in UIs
</h1>
</div>
</div>

<div className="mb-40">
<p className="mt-4 mb-10 text-xl text-gray-600 dark:text-gray-400">
Manage your ServiceStack App and explore, discover, query and call APIs instantly with
built-in Auto UIs dynamically generated from the rich metadata of your App's typed C# APIs &amp; DTOs
</p>

<BuiltInUis />
</div>

{heroPost && (
<HeroPost
title={heroPost.title}
coverImage={heroPost.coverImage}
date={heroPost.date}
author={heroPost.author}
slug={heroPost.slug}
excerpt={heroPost.excerpt}
/>
)}
{morePosts.length > 0 && <MoreStories posts={morePosts} />}
</Container>
</Layout>
)
redirect('/dashboard')
}
72 changes: 72 additions & 0 deletions MyApp.Client/components/dashboard/create-project-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';

import { useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';

interface CreateProjectDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (name: string) => void;
}

export function CreateProjectDialog({
open,
onOpenChange,
onSubmit,
}: CreateProjectDialogProps) {
const [name, setName] = useState('');

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (name.trim()) {
onSubmit(name.trim());
setName('');
onOpenChange(false);
}
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Create New Project</DialogTitle>
<DialogDescription>
Give your video project a name to get started.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Project Name</Label>
<Input
id="name"
placeholder="My Awesome Video"
value={name}
onChange={(e) => setName(e.target.value)}
autoFocus
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={!name.trim()}>
Create Project
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
72 changes: 72 additions & 0 deletions MyApp.Client/components/dashboard/project-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';

import { Project } from '@/lib/types/project';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Trash2, Edit, Play } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import Link from 'next/link';

interface ProjectCardProps {
project: Project;
onDelete: (id: string) => void;
onRename: (id: string) => void;
}

export function ProjectCard({ project, onDelete, onRename }: ProjectCardProps) {
const formattedDate = formatDistanceToNow(new Date(project.updatedAt), {
addSuffix: true,
});

const duration = Math.floor(project.settings.durationInFrames / project.settings.fps);
const minutes = Math.floor(duration / 60);
const seconds = duration % 60;

return (
<Card className="group overflow-hidden hover:shadow-lg transition-shadow">
<div className="relative aspect-video bg-black">
{project.thumbnail ? (
<img
src={project.thumbnail}
alt={project.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600">
<Play className="w-16 h-16 text-white opacity-50" />
</div>
)}
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<Link href={`/editor/${project.id}`}>
<Button size="sm" variant="secondary">
<Play className="w-4 h-4 mr-1" />
Open
</Button>
</Link>
<Button size="sm" variant="secondary" onClick={() => onRename(project.id)}>
<Edit className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => onDelete(project.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
<CardContent className="p-4">
<h3 className="font-semibold text-lg mb-1 truncate">{project.name}</h3>
<div className="text-sm text-muted-foreground space-y-1">
<p>
{project.settings.width}x{project.settings.height} • {project.settings.fps}fps
</p>
<p>
{minutes}:{seconds.toString().padStart(2, '0')} • {project.metadata.assetCount} assets
</p>
<p>Updated {formattedDate}</p>
</div>
</CardContent>
</Card>
);
}
Loading
Loading