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
This commit is contained in:
Yusuf İpek
2026-02-05 22:11:20 +03:00
commit 264392c2ec
64 changed files with 7917 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
import type { VideoProvider, VideoMetadata, EmbedOptions, ThumbnailSize } from './types';
// Vimeo URL patterns
const VIMEO_PATTERNS = [
/vimeo\.com\/(\d+)/,
/vimeo\.com\/video\/(\d+)/,
/player\.vimeo\.com\/video\/(\d+)/,
/vimeo\.com\/channels\/\w+\/(\d+)/,
/vimeo\.com\/groups\/\w+\/videos\/(\d+)/,
];
export const vimeoProvider: VideoProvider = {
id: 'vimeo',
name: 'Vimeo',
icon: 'Video', // Lucide doesn't have Vimeo icon
canHandle(url: string): boolean {
return VIMEO_PATTERNS.some(pattern => pattern.test(url));
},
extractVideoId(url: string): string | null {
for (const pattern of VIMEO_PATTERNS) {
const match = url.match(pattern);
if (match?.[1]) {
return match[1];
}
}
return null;
},
getEmbedUrl(videoId: string, options: EmbedOptions = {}): string {
const params = new URLSearchParams();
if (options.autoplay) params.set('autoplay', '1');
if (options.startTime) params.set('t', `${Math.floor(options.startTime)}s`);
if (options.loop) params.set('loop', '1');
if (options.muted) params.set('muted', '1');
// Better UX options
params.set('byline', '0');
params.set('portrait', '0');
params.set('title', '0');
const queryString = params.toString();
return `https://player.vimeo.com/video/${videoId}${queryString ? `?${queryString}` : ''}`;
},
getThumbnailUrl(videoId: string, size: ThumbnailSize = 'medium'): string {
// Vimeo requires API call to get thumbnail, return placeholder
// In production, cache these after fetching metadata
const sizeMap: Record<ThumbnailSize, number> = {
small: 200,
medium: 400,
large: 640,
maxres: 1280,
};
// This is a placeholder - actual thumbnail comes from metadata API
return `https://vumbnail.com/${videoId}_${sizeMap[size]}.jpg`;
},
async getMetadata(videoId: string): Promise<VideoMetadata> {
try {
const response = await fetch(
`https://vimeo.com/api/oembed.json?url=https://vimeo.com/${videoId}`
);
if (!response.ok) {
throw new Error('Failed to fetch video metadata');
}
const data = await response.json();
return {
title: data.title,
description: data.description,
thumbnailUrl: data.thumbnail_url,
duration: data.duration,
author: data.author_name,
authorUrl: data.author_url,
uploadDate: data.upload_date ? new Date(data.upload_date) : undefined,
};
} catch (error) {
return {
title: 'Vimeo Video',
thumbnailUrl: this.getThumbnailUrl(videoId, 'large'),
};
}
},
};