From 294ba01abeb2d8c3d3a04cb7ecc54abf97a6d4d3 Mon Sep 17 00:00:00 2001 From: Hyteq Date: Mon, 23 Jun 2025 15:39:56 +0300 Subject: [PATCH 1/4] feat: initial state for composition and overlay editing on the video player --- apps/web/src/app/editor/page.tsx | 6 +- .../src/components/editor/preview-panel.tsx | 536 +++++++++++++++--- 2 files changed, 449 insertions(+), 93 deletions(-) diff --git a/apps/web/src/app/editor/page.tsx b/apps/web/src/app/editor/page.tsx index 2f825a22..64186a3e 100644 --- a/apps/web/src/app/editor/page.tsx +++ b/apps/web/src/app/editor/page.tsx @@ -7,7 +7,7 @@ import { ResizableHandle, } from "../../components/ui/resizable"; import { MediaPanel } from "../../components/editor/media-panel"; -import { PropertiesPanel } from "../../components/editor/properties-panel"; +// import { PropertiesPanel } from "../../components/editor/properties-panel"; import { Timeline } from "../../components/editor/timeline"; import { PreviewPanel } from "../../components/editor/preview-panel"; import { EditorHeader } from "@/components/editor-header"; @@ -74,13 +74,13 @@ export default function Editor() { {/* Properties Panel */} - - + */} diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 132d6c06..5d03e941 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -3,110 +3,397 @@ import { useTimelineStore } from "@/stores/timeline-store"; import { useMediaStore } from "@/stores/media-store"; import { usePlaybackStore } from "@/stores/playback-store"; -import { ImageTimelineTreatment } from "@/components/ui/image-timeline-treatment"; import { VideoPlayer } from "@/components/ui/video-player"; import { Button } from "@/components/ui/button"; -import { Play, Pause } from "lucide-react"; +import { Play, Pause, Move, RotateCw, Crop, ZoomIn, ZoomOut } from "lucide-react"; +import { useState, useRef, useEffect, useCallback } from "react"; + +interface ClipTransform { + x: number; + y: number; + scale: number; + rotation: number; + opacity: number; + width: number; + height: number; + blendMode: string; + cropTop: number; + cropBottom: number; + cropLeft: number; + cropRight: number; +} + +interface DragState { + isDragging: boolean; + dragType: 'move' | 'resize-nw' | 'resize-ne' | 'resize-sw' | 'resize-se' | 'rotate' | 'scale' | 'crop-n' | 'crop-s' | 'crop-e' | 'crop-w'; + startMouseX: number; + startMouseY: number; + startTransform: ClipTransform; + clipId: string; +} export function PreviewPanel() { const { tracks } = useTimelineStore(); const { mediaItems } = useMediaStore(); const { isPlaying, toggle, currentTime } = usePlaybackStore(); - // Find the active clip at the current playback time - const getActiveClip = () => { - for (const track of tracks) { - for (const clip of track.clips) { + const [clipTransforms, setClipTransforms] = useState>({}); + const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 }); // Default 16:9 + const [dragState, setDragState] = useState(null); + const previewRef = useRef(null); + + // Get all active clips at current time (for overlaying) + const getActiveClips = () => { + const activeClips: Array<{ + clip: any; + track: any; + mediaItem: any; + layer: number; + }> = []; + + tracks.forEach((track, trackIndex) => { + track.clips.forEach((clip) => { const clipStart = clip.startTime; const clipEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); if (currentTime >= clipStart && currentTime < clipEnd) { - return clip; + const mediaItem = clip.mediaId === "test" + ? { type: "test", name: clip.name, url: "", thumbnailUrl: "" } + : mediaItems.find((item) => item.id === clip.mediaId); + + if (mediaItem || clip.mediaId === "test") { + activeClips.push({ + clip, + track, + mediaItem, + layer: trackIndex, // Track index determines layer order + }); + } } + }); + }); + + // Sort by layer (track order) - higher index = on top + return activeClips.sort((a, b) => a.layer - b.layer); + }; + + const activeClips = getActiveClips(); + const aspectRatio = canvasSize.width / canvasSize.height; + + // Get or create transform for a clip + const getClipTransform = (clipId: string): ClipTransform => { + return clipTransforms[clipId] || { + x: 0, + y: 0, + scale: 1, + rotation: 0, + opacity: 1, + width: 100, // Percentage of canvas + height: 100, + blendMode: 'normal', + cropTop: 0, + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + }; + }; + + // Update clip transform + const updateClipTransform = useCallback((clipId: string, updates: Partial) => { + setClipTransforms(prev => { + const currentTransform = prev[clipId] || { + x: 0, + y: 0, + scale: 1, + rotation: 0, + opacity: 1, + width: 100, + height: 100, + blendMode: 'normal', + cropTop: 0, + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + }; + + return { + ...prev, + [clipId]: { ...currentTransform, ...updates } + }; + }); + }, []); + + // Mouse event handlers + const handleMouseDown = (e: React.MouseEvent, clipId: string, dragType: DragState['dragType']) => { + e.preventDefault(); + e.stopPropagation(); + + setDragState({ + isDragging: true, + dragType, + startMouseX: e.clientX, + startMouseY: e.clientY, + startTransform: getClipTransform(clipId), + clipId + }); + }; + + const handleMouseMove = useCallback((e: MouseEvent) => { + if (!dragState || !dragState.isDragging) return; + + const deltaX = e.clientX - dragState.startMouseX; + const deltaY = e.clientY - dragState.startMouseY; + const { startTransform, clipId, dragType } = dragState; + + switch (dragType) { + case 'move': + updateClipTransform(clipId, { + x: Math.max(-100, Math.min(100, startTransform.x + deltaX * 0.3)), + y: Math.max(-100, Math.min(100, startTransform.y + deltaY * 0.3)) + }); + break; + + case 'resize-nw': + updateClipTransform(clipId, { + width: Math.max(20, startTransform.width - deltaX * 0.5), + height: Math.max(20, startTransform.height - deltaY * 0.5) + }); + break; + + case 'resize-ne': + updateClipTransform(clipId, { + width: Math.max(20, startTransform.width + deltaX * 0.5), + height: Math.max(20, startTransform.height - deltaY * 0.5) + }); + break; + + case 'resize-sw': + updateClipTransform(clipId, { + width: Math.max(20, startTransform.width - deltaX * 0.5), + height: Math.max(20, startTransform.height + deltaY * 0.5) + }); + break; + + case 'resize-se': + updateClipTransform(clipId, { + width: Math.max(20, startTransform.width + deltaX * 0.5), + height: Math.max(20, startTransform.height + deltaY * 0.5) + }); + break; + + case 'rotate': + updateClipTransform(clipId, { + rotation: (startTransform.rotation + deltaX * 2) % 360 + }); + break; + + case 'scale': + updateClipTransform(clipId, { + scale: Math.max(0.1, Math.min(3, startTransform.scale + deltaX * 0.01)) + }); + break; + + case 'crop-n': + updateClipTransform(clipId, { + cropTop: Math.max(0, Math.min(40, startTransform.cropTop + deltaY * 0.2)) + }); + break; + + case 'crop-s': + updateClipTransform(clipId, { + cropBottom: Math.max(0, Math.min(40, startTransform.cropBottom - deltaY * 0.2)) + }); + break; + + case 'crop-e': + updateClipTransform(clipId, { + cropRight: Math.max(0, Math.min(40, startTransform.cropRight - deltaX * 0.2)) + }); + break; + + case 'crop-w': + updateClipTransform(clipId, { + cropLeft: Math.max(0, Math.min(40, startTransform.cropLeft + deltaX * 0.2)) + }); + break; + } + }, [dragState, updateClipTransform]); + + const handleMouseUp = useCallback(() => { + setDragState(null); + }, []); + + // Add global mouse event listeners + useEffect(() => { + if (dragState?.isDragging) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + } + }, [dragState, handleMouseMove, handleMouseUp]); + + // Initialize transforms for new clips + useEffect(() => { + const activeClips = getActiveClips(); + const newTransforms: Record = {}; + let hasNewClips = false; + + activeClips.forEach(({ clip }) => { + if (!clipTransforms[clip.id]) { + hasNewClips = true; + newTransforms[clip.id] = { + x: 0, + y: 0, + scale: 1, + rotation: 0, + opacity: 1, + width: 100, + height: 100, + blendMode: 'normal', + cropTop: 0, + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + }; } + }); + + if (hasNewClips) { + setClipTransforms(prev => ({ ...prev, ...newTransforms })); } - return null; - }; + }, [tracks, currentTime]); // Re-run when tracks or time changes - const activeClip = getActiveClip(); - const activeMediaItem = activeClip - ? mediaItems.find((item) => item.id === activeClip.mediaId) - : null; - const aspectRatio = activeMediaItem?.aspectRatio || 16 / 9; - const renderContent = () => { - if (!activeClip) { + // Render a single clip layer + const renderClipLayer = (clipData: any, index: number) => { + const { clip, mediaItem } = clipData; + const transform = getClipTransform(clip.id); + + const layerStyle = { + position: 'absolute' as const, + left: '50%', + top: '50%', + width: `${transform.width}%`, + height: `${transform.height}%`, + transform: `translate(-50%, -50%) translate(${transform.x}%, ${transform.y}%) scale(${transform.scale}) rotate(${transform.rotation}deg)`, + opacity: transform.opacity, + mixBlendMode: transform.blendMode as any, + clipPath: `inset(${transform.cropTop}% ${transform.cropRight}% ${transform.cropBottom}% ${transform.cropLeft}%)`, + zIndex: index + 10, + cursor: dragState?.isDragging && dragState.clipId === clip.id ? 'grabbing' : 'grab', + userSelect: 'none' as const, + }; + + const handleClipMouseDown = (e: React.MouseEvent) => { + e.stopPropagation(); + handleMouseDown(e, clip.id, 'move'); + }; + + // Handle test clips + if (!mediaItem || clip.mediaId === "test") { return ( -
- {tracks.length === 0 ? "Drop media to start editing" : "No clip at current time"} -
- ); - } - - // Handle test clips without media items - if (!activeMediaItem && activeClip.mediaId === "test") { - return ( -
-
-
🎬
-

