feat: add ultracite (#452)

* Init ultracite

* Update scripts and biome.jsonc

* Update biome.jsonc

* Update biome.jsonc

* Update biome.jsonc

* Run format
This commit is contained in:
Hayden Bleasel
2025-07-25 00:41:34 +03:00
committed by GitHub
parent 7b840e9b4e
commit 5931ddb77a
113 changed files with 8033 additions and 7070 deletions
+48 -39
View File
@@ -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);
}
+2 -2
View File
@@ -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);
+124 -85
View File
@@ -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;
};
};
+39 -39
View File
@@ -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;
+16 -16
View File
@@ -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",
});
+89 -89
View File
@@ -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();
});
}
}
+1 -1
View File
@@ -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;
}
+25 -25
View File
@@ -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")}`;
}
};
+54 -54
View File
@@ -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;
};
+13 -13
View File
@@ -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;
}
}