diff options
Diffstat (limited to 'apps/web/src/lib')
| -rw-r--r-- | apps/web/src/lib/blog-query.ts | 87 | ||||
| -rw-r--r-- | apps/web/src/lib/fetch-github-stars.ts | 4 | ||||
| -rw-r--r-- | apps/web/src/lib/ffmpeg-utils.ts | 209 | ||||
| -rw-r--r-- | apps/web/src/lib/font-config.ts | 78 | ||||
| -rw-r--r-- | apps/web/src/lib/rate-limit.ts | 32 | ||||
| -rw-r--r-- | apps/web/src/lib/storage/indexeddb-adapter.ts | 178 | ||||
| -rw-r--r-- | apps/web/src/lib/storage/opfs-adapter.ts | 2 | ||||
| -rw-r--r-- | apps/web/src/lib/time.ts | 50 | ||||
| -rw-r--r-- | apps/web/src/lib/timeline.ts | 108 | ||||
| -rw-r--r-- | apps/web/src/lib/waitlist.ts | 26 |
10 files changed, 363 insertions, 411 deletions
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<T>(endpoint: string): Promise<T> { - 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<MarblePostList>("posts"); -} - -export async function getTags() { - return fetchFromMarble<MarbleTagList>("tags"); -} - -export async function getSinglePost(slug: string) { - return fetchFromMarble<MarblePost>(`posts/${slug}`); -} - -export async function getCategories() { - return fetchFromMarble<MarbleCategoryList>("categories"); -} - -export async function getAuthors() { - return fetchFromMarble<MarbleAuthorList>("authors"); -} + + export async function getPosts() { + return fetchFromMarble<MarblePostList>('posts'); + } + + export async function getTags() { + return fetchFromMarble<MarbleTagList>('tags'); + } + + export async function getSinglePost(slug: string) { + return fetchFromMarble<MarblePost>(`posts/${slug}`); + } + + export async function getCategories() { + return fetchFromMarble<MarbleCategoryList>('categories'); + } + + export async function getAuthors() { + return fetchFromMarble<MarbleAuthorList>('authors'); + } export async function processHtmlContent(html: string): Promise<string> { - const processor = unified() - .use(rehypeSanitize) - .use(rehypeParse, { fragment: true }) - .use(rehypeSlug) - .use(rehypeAutolinkHeadings, { behavior: "append" }) - .use(rehypeStringify); + 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 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<string> { 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<FFmpeg> => { 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<FFmpeg> => { export const generateThumbnail = async ( videoFile: File, - timeInSeconds = 1 + timeInSeconds: number = 1 ): Promise<string> => { 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<Blob> => { 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<Blob> => { 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<Blob> => { 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<T> implements StorageAdapter<T> { - 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<IDBDatabase> { - 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<T | null> { - 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<void> { - 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<void> { - 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<string[]> { - 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<void> { - 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<T> implements StorageAdapter<T> {
+ 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<IDBDatabase> {
+ 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<T | null> {
+ 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<void> {
+ 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<void> {
+ 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<string[]> {
+ 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<void> {
+ 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<File> { 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<number>`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<number>`count(*)` })
+ .from(waitlist);
+ return result[0]?.count || 0;
+ } catch (error) {
+ console.error("Failed to fetch waitlist count:", error);
+ return 0;
+ }
+}
|
