From 5931ddb77aac23ffe1b82d9d7c03afde3e671109 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Thu, 24 Jul 2025 14:41:34 -0700 Subject: feat: add ultracite (#452) * Init ultracite * Update scripts and biome.jsonc * Update biome.jsonc * Update biome.jsonc * Update biome.jsonc * Run format --- 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, 412 insertions(+), 364 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 430f58bf..5cba0cf3 100644 --- a/apps/web/src/lib/blog-query.ts +++ b/apps/web/src/lib/blog-query.ts @@ -1,4 +1,10 @@ -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"; @@ -6,50 +12,53 @@ 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}`); - } - return await response.json() as T; - } catch (error) { - console.error(`Error fetching ${endpoint}:`, error); - throw error; + 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; } - - 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 c4444a62..0372f25b 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 >= 1_000) - return (count / 1_000).toFixed(1).replace(/\.0$/, "") + "k"; + if (count >= 1000) + return (count / 1000).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 ae0c267b..c34200f6 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,34 +21,42 @@ export const initFFmpeg = async (): Promise => { export const generateThumbnail = async ( videoFile: File, - timeInSeconds: number = 1 + timeInSeconds = 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); }; @@ -59,43 +67,52 @@ 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; @@ -103,27 +120,32 @@ export const getVideoInfo = async (videoFile: File): Promise<{ }> => { 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 @@ -143,8 +165,12 @@ export const getVideoInfo = async (videoFile: File): Promise<{ 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]); @@ -155,7 +181,7 @@ export const getVideoInfo = async (videoFile: File): Promise<{ duration, width, height, - fps + fps, }; }; @@ -164,68 +190,81 @@ 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 0389054a..b47b3c3e 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 ac63a64c..e1996cf5 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 f43a8798..597305c7 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: 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(); - }); - } -} +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(); + }); + } +} diff --git a/apps/web/src/lib/storage/opfs-adapter.ts b/apps/web/src/lib/storage/opfs-adapter.ts index c01954e5..87b5c886 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: string = "media") { + constructor(directoryName = "media") { this.directoryName = directoryName; } diff --git a/apps/web/src/lib/time.ts b/apps/web/src/lib/time.ts index b3147946..df02d837 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: 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")}`; - } -}; +// 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")}`; + } +}; diff --git a/apps/web/src/lib/timeline.ts b/apps/web/src/lib/timeline.ts index 531defac..85fcabe6 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 22b15b78..43a620b0 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