summaryrefslogtreecommitdiff
path: root/apps/web/src/lib
diff options
context:
space:
mode:
authorKha Nguyen <hi@khanguyen.com>2025-07-24 16:50:14 -0500
committerKha Nguyen <hi@khanguyen.com>2025-07-24 16:50:14 -0500
commit262a7d53d63cc9997f730c7f64b7f8ec29723926 (patch)
tree894d6e3986b3ca339dc3ff7ec62fb31b3690be82 /apps/web/src/lib
parent5a7745f806edf4a386b8cfd079861f96c3b92baf (diff)
parent5931ddb77aac23ffe1b82d9d7c03afde3e671109 (diff)
downloadopencut-262a7d53d63cc9997f730c7f64b7f8ec29723926.tar.gz
Merge branch 'main' into fix/improve-shortcuts
Diffstat (limited to 'apps/web/src/lib')
-rw-r--r--apps/web/src/lib/blog-query.ts87
-rw-r--r--apps/web/src/lib/fetch-github-stars.ts4
-rw-r--r--apps/web/src/lib/ffmpeg-utils.ts209
-rw-r--r--apps/web/src/lib/font-config.ts78
-rw-r--r--apps/web/src/lib/rate-limit.ts32
-rw-r--r--apps/web/src/lib/storage/indexeddb-adapter.ts178
-rw-r--r--apps/web/src/lib/storage/opfs-adapter.ts2
-rw-r--r--apps/web/src/lib/time.ts50
-rw-r--r--apps/web/src/lib/timeline.ts108
-rw-r--r--apps/web/src/lib/waitlist.ts26
10 files changed, 411 insertions, 363 deletions
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<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}`);
- }
- 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<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 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<string> {
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<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,34 +21,42 @@ export const initFFmpeg = async (): Promise<FFmpeg> => {
export const generateThumbnail = async (
videoFile: File,
- timeInSeconds: number = 1
+ timeInSeconds = 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);
};
@@ -59,43 +67,52 @@ 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;
@@ -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<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 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<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();
- });
- }
-}
+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();
+ });
+ }
+}
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<File> {
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<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;
+ }
+}