{activeClip.name}

-

Test clip for playback

+
+
+
🎬
+

{clip.name}

+ + {/* Hover resize corners */} +
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } - if (!activeMediaItem) { + // Render video + if (mediaItem.type === "video") { return ( -
- Media not found +
+ + + {/* Hover resize corners */} +
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } - if (activeMediaItem.type === "video") { + // Render image + if (mediaItem.type === "image") { return ( - +
+ {mediaItem.name} + + {/* Hover resize corners */} +
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
+
); } - if (activeMediaItem.type === "image") { + // Render audio (visual representation) + if (mediaItem.type === "audio") { return ( - - ); - } - - if (activeMediaItem.type === "audio") { - return ( -
-
-
🎵
-

{activeMediaItem.name}

- +
+
+
🎵
+

{mediaItem.name}

+ + {/* Hover resize corners */} +
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-nw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-ne'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-sw'); }}>
+
{ e.stopPropagation(); handleMouseDown(e, clip.id, 'resize-se'); }}>
); } @@ -114,31 +401,100 @@ export function PreviewPanel() { return null; }; + // Canvas size presets + const canvasPresets = [ + { name: "16:9 HD", width: 1920, height: 1080 }, + { name: "16:9 4K", width: 3840, height: 2160 }, + { name: "9:16 Mobile", width: 1080, height: 1920 }, + { name: "1:1 Square", width: 1080, height: 1080 }, + { name: "4:3 Standard", width: 1440, height: 1080 }, + ]; + return ( -
-
1 ? "100%" : "auto", - height: aspectRatio <= 1 ? "100%" : "auto", - maxWidth: "100%", - maxHeight: "100%", - }} - > - {renderContent()} +
+ {/* Canvas Controls */} +
+ Canvas: + + +
- {activeMediaItem && ( -
-

- {activeMediaItem.name} -

-

- {aspectRatio.toFixed(2)} • {aspectRatio > 1 ? "Landscape" : aspectRatio < 1 ? "Portrait" : "Square"} -

+ {/* Preview Area - Full Width */} +
+
1 ? "100%" : "auto", + height: aspectRatio <= 1 ? "100%" : "auto", + maxWidth: "100%", + maxHeight: "100%", + background: '#000000', + border: '1px solid #374151' + }} + + > + + + {/* Render all active clips as layers */} + {activeClips.length === 0 ? ( +
+ {tracks.length === 0 ? "Drop media to start editing" : "No clips at current time"} +
+ ) : ( + activeClips.map((clipData, index) => renderClipLayer(clipData, index)) + )} + +
- )} +
+ + {/* Bottom Info Panel */} +
+ {/* Layer List */} +
+
Active Layers ({activeClips.length})
+
+ {activeClips.map((clipData, index) => ( +
+ + {index + 1} + + {clipData.clip.name} + {clipData.track.name} +
+ ))} +
+
+ + +
); } From 054c68140c874ce7e42ad9a36f74867f5b01a239 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 23 Jun 2025 16:25:06 +0200 Subject: [PATCH 2/4] feat: add redirect from appcut to opencut in Netlify configuration --- apps/web/netlify.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/web/netlify.toml b/apps/web/netlify.toml index 5aadb562..4cf54843 100644 --- a/apps/web/netlify.toml +++ b/apps/web/netlify.toml @@ -4,3 +4,9 @@ [[plugins]] package = "@netlify/plugin-nextjs" + +[[redirects]] + from = "https://appcut.app/*" + to = "https://opencut.app/:splat" + status = 301 + force = true From 0f7217f42d3b210edb7156a436246b0184facbd3 Mon Sep 17 00:00:00 2001 From: Hyteq Date: Mon, 23 Jun 2025 17:43:44 +0300 Subject: [PATCH 3/4] dik --- .../components/editor/properties-panel.tsx | 65 ------------------- 1 file changed, 65 deletions(-) diff --git a/apps/web/src/components/editor/properties-panel.tsx b/apps/web/src/components/editor/properties-panel.tsx index af80f464..496550b7 100644 --- a/apps/web/src/components/editor/properties-panel.tsx +++ b/apps/web/src/components/editor/properties-panel.tsx @@ -40,72 +40,7 @@ export function PropertiesPanel() { return (
- {/* Image Treatment - only show if an image is selected */} - {firstImageItem && ( - <> -
-

Image Treatment

-
- {/* Preview */} -
- -
- -
-
- {/* Background Type */} -
- - -
- - {/* Background Color - only show for color type */} - {backgroundType === "color" && ( -
- -
- setBackgroundColor(e.target.value)} - className="w-16 h-10 p-1" - /> - setBackgroundColor(e.target.value)} - placeholder="#000000" - className="flex-1" - /> -
-
- )} -
-
- - - - )} {/* Transform */}
From 2cad7f9c3cc8fd119c1d7940851343b02f2175dc Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Mon, 23 Jun 2025 16:44:07 +0200 Subject: [PATCH 4/4] feat: add waitlist table and migration for tracking user signups --- apps/web/migrations/0001_tricky_jackpot.sql | 8 + apps/web/migrations/meta/0001_snapshot.json | 358 ++++++++++++++++++++ apps/web/migrations/meta/_journal.json | 7 + 3 files changed, 373 insertions(+) create mode 100644 apps/web/migrations/0001_tricky_jackpot.sql create mode 100644 apps/web/migrations/meta/0001_snapshot.json diff --git a/apps/web/migrations/0001_tricky_jackpot.sql b/apps/web/migrations/0001_tricky_jackpot.sql new file mode 100644 index 00000000..6eaeb56e --- /dev/null +++ b/apps/web/migrations/0001_tricky_jackpot.sql @@ -0,0 +1,8 @@ +CREATE TABLE "waitlist" ( + "id" text PRIMARY KEY NOT NULL, + "email" text NOT NULL, + "created_at" timestamp NOT NULL, + CONSTRAINT "waitlist_email_unique" UNIQUE("email") +); +--> statement-breakpoint +ALTER TABLE "waitlist" ENABLE ROW LEVEL SECURITY; \ No newline at end of file diff --git a/apps/web/migrations/meta/0001_snapshot.json b/apps/web/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..78c3cffb --- /dev/null +++ b/apps/web/migrations/meta/0001_snapshot.json @@ -0,0 +1,358 @@ +{ + "id": "b7d920ca-6dd0-430f-8ee6-1d38fdf3e80f", + "prevId": "4440fb90-cf0e-4cb2-afad-08250ce3dc1e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.verifications": { + "name": "verifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json index 3f918785..9cbceb13 100644 --- a/apps/web/migrations/meta/_journal.json +++ b/apps/web/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1750581188229, "tag": "0000_hot_the_fallen", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1750689835736, + "tag": "0001_tricky_jackpot", + "breakpoints": true } ] } \ No newline at end of file