From 8e737d87e0accbf1d433f4c8b1e8e0f125849799 Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Thu, 24 Jul 2025 17:04:27 -0500 Subject: Revert "merge main" This reverts commit 1ccacf6bb5db579cf6f343d9de57bce92d296f5f. --- apps/web/src/lib/blog-query.ts | 89 +++++------ apps/web/src/lib/fetch-github-stars.ts | 4 +- apps/web/src/lib/ffmpeg-utils.ts | 209 +++++++++++--------------- apps/web/src/lib/font-config.ts | 78 +++++----- apps/web/src/lib/rate-limit.ts | 32 ++-- apps/web/src/lib/storage/indexeddb-adapter.ts | 178 +++++++++++----------- apps/web/src/lib/storage/opfs-adapter.ts | 2 +- apps/web/src/lib/time.ts | 50 +++--- apps/web/src/lib/timeline.ts | 108 ++++++------- apps/web/src/lib/waitlist.ts | 26 ++-- 10 files changed, 364 insertions(+), 412 deletions(-) (limited to 'apps/web/src/lib') diff --git a/apps/web/src/lib/blog-query.ts b/apps/web/src/lib/blog-query.ts index 5cba0cf3..430f58bf 100644 --- a/apps/web/src/lib/blog-query.ts +++ b/apps/web/src/lib/blog-query.ts @@ -1,10 +1,4 @@ -import type { - MarbleAuthorList, - MarbleCategoryList, - MarblePost, - MarblePostList, - MarbleTagList, -} from "@/types/post"; +import type { MarbleAuthorList, MarbleCategoryList, MarblePost, MarblePostList, MarbleTagList } from '@/types/post'; import { unified } from "unified"; import rehypeParse from "rehype-parse"; import rehypeStringify from "rehype-stringify"; @@ -12,53 +6,50 @@ import rehypeSlug from "rehype-slug"; import rehypeAutolinkHeadings from "rehype-autolink-headings"; import rehypeSanitize from "rehype-sanitize"; -const url = - process.env.NEXT_PUBLIC_MARBLE_API_URL ?? "https://api.marblecms.com"; +const url = process.env.NEXT_PUBLIC_MARBLE_API_URL ?? "https://api.marblecms.com"; const key = process.env.MARBLE_WORKSPACE_KEY ?? "cmd4iw9mm0006l804kwqv0k46"; async function fetchFromMarble(endpoint: string): Promise { - try { - const response = await fetch(`${url}/${key}/${endpoint}`); - if (!response.ok) { - throw new Error( - `Failed to fetch ${endpoint}: ${response.status} ${response.statusText}` - ); + try { + const response = await fetch(`${url}/${key}/${endpoint}`); + if (!response.ok) { + throw new Error(`Failed to fetch ${endpoint}: ${response.status} ${response.statusText}`); + } + return await response.json() as T; + } catch (error) { + console.error(`Error fetching ${endpoint}:`, error); + throw error; } - return (await response.json()) as T; - } catch (error) { - console.error(`Error fetching ${endpoint}:`, error); - throw error; } -} - -export async function getPosts() { - return fetchFromMarble("posts"); -} - -export async function getTags() { - return fetchFromMarble("tags"); -} - -export async function getSinglePost(slug: string) { - return fetchFromMarble(`posts/${slug}`); -} - -export async function getCategories() { - return fetchFromMarble("categories"); -} - -export async function getAuthors() { - return fetchFromMarble("authors"); -} + + export async function getPosts() { + return fetchFromMarble('posts'); + } + + export async function getTags() { + return fetchFromMarble('tags'); + } + + export async function getSinglePost(slug: string) { + return fetchFromMarble(`posts/${slug}`); + } + + export async function getCategories() { + return fetchFromMarble('categories'); + } + + export async function getAuthors() { + return fetchFromMarble('authors'); + } export async function processHtmlContent(html: string): Promise { - const processor = unified() - .use(rehypeSanitize) - .use(rehypeParse, { fragment: true }) - .use(rehypeSlug) - .use(rehypeAutolinkHeadings, { behavior: "append" }) - .use(rehypeStringify); - - const file = await processor.process(html); - return String(file); + const processor = unified() + .use(rehypeSanitize) + .use(rehypeParse, { fragment: true }) + .use(rehypeSlug) + .use(rehypeAutolinkHeadings, { behavior: "append" }) + .use(rehypeStringify); + + const file = await processor.process(html); + return String(file); } diff --git a/apps/web/src/lib/fetch-github-stars.ts b/apps/web/src/lib/fetch-github-stars.ts index 0372f25b..c4444a62 100644 --- a/apps/web/src/lib/fetch-github-stars.ts +++ b/apps/web/src/lib/fetch-github-stars.ts @@ -19,8 +19,8 @@ export async function getStars(): Promise { if (count >= 1_000_000) return (count / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M"; - if (count >= 1000) - return (count / 1000).toFixed(1).replace(/\.0$/, "") + "k"; + if (count >= 1_000) + return (count / 1_000).toFixed(1).replace(/\.0$/, "") + "k"; return count.toString(); } catch (error) { console.error("Failed to fetch GitHub stars:", error); diff --git a/apps/web/src/lib/ffmpeg-utils.ts b/apps/web/src/lib/ffmpeg-utils.ts index c34200f6..ae0c267b 100644 --- a/apps/web/src/lib/ffmpeg-utils.ts +++ b/apps/web/src/lib/ffmpeg-utils.ts @@ -1,5 +1,5 @@ -import { FFmpeg } from "@ffmpeg/ffmpeg"; -import { toBlobURL } from "@ffmpeg/util"; +import { FFmpeg } from '@ffmpeg/ffmpeg'; +import { toBlobURL } from '@ffmpeg/util'; let ffmpeg: FFmpeg | null = null; @@ -7,13 +7,13 @@ export const initFFmpeg = async (): Promise => { if (ffmpeg) return ffmpeg; ffmpeg = new FFmpeg(); - + // Use locally hosted files instead of CDN - const baseURL = "/ffmpeg"; - + const baseURL = '/ffmpeg'; + await ffmpeg.load({ - coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, "text/javascript"), - wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, "application/wasm"), + coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), + wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), }); return ffmpeg; @@ -21,42 +21,34 @@ export const initFFmpeg = async (): Promise => { export const generateThumbnail = async ( videoFile: File, - timeInSeconds = 1 + timeInSeconds: number = 1 ): Promise => { const ffmpeg = await initFFmpeg(); - - const inputName = "input.mp4"; - const outputName = "thumbnail.jpg"; - + + const inputName = 'input.mp4'; + const outputName = 'thumbnail.jpg'; + // Write input file - await ffmpeg.writeFile( - inputName, - new Uint8Array(await videoFile.arrayBuffer()) - ); - + await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); + // Generate thumbnail at specific time await ffmpeg.exec([ - "-i", - inputName, - "-ss", - timeInSeconds.toString(), - "-vframes", - "1", - "-vf", - "scale=320:240", - "-q:v", - "2", - outputName, + '-i', inputName, + '-ss', timeInSeconds.toString(), + '-vframes', '1', + '-vf', 'scale=320:240', + '-q:v', '2', + outputName ]); - + // Read output file const data = await ffmpeg.readFile(outputName); - const blob = new Blob([data], { type: "image/jpeg" }); - + const blob = new Blob([data], { type: 'image/jpeg' }); + // Cleanup await ffmpeg.deleteFile(inputName); await ffmpeg.deleteFile(outputName); - + return URL.createObjectURL(blob); }; @@ -67,52 +59,43 @@ export const trimVideo = async ( onProgress?: (progress: number) => void ): Promise => { const ffmpeg = await initFFmpeg(); - - const inputName = "input.mp4"; - const outputName = "output.mp4"; - + + const inputName = 'input.mp4'; + const outputName = 'output.mp4'; + // Set up progress callback if (onProgress) { - ffmpeg.on("progress", ({ progress }) => { + ffmpeg.on('progress', ({ progress }) => { onProgress(progress * 100); }); } - + // Write input file - await ffmpeg.writeFile( - inputName, - new Uint8Array(await videoFile.arrayBuffer()) - ); - + await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); + const duration = endTime - startTime; - + // Trim video await ffmpeg.exec([ - "-i", - inputName, - "-ss", - startTime.toString(), - "-t", - duration.toString(), - "-c", - "copy", // Use stream copy for faster processing - outputName, + '-i', inputName, + '-ss', startTime.toString(), + '-t', duration.toString(), + '-c', 'copy', // Use stream copy for faster processing + outputName ]); - + // Read output file const data = await ffmpeg.readFile(outputName); - const blob = new Blob([data], { type: "video/mp4" }); - + const blob = new Blob([data], { type: 'video/mp4' }); + // Cleanup await ffmpeg.deleteFile(inputName); await ffmpeg.deleteFile(outputName); - + return blob; }; -export const getVideoInfo = async ( - videoFile: File -): Promise<{ +export const getVideoInfo = async (videoFile: File): Promise<{ duration: number; width: number; height: number; @@ -120,32 +103,27 @@ export const getVideoInfo = async ( }> => { const ffmpeg = await initFFmpeg(); - const inputName = "input.mp4"; + const inputName = 'input.mp4'; // Write input file - await ffmpeg.writeFile( - inputName, - new Uint8Array(await videoFile.arrayBuffer()) - ); + await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); // Capture FFmpeg stderr output with a one-time listener pattern - let ffmpegOutput = ""; + let ffmpegOutput = ''; let listening = true; const listener = (data: string) => { if (listening) ffmpegOutput += data; }; - ffmpeg.on("log", ({ message }) => listener(message)); + ffmpeg.on('log', ({ message }) => listener(message)); // Run ffmpeg to get info (stderr will contain the info) try { - await ffmpeg.exec(["-i", inputName, "-f", "null", "-"]); + await ffmpeg.exec(['-i', inputName, '-f', 'null', '-']); } catch (error) { listening = false; await ffmpeg.deleteFile(inputName); - console.error("FFmpeg execution failed:", error); - throw new Error( - "Failed to extract video info. The file may be corrupted or in an unsupported format." - ); + console.error('FFmpeg execution failed:', error); + throw new Error('Failed to extract video info. The file may be corrupted or in an unsupported format.'); } // Disable listener after exec completes @@ -165,12 +143,8 @@ export const getVideoInfo = async ( duration = parseInt(h) * 3600 + parseInt(m) * 60 + parseFloat(s); } - const videoStreamMatch = ffmpegOutput.match( - /Video:.* (\d+)x(\d+)[^,]*, ([\d.]+) fps/ - ); - let width = 0, - height = 0, - fps = 0; + const videoStreamMatch = ffmpegOutput.match(/Video:.* (\d+)x(\d+)[^,]*, ([\d.]+) fps/); + let width = 0, height = 0, fps = 0; if (videoStreamMatch) { width = parseInt(videoStreamMatch[1]); height = parseInt(videoStreamMatch[2]); @@ -181,7 +155,7 @@ export const getVideoInfo = async ( duration, width, height, - fps, + fps }; }; @@ -190,81 +164,68 @@ export const convertToWebM = async ( onProgress?: (progress: number) => void ): Promise => { const ffmpeg = await initFFmpeg(); - - const inputName = "input.mp4"; - const outputName = "output.webm"; - + + const inputName = 'input.mp4'; + const outputName = 'output.webm'; + // Set up progress callback if (onProgress) { - ffmpeg.on("progress", ({ progress }) => { + ffmpeg.on('progress', ({ progress }) => { onProgress(progress * 100); }); } - + // Write input file - await ffmpeg.writeFile( - inputName, - new Uint8Array(await videoFile.arrayBuffer()) - ); - + await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); + // Convert to WebM await ffmpeg.exec([ - "-i", - inputName, - "-c:v", - "libvpx-vp9", - "-crf", - "30", - "-b:v", - "0", - "-c:a", - "libopus", - outputName, + '-i', inputName, + '-c:v', 'libvpx-vp9', + '-crf', '30', + '-b:v', '0', + '-c:a', 'libopus', + outputName ]); - + // Read output file const data = await ffmpeg.readFile(outputName); - const blob = new Blob([data], { type: "video/webm" }); - + const blob = new Blob([data], { type: 'video/webm' }); + // Cleanup await ffmpeg.deleteFile(inputName); await ffmpeg.deleteFile(outputName); - + return blob; }; export const extractAudio = async ( videoFile: File, - format: "mp3" | "wav" = "mp3" + format: 'mp3' | 'wav' = 'mp3' ): Promise => { const ffmpeg = await initFFmpeg(); - - const inputName = "input.mp4"; + + const inputName = 'input.mp4'; const outputName = `output.${format}`; - + // Write input file - await ffmpeg.writeFile( - inputName, - new Uint8Array(await videoFile.arrayBuffer()) - ); - + await ffmpeg.writeFile(inputName, new Uint8Array(await videoFile.arrayBuffer())); + // Extract audio await ffmpeg.exec([ - "-i", - inputName, - "-vn", // Disable video - "-acodec", - format === "mp3" ? "libmp3lame" : "pcm_s16le", - outputName, + '-i', inputName, + '-vn', // Disable video + '-acodec', format === 'mp3' ? 'libmp3lame' : 'pcm_s16le', + outputName ]); - + // Read output file const data = await ffmpeg.readFile(outputName); const blob = new Blob([data], { type: `audio/${format}` }); - + // Cleanup await ffmpeg.deleteFile(inputName); await ffmpeg.deleteFile(outputName); - + return blob; -}; +}; \ No newline at end of file diff --git a/apps/web/src/lib/font-config.ts b/apps/web/src/lib/font-config.ts index b47b3c3e..0389054a 100644 --- a/apps/web/src/lib/font-config.ts +++ b/apps/web/src/lib/font-config.ts @@ -1,39 +1,39 @@ -import { - Inter, - Roboto, - Open_Sans, - Playfair_Display, - Comic_Neue, -} from "next/font/google"; - -// Configure all fonts -const inter = Inter({ subsets: ["latin"] }); -const roboto = Roboto({ subsets: ["latin"], weight: ["400", "700"] }); -const openSans = Open_Sans({ subsets: ["latin"] }); -const playfairDisplay = Playfair_Display({ subsets: ["latin"] }); -const comicNeue = Comic_Neue({ subsets: ["latin"], weight: ["400", "700"] }); - -// Export font class mapping for use in components -export const FONT_CLASS_MAP = { - Inter: inter.className, - Roboto: roboto.className, - "Open Sans": openSans.className, - "Playfair Display": playfairDisplay.className, - "Comic Neue": comicNeue.className, - Arial: "", - Helvetica: "", - "Times New Roman": "", - Georgia: "", -} as const; - -// Export individual fonts for use in layout -export const fonts = { - inter, - roboto, - openSans, - playfairDisplay, - comicNeue, -}; - -// Default font for the body -export const defaultFont = inter; +import { + Inter, + Roboto, + Open_Sans, + Playfair_Display, + Comic_Neue, +} from "next/font/google"; + +// Configure all fonts +const inter = Inter({ subsets: ["latin"] }); +const roboto = Roboto({ subsets: ["latin"], weight: ["400", "700"] }); +const openSans = Open_Sans({ subsets: ["latin"] }); +const playfairDisplay = Playfair_Display({ subsets: ["latin"] }); +const comicNeue = Comic_Neue({ subsets: ["latin"], weight: ["400", "700"] }); + +// Export font class mapping for use in components +export const FONT_CLASS_MAP = { + Inter: inter.className, + Roboto: roboto.className, + "Open Sans": openSans.className, + "Playfair Display": playfairDisplay.className, + "Comic Neue": comicNeue.className, + Arial: "", + Helvetica: "", + "Times New Roman": "", + Georgia: "", +} as const; + +// Export individual fonts for use in layout +export const fonts = { + inter, + roboto, + openSans, + playfairDisplay, + comicNeue, +}; + +// Default font for the body +export const defaultFont = inter; diff --git a/apps/web/src/lib/rate-limit.ts b/apps/web/src/lib/rate-limit.ts index e1996cf5..ac63a64c 100644 --- a/apps/web/src/lib/rate-limit.ts +++ b/apps/web/src/lib/rate-limit.ts @@ -1,16 +1,16 @@ -// lib/rate-limit.ts -import { Ratelimit } from "@upstash/ratelimit"; -import { Redis } from "@upstash/redis"; -import { env } from "@/env"; - -const redis = new Redis({ - url: env.UPSTASH_REDIS_REST_URL, - token: env.UPSTASH_REDIS_REST_TOKEN, -}); - -export const waitlistRateLimit = new Ratelimit({ - redis, - limiter: Ratelimit.slidingWindow(5, "1 m"), // 5 requests per minute - analytics: true, - prefix: "waitlist-rate-limit", -}); +// lib/rate-limit.ts +import { Ratelimit } from "@upstash/ratelimit"; +import { Redis } from "@upstash/redis"; +import { env } from "@/env"; + +const redis = new Redis({ + url: env.UPSTASH_REDIS_REST_URL, + token: env.UPSTASH_REDIS_REST_TOKEN, +}); + +export const waitlistRateLimit = new Ratelimit({ + redis, + limiter: Ratelimit.slidingWindow(5, "1 m"), // 5 requests per minute + analytics: true, + prefix: "waitlist-rate-limit", +}); diff --git a/apps/web/src/lib/storage/indexeddb-adapter.ts b/apps/web/src/lib/storage/indexeddb-adapter.ts index 597305c7..f43a8798 100644 --- a/apps/web/src/lib/storage/indexeddb-adapter.ts +++ b/apps/web/src/lib/storage/indexeddb-adapter.ts @@ -1,89 +1,89 @@ -import { StorageAdapter } from "./types"; - -export class IndexedDBAdapter implements StorageAdapter { - private dbName: string; - private storeName: string; - private version: number; - - constructor(dbName: string, storeName: string, version = 1) { - this.dbName = dbName; - this.storeName = storeName; - this.version = version; - } - - private async getDB(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open(this.dbName, this.version); - - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result); - - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - if (!db.objectStoreNames.contains(this.storeName)) { - db.createObjectStore(this.storeName, { keyPath: "id" }); - } - }; - }); - } - - async get(key: string): Promise { - const db = await this.getDB(); - const transaction = db.transaction([this.storeName], "readonly"); - const store = transaction.objectStore(this.storeName); - - return new Promise((resolve, reject) => { - const request = store.get(key); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result || null); - }); - } - - async set(key: string, value: T): Promise { - const db = await this.getDB(); - const transaction = db.transaction([this.storeName], "readwrite"); - const store = transaction.objectStore(this.storeName); - - return new Promise((resolve, reject) => { - const request = store.put({ id: key, ...value }); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(); - }); - } - - async remove(key: string): Promise { - const db = await this.getDB(); - const transaction = db.transaction([this.storeName], "readwrite"); - const store = transaction.objectStore(this.storeName); - - return new Promise((resolve, reject) => { - const request = store.delete(key); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(); - }); - } - - async list(): Promise { - const db = await this.getDB(); - const transaction = db.transaction([this.storeName], "readonly"); - const store = transaction.objectStore(this.storeName); - - return new Promise((resolve, reject) => { - const request = store.getAllKeys(); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(request.result as string[]); - }); - } - - async clear(): Promise { - const db = await this.getDB(); - const transaction = db.transaction([this.storeName], "readwrite"); - const store = transaction.objectStore(this.storeName); - - return new Promise((resolve, reject) => { - const request = store.clear(); - request.onerror = () => reject(request.error); - request.onsuccess = () => resolve(); - }); - } -} +import { StorageAdapter } from "./types"; + +export class IndexedDBAdapter implements StorageAdapter { + private dbName: string; + private storeName: string; + private version: number; + + constructor(dbName: string, storeName: string, version: number = 1) { + this.dbName = dbName; + this.storeName = storeName; + this.version = version; + } + + private async getDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.dbName, this.version); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(this.storeName)) { + db.createObjectStore(this.storeName, { keyPath: "id" }); + } + }; + }); + } + + async get(key: string): Promise { + const db = await this.getDB(); + const transaction = db.transaction([this.storeName], "readonly"); + const store = transaction.objectStore(this.storeName); + + return new Promise((resolve, reject) => { + const request = store.get(key); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result || null); + }); + } + + async set(key: string, value: T): Promise { + const db = await this.getDB(); + const transaction = db.transaction([this.storeName], "readwrite"); + const store = transaction.objectStore(this.storeName); + + return new Promise((resolve, reject) => { + const request = store.put({ id: key, ...value }); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(); + }); + } + + async remove(key: string): Promise { + const db = await this.getDB(); + const transaction = db.transaction([this.storeName], "readwrite"); + const store = transaction.objectStore(this.storeName); + + return new Promise((resolve, reject) => { + const request = store.delete(key); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(); + }); + } + + async list(): Promise { + const db = await this.getDB(); + const transaction = db.transaction([this.storeName], "readonly"); + const store = transaction.objectStore(this.storeName); + + return new Promise((resolve, reject) => { + const request = store.getAllKeys(); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result as string[]); + }); + } + + async clear(): Promise { + const db = await this.getDB(); + const transaction = db.transaction([this.storeName], "readwrite"); + const store = transaction.objectStore(this.storeName); + + return new Promise((resolve, reject) => { + const request = store.clear(); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(); + }); + } +} diff --git a/apps/web/src/lib/storage/opfs-adapter.ts b/apps/web/src/lib/storage/opfs-adapter.ts index 87b5c886..c01954e5 100644 --- a/apps/web/src/lib/storage/opfs-adapter.ts +++ b/apps/web/src/lib/storage/opfs-adapter.ts @@ -3,7 +3,7 @@ import { StorageAdapter } from "./types"; export class OPFSAdapter implements StorageAdapter { private directoryName: string; - constructor(directoryName = "media") { + constructor(directoryName: string = "media") { this.directoryName = directoryName; } diff --git a/apps/web/src/lib/time.ts b/apps/web/src/lib/time.ts index df02d837..b3147946 100644 --- a/apps/web/src/lib/time.ts +++ b/apps/web/src/lib/time.ts @@ -1,25 +1,25 @@ -// Time-related utility functions - -// Helper function to format time in various formats (MM:SS, HH:MM:SS, HH:MM:SS:CS, HH:MM:SS:FF) -export const formatTimeCode = ( - timeInSeconds: number, - format: "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF" = "HH:MM:SS:CS", - fps = 30 -): string => { - const hours = Math.floor(timeInSeconds / 3600); - const minutes = Math.floor((timeInSeconds % 3600) / 60); - const seconds = Math.floor(timeInSeconds % 60); - const centiseconds = Math.floor((timeInSeconds % 1) * 100); - const frames = Math.floor((timeInSeconds % 1) * fps); - - switch (format) { - case "MM:SS": - return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; - case "HH:MM:SS": - return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; - case "HH:MM:SS:CS": - return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${centiseconds.toString().padStart(2, "0")}`; - case "HH:MM:SS:FF": - return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${frames.toString().padStart(2, "0")}`; - } -}; +// Time-related utility functions + +// Helper function to format time in various formats (MM:SS, HH:MM:SS, HH:MM:SS:CS, HH:MM:SS:FF) +export const formatTimeCode = ( + timeInSeconds: number, + format: "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF" = "HH:MM:SS:CS", + fps: number = 30 +): string => { + const hours = Math.floor(timeInSeconds / 3600); + const minutes = Math.floor((timeInSeconds % 3600) / 60); + const seconds = Math.floor(timeInSeconds % 60); + const centiseconds = Math.floor((timeInSeconds % 1) * 100); + const frames = Math.floor((timeInSeconds % 1) * fps); + + switch (format) { + case "MM:SS": + return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + case "HH:MM:SS": + return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; + case "HH:MM:SS:CS": + return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${centiseconds.toString().padStart(2, "0")}`; + case "HH:MM:SS:FF": + return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${frames.toString().padStart(2, "0")}`; + } +}; diff --git a/apps/web/src/lib/timeline.ts b/apps/web/src/lib/timeline.ts index 85fcabe6..531defac 100644 --- a/apps/web/src/lib/timeline.ts +++ b/apps/web/src/lib/timeline.ts @@ -1,54 +1,54 @@ -import { TimelineElement } from "@/types/timeline"; - -// Helper function to check for element overlaps and prevent invalid timeline states -export const checkElementOverlaps = (elements: TimelineElement[]): boolean => { - // Sort elements by start time - const sortedElements = [...elements].sort( - (a, b) => a.startTime - b.startTime - ); - - for (let i = 0; i < sortedElements.length - 1; i++) { - const current = sortedElements[i]; - const next = sortedElements[i + 1]; - - const currentEnd = - current.startTime + - (current.duration - current.trimStart - current.trimEnd); - - // Check if current element overlaps with next element - if (currentEnd > next.startTime) return true; // Overlap detected - } - - return false; // No overlaps -}; - -// Helper function to resolve overlaps by adjusting element positions -export const resolveElementOverlaps = ( - elements: TimelineElement[] -): TimelineElement[] => { - // Sort elements by start time - const sortedElements = [...elements].sort( - (a, b) => a.startTime - b.startTime - ); - const resolvedElements: TimelineElement[] = []; - - for (let i = 0; i < sortedElements.length; i++) { - const current = { ...sortedElements[i] }; - - if (resolvedElements.length > 0) { - const previous = resolvedElements[resolvedElements.length - 1]; - const previousEnd = - previous.startTime + - (previous.duration - previous.trimStart - previous.trimEnd); - - // If current element would overlap with previous, push it after previous ends - if (current.startTime < previousEnd) { - current.startTime = previousEnd; - } - } - - resolvedElements.push(current); - } - - return resolvedElements; -}; +import { TimelineElement } from "@/types/timeline"; + +// Helper function to check for element overlaps and prevent invalid timeline states +export const checkElementOverlaps = (elements: TimelineElement[]): boolean => { + // Sort elements by start time + const sortedElements = [...elements].sort( + (a, b) => a.startTime - b.startTime + ); + + for (let i = 0; i < sortedElements.length - 1; i++) { + const current = sortedElements[i]; + const next = sortedElements[i + 1]; + + const currentEnd = + current.startTime + + (current.duration - current.trimStart - current.trimEnd); + + // Check if current element overlaps with next element + if (currentEnd > next.startTime) return true; // Overlap detected + } + + return false; // No overlaps +}; + +// Helper function to resolve overlaps by adjusting element positions +export const resolveElementOverlaps = ( + elements: TimelineElement[] +): TimelineElement[] => { + // Sort elements by start time + const sortedElements = [...elements].sort( + (a, b) => a.startTime - b.startTime + ); + const resolvedElements: TimelineElement[] = []; + + for (let i = 0; i < sortedElements.length; i++) { + const current = { ...sortedElements[i] }; + + if (resolvedElements.length > 0) { + const previous = resolvedElements[resolvedElements.length - 1]; + const previousEnd = + previous.startTime + + (previous.duration - previous.trimStart - previous.trimEnd); + + // If current element would overlap with previous, push it after previous ends + if (current.startTime < previousEnd) { + current.startTime = previousEnd; + } + } + + resolvedElements.push(current); + } + + return resolvedElements; +}; diff --git a/apps/web/src/lib/waitlist.ts b/apps/web/src/lib/waitlist.ts index 43a620b0..22b15b78 100644 --- a/apps/web/src/lib/waitlist.ts +++ b/apps/web/src/lib/waitlist.ts @@ -1,13 +1,13 @@ -import { db, sql, waitlist } from "@opencut/db"; - -export async function getWaitlistCount() { - try { - const result = await db - .select({ count: sql`count(*)` }) - .from(waitlist); - return result[0]?.count || 0; - } catch (error) { - console.error("Failed to fetch waitlist count:", error); - return 0; - } -} +import { db, sql, waitlist } from "@opencut/db"; + +export async function getWaitlistCount() { + try { + const result = await db + .select({ count: sql`count(*)` }) + .from(waitlist); + return result[0]?.count || 0; + } catch (error) { + console.error("Failed to fetch waitlist count:", error); + return 0; + } +} -- cgit v1.3.1