From 18d2186101043c251687235bddf13b7f00b16846 Mon Sep 17 00:00:00 2001 From: Dominik Koch Date: Wed, 16 Jul 2025 15:19:06 +0200 Subject: [PATCH 01/20] feat(hero): add sponsored by vercel to hero --- apps/web/src/components/icons.tsx | 8 ++++ apps/web/src/components/landing/hero.tsx | 14 +++++++ apps/web/src/components/ui/sponsor-button.tsx | 42 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 apps/web/src/components/ui/sponsor-button.tsx diff --git a/apps/web/src/components/icons.tsx b/apps/web/src/components/icons.tsx index 4f3986e3..68e8f0ff 100644 --- a/apps/web/src/components/icons.tsx +++ b/apps/web/src/components/icons.tsx @@ -38,6 +38,14 @@ export function GithubIcon({ className }: { className?: string }) { ); } +export function VercelIcon({ className }: { className?: string }) { + return ( + + + + ); +} + export function BackgroundIcon({ className }: { className?: string }) { return ( + + + ; + companyName: string; + className?: string; +} + +export function SponsorButton({ + href, + logo: Logo, + companyName, + className = "", +}: SponsorButtonProps) { + + return ( + + + Sponsored by + + +
+
+ +
+ + + {companyName} + +
+ + ); +} From 5483db83de2c8d3a051148dfc56d6ab52335ee73 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 15:52:25 +0200 Subject: [PATCH 02/20] fix keyboard shortcuts --- .../components/keyboard-shortcuts-help.tsx | 24 +++++-------------- apps/web/src/components/ui/dialog.tsx | 12 ++++++++-- apps/web/src/hooks/use-keyboard-shortcuts.ts | 16 +------------ 3 files changed, 17 insertions(+), 35 deletions(-) diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx index 9f0d1e57..95fb6031 100644 --- a/apps/web/src/components/keyboard-shortcuts-help.tsx +++ b/apps/web/src/components/keyboard-shortcuts-help.tsx @@ -21,7 +21,7 @@ const KeyBadge = ({ keyName }: { keyName: string }) => { .replace("Shift", "⇧") .replace("←", "◀") .replace("→", "▶") - .replace("Space", "⎵"); + .replace("Space", "Space"); return ( @@ -31,7 +31,7 @@ const KeyBadge = ({ keyName }: { keyName: string }) => { }; const ShortcutItem = ({ shortcut }: { shortcut: any }) => ( -
+
{shortcut.icon && (
{shortcut.icon}
@@ -67,7 +67,7 @@ export const KeyboardShortcutsHelp = () => { Shortcuts - + @@ -81,11 +81,11 @@ export const KeyboardShortcutsHelp = () => {
{categories.map((category) => ( -
-

+
+

{category}

-
+
{shortcuts .filter((shortcut) => shortcut.category === category) .map((shortcut, index) => ( @@ -95,18 +95,6 @@ export const KeyboardShortcutsHelp = () => {
))}
- -
-

Tips:

-
    -
  • - • Shortcuts work when the editor is focused (not typing in inputs) -
  • -
  • • J/K/L are industry-standard video editing shortcuts
  • -
  • • Use arrow keys for frame-perfect positioning
  • -
  • • Hold Shift with arrow keys for larger jumps
  • -
-
); diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx index 0a487d0a..ebfd8907 100644 --- a/apps/web/src/components/ui/dialog.tsx +++ b/apps/web/src/components/ui/dialog.tsx @@ -22,7 +22,7 @@ const DialogOverlay = React.forwardRef< { + e.stopPropagation(); + e.preventDefault(); + }} + onOpenAutoFocus={(e) => { + e.stopPropagation(); + e.preventDefault(); + }} {...props} > diff --git a/apps/web/src/hooks/use-keyboard-shortcuts.ts b/apps/web/src/hooks/use-keyboard-shortcuts.ts index 86190c94..d8fc032e 100644 --- a/apps/web/src/hooks/use-keyboard-shortcuts.ts +++ b/apps/web/src/hooks/use-keyboard-shortcuts.ts @@ -15,6 +15,7 @@ export interface KeyboardShortcut { enabled?: boolean; requiresSelection?: boolean; icon?: React.ReactNode; + displayKeys?: string[]; // Keys to show in help (defaults to keys if not provided) } interface UseKeyboardShortcutsOptions { @@ -66,7 +67,6 @@ export const useKeyboardShortcuts = ( category: "Playback", action: () => { toggle(); - toast.info(isPlaying ? "Paused" : "Playing", { duration: 1000 }); }, }, { @@ -76,7 +76,6 @@ export const useKeyboardShortcuts = ( category: "Playback", action: () => { seek(Math.max(0, currentTime - 1)); - toast.info("Rewind 1s", { duration: 1000 }); }, }, { @@ -86,7 +85,6 @@ export const useKeyboardShortcuts = ( category: "Playback", action: () => { toggle(); - toast.info(isPlaying ? "Paused" : "Playing", { duration: 1000 }); }, }, { @@ -96,7 +94,6 @@ export const useKeyboardShortcuts = ( category: "Playback", action: () => { seek(Math.min(duration, currentTime + 1)); - toast.info("Forward 1s", { duration: 1000 }); }, }, @@ -146,7 +143,6 @@ export const useKeyboardShortcuts = ( category: "Navigation", action: () => { seek(0); - toast.info("Start of timeline", { duration: 1000 }); }, }, { @@ -156,7 +152,6 @@ export const useKeyboardShortcuts = ( category: "Navigation", action: () => { seek(duration); - toast.info("End of timeline", { duration: 1000 }); }, }, @@ -185,7 +180,6 @@ export const useKeyboardShortcuts = ( if (currentTime > effectiveStart && currentTime < effectiveEnd) { splitElement(trackId, elementId, currentTime); - toast.success("Element split at playhead"); } else { toast.error("Playhead must be within selected element"); } @@ -218,9 +212,6 @@ export const useKeyboardShortcuts = ( category: "Editing", action: () => { toggleSnapping(); - toast.info(`Snapping ${snappingEnabled ? "disabled" : "enabled"}`, { - duration: 1000, - }); }, }, @@ -238,9 +229,6 @@ export const useKeyboardShortcuts = ( })) ); setSelectedElements(allElements); - toast.info(`Selected ${allElements.length} elements`, { - duration: 1000, - }); }, }, { @@ -270,8 +258,6 @@ export const useKeyboardShortcuts = ( ...elementWithoutId, startTime: newStartTime, }); - - toast.success("Element duplicated"); } }, }, From 0c086e3c240e229b3dd89e6925b34f8c5fc20052 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 16:05:30 +0200 Subject: [PATCH 03/20] more shortcut fixes --- .../components/keyboard-shortcuts-help.tsx | 76 +++++++++++++------ apps/web/src/hooks/use-keyboard-shortcuts.ts | 1 - 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx index 95fb6031..936254be 100644 --- a/apps/web/src/components/keyboard-shortcuts-help.tsx +++ b/apps/web/src/components/keyboard-shortcuts-help.tsx @@ -15,41 +15,73 @@ import { Keyboard } from "lucide-react"; import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts"; const KeyBadge = ({ keyName }: { keyName: string }) => { - // Replace common key names with symbols + // Replace common key names with symbols or friendly names const displayKey = keyName .replace("Cmd", "⌘") - .replace("Shift", "⇧") + .replace("Shift", "Shift") + .replace("ArrowLeft", "Arrow Left") + .replace("ArrowRight", "Arrow Right") + .replace("ArrowUp", "Arrow Up") + .replace("ArrowDown", "Arrow Down") .replace("←", "◀") .replace("→", "▶") .replace("Space", "Space"); return ( - + {displayKey} ); }; -const ShortcutItem = ({ shortcut }: { shortcut: any }) => ( -
-
- {shortcut.icon && ( -
{shortcut.icon}
- )} - {shortcut.description} +const ShortcutItem = ({ shortcut }: { shortcut: any }) => { + // Filter out lowercase duplicates for display - if both "j" and "J" exist, only show "J" + const displayKeys = shortcut.keys.filter((key: string) => { + const lowerKey = key.toLowerCase(); + const upperKey = key.toUpperCase(); + + // If this is a lowercase letter and the uppercase version exists, skip it + if ( + key === lowerKey && + key !== upperKey && + shortcut.keys.includes(upperKey) + ) { + return false; + } + + return true; + }); + + return ( +
+
+ {shortcut.icon && ( +
{shortcut.icon}
+ )} + {shortcut.description} +
+
+ {displayKeys.map((key: string, index: number) => ( +
+
+ {key.split("+").map((keyPart: string, partIndex: number) => ( +
+ + {partIndex < key.split("+").length - 1 && ( + + + )} +
+ ))} +
+ {index < displayKeys.length - 1 && ( + or + )} +
+ ))} +
-
- {shortcut.keys.map((key: string, index: number) => ( -
- - {index < shortcut.keys.length - 1 && ( - + - )} -
- ))} -
-
-); + ); +}; export const KeyboardShortcutsHelp = () => { const [open, setOpen] = useState(false); diff --git a/apps/web/src/hooks/use-keyboard-shortcuts.ts b/apps/web/src/hooks/use-keyboard-shortcuts.ts index d8fc032e..84cb78f8 100644 --- a/apps/web/src/hooks/use-keyboard-shortcuts.ts +++ b/apps/web/src/hooks/use-keyboard-shortcuts.ts @@ -15,7 +15,6 @@ export interface KeyboardShortcut { enabled?: boolean; requiresSelection?: boolean; icon?: React.ReactNode; - displayKeys?: string[]; // Keys to show in help (defaults to keys if not provided) } interface UseKeyboardShortcutsOptions { From c3de5327091cb84d8233c25b2e7eeacc2c972a7c Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 16:14:45 +0200 Subject: [PATCH 04/20] fix --- apps/web/src/components/landing/hero.tsx | 2 +- apps/web/src/components/ui/sponsor-button.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index 4a66e433..d5fe38d1 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -119,7 +119,7 @@ export function Hero() { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.6, duration: 0.8 }} - className="mb-4 flex justify-center" + className="mb-8 flex justify-center" > Sponsored by From fcfe612cd66c6c38c4ea27513dbcb9a70bd89c92 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 17:36:47 +0200 Subject: [PATCH 05/20] fix: handlers component on mobile --- .../web/src/components/landing/handlebars.tsx | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx index 36705e90..e2eb2573 100644 --- a/apps/web/src/components/landing/handlebars.tsx +++ b/apps/web/src/components/landing/handlebars.tsx @@ -19,6 +19,7 @@ export function Handlebars({ const [leftHandle, setLeftHandle] = useState(0); const [rightHandle, setRightHandle] = useState(maxWidth); const [contentWidth, setContentWidth] = useState(maxWidth); + const [isDragging, setIsDragging] = useState(false); const leftHandleX = useMotionValue(0); const rightHandleX = useMotionValue(maxWidth); @@ -33,6 +34,27 @@ export function Handlebars({ const containerRef = useRef(null); const measureRef = useRef(null); + // Prevent scroll when dragging on mobile + useEffect(() => { + const preventDefault = (e: TouchEvent) => { + if (isDragging) { + e.preventDefault(); + } + }; + + if (isDragging) { + document.addEventListener("touchmove", preventDefault, { + passive: false, + }); + document.body.style.overflow = "hidden"; + } + + return () => { + document.removeEventListener("touchmove", preventDefault); + document.body.style.overflow = ""; + }; + }, [isDragging]); + useEffect(() => { if (!measureRef.current) return; @@ -80,6 +102,14 @@ export function Handlebars({ setRightHandle(newRight); }; + const handleDragStart = () => { + setIsDragging(true); + }; + + const handleDragEnd = () => { + setIsDragging(false); + }; + return (
+ {/* Left Handle */}
+ {/* Right Handle */}
@@ -161,4 +201,4 @@ export function Handlebars({
); -}; +} From c1c4b114eb583489afdf1e49cf96197fbb10bca2 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 17:38:29 +0200 Subject: [PATCH 06/20] fix: snapping for both directions (left/right) --- .../src/components/editor/timeline-track.tsx | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index 2be143eb..9b94ee6d 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -54,7 +54,7 @@ export function TimelineTrackContent({ const { currentTime } = usePlaybackStore(); // Initialize snapping hook - const { snapElementPosition } = useTimelineSnapping({ + const { snapElementPosition, snapElementEdge } = useTimelineSnapping({ snapThreshold: 10, enableElementSnapping: snappingEnabled, enablePlayheadSnapping: snappingEnabled, @@ -103,15 +103,52 @@ export function TimelineTrackContent({ let finalTime = adjustedTime; let snapPoint = null; if (snappingEnabled) { - const snapResult = snapElementPosition( + // Find the element being dragged to get its duration + let elementDuration = 5; // fallback duration + if (dragState.elementId && dragState.trackId) { + const sourceTrack = tracks.find((t) => t.id === dragState.trackId); + const element = sourceTrack?.elements.find( + (e) => e.id === dragState.elementId + ); + if (element) { + elementDuration = + element.duration - element.trimStart - element.trimEnd; + } + } + + // Try snapping both start and end edges + const startSnapResult = snapElementEdge( adjustedTime, + elementDuration, tracks, currentTime, zoomLevel, - dragState.elementId || undefined + dragState.elementId || undefined, + true // snap to start edge ); - finalTime = snapResult.snappedTime; - snapPoint = snapResult.snapPoint; + + const endSnapResult = snapElementEdge( + adjustedTime, + elementDuration, + tracks, + currentTime, + zoomLevel, + dragState.elementId || undefined, + false // snap to end edge + ); + + // Choose the snap result with the smaller distance (closer snap) + let bestSnapResult = startSnapResult; + if ( + endSnapResult.snapPoint && + (!startSnapResult.snapPoint || + endSnapResult.snapDistance < startSnapResult.snapDistance) + ) { + bestSnapResult = endSnapResult; + } + + finalTime = bestSnapResult.snappedTime; + snapPoint = bestSnapResult.snapPoint; // Notify parent component about snap point change onSnapPointChange?.(snapPoint); From 218625b61a344e371688bbffc152ec3632615e46 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 17:44:44 +0200 Subject: [PATCH 07/20] feat: advance snapping --- .../src/components/editor/timeline-track.tsx | 119 +++++++++++++----- 1 file changed, 91 insertions(+), 28 deletions(-) diff --git a/apps/web/src/components/editor/timeline-track.tsx b/apps/web/src/components/editor/timeline-track.tsx index 9b94ee6d..91b3ffa3 100644 --- a/apps/web/src/components/editor/timeline-track.tsx +++ b/apps/web/src/components/editor/timeline-track.tsx @@ -60,6 +60,53 @@ export function TimelineTrackContent({ enablePlayheadSnapping: snappingEnabled, }); + // Helper function for drop snapping that tries both edges + const getDropSnappedTime = ( + dropTime: number, + elementDuration: number, + excludeElementId?: string + ) => { + if (!snappingEnabled) { + // Use frame snapping if project has FPS, otherwise use decimal snapping + const projectStore = useProjectStore.getState(); + const projectFps = projectStore.activeProject?.fps || 30; + return snapTimeToFrame(dropTime, projectFps); + } + + // Try snapping both start and end edges for drops + const startSnapResult = snapElementEdge( + dropTime, + elementDuration, + tracks, + currentTime, + zoomLevel, + excludeElementId, + true // snap to start edge + ); + + const endSnapResult = snapElementEdge( + dropTime, + elementDuration, + tracks, + currentTime, + zoomLevel, + excludeElementId, + false // snap to end edge + ); + + // Choose the snap result with the smaller distance (closer snap) + let bestSnapResult = startSnapResult; + if ( + endSnapResult.snapPoint && + (!startSnapResult.snapPoint || + endSnapResult.snapDistance < startSnapResult.snapDistance) + ) { + bestSnapResult = endSnapResult; + } + + return bestSnapResult.snappedTime; + }; + const timelineRef = useRef(null); const [isDropping, setIsDropping] = useState(false); const [dropPosition, setDropPosition] = useState(null); @@ -427,9 +474,10 @@ export function TimelineTrackContent({ if (dragData.type === "text") { // Text elements have default duration of 5 seconds const newElementDuration = 5; - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - const snappedTime = snapTimeToFrame(dropTime, projectFps); + const snappedTime = getDropSnappedTime( + dropTime, + newElementDuration + ); const newElementEnd = snappedTime + newElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -448,9 +496,10 @@ export function TimelineTrackContent({ ); if (mediaItem) { const newElementDuration = mediaItem.duration || 5; - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - const snappedTime = snapTimeToFrame(dropTime, projectFps); + const snappedTime = getDropSnappedTime( + dropTime, + newElementDuration + ); const newElementEnd = snappedTime + newElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -490,9 +539,11 @@ export function TimelineTrackContent({ movingElement.duration - movingElement.trimStart - movingElement.trimEnd; - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - const snappedTime = snapTimeToFrame(dropTime, projectFps); + const snappedTime = getDropSnappedTime( + dropTime, + movingElementDuration, + elementId + ); const movingElementEnd = snappedTime + movingElementDuration; wouldOverlap = track.elements.some((existingElement) => { @@ -519,17 +570,15 @@ export function TimelineTrackContent({ if (wouldOverlap) { e.dataTransfer.dropEffect = "none"; setWouldOverlap(true); - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - setDropPosition(snapTimeToFrame(dropTime, projectFps)); + // Use default duration for position indicator + setDropPosition(getDropSnappedTime(dropTime, 5)); return; } e.dataTransfer.dropEffect = hasTimelineElement ? "move" : "copy"; setWouldOverlap(false); - const projectStore = useProjectStore.getState(); - const projectFps = projectStore.activeProject?.fps || 30; - setDropPosition(snapTimeToFrame(dropTime, projectFps)); + // Use default duration for position indicator + setDropPosition(getDropSnappedTime(dropTime, 5)); }; const handleTrackDragEnter = (e: React.DragEvent) => { @@ -651,18 +700,20 @@ export function TimelineTrackContent({ return; } - // Adjust position based on where user clicked on the element - const adjustedStartTime = snappedTime - clickOffsetTime; - const finalStartTime = Math.max( - 0, - snapTimeToFrame(adjustedStartTime, projectFps) - ); - // Check for overlaps with existing elements (excluding the moving element itself) const movingElementDuration = movingElement.duration - movingElement.trimStart - movingElement.trimEnd; + + // Adjust position based on where user clicked on the element + const adjustedStartTime = newStartTime - clickOffsetTime; + const snappedStartTime = getDropSnappedTime( + adjustedStartTime, + movingElementDuration, + elementId + ); + const finalStartTime = Math.max(0, snappedStartTime); const movingElementEnd = finalStartTime + movingElementDuration; const hasOverlap = track.elements.some((existingElement) => { @@ -748,7 +799,11 @@ export function TimelineTrackContent({ // Check for overlaps with existing elements in target track const newElementDuration = 5; // Default text duration - const newElementEnd = snappedTime + newElementDuration; + const textSnappedTime = getDropSnappedTime( + newStartTime, + newElementDuration + ); + const newElementEnd = textSnappedTime + newElementDuration; const hasOverlap = targetTrack.elements.some((existingElement) => { const existingStart = existingElement.startTime; @@ -759,7 +814,9 @@ export function TimelineTrackContent({ existingElement.trimEnd); // Check if elements overlap - return snappedTime < existingEnd && newElementEnd > existingStart; + return ( + textSnappedTime < existingEnd && newElementEnd > existingStart + ); }); if (hasOverlap) { @@ -774,7 +831,7 @@ export function TimelineTrackContent({ name: dragData.name || "Text", content: dragData.content || "Default Text", duration: TIMELINE_CONSTANTS.DEFAULT_TEXT_DURATION, - startTime: snappedTime, + startTime: textSnappedTime, trimStart: 0, trimEnd: 0, fontSize: 48, @@ -894,7 +951,11 @@ export function TimelineTrackContent({ // Check for overlaps with existing elements in target track const newElementDuration = mediaItem.duration || 5; - const newElementEnd = snappedTime + newElementDuration; + const mediaSnappedTime = getDropSnappedTime( + newStartTime, + newElementDuration + ); + const newElementEnd = mediaSnappedTime + newElementDuration; const hasOverlap = targetTrack.elements.some((existingElement) => { const existingStart = existingElement.startTime; @@ -905,7 +966,9 @@ export function TimelineTrackContent({ existingElement.trimEnd); // Check if elements overlap - return snappedTime < existingEnd && newElementEnd > existingStart; + return ( + mediaSnappedTime < existingEnd && newElementEnd > existingStart + ); }); if (hasOverlap) { @@ -920,7 +983,7 @@ export function TimelineTrackContent({ mediaId: mediaItem.id, name: mediaItem.name, duration: mediaItem.duration || 5, - startTime: snappedTime, + startTime: mediaSnappedTime, trimStart: 0, trimEnd: 0, }); From e419eba29bb2c6092a8c3d3866fbe46ee2937b23 Mon Sep 17 00:00:00 2001 From: Zaid-maker <53424436+Zaid-maker@users.noreply.github.com> Date: Wed, 16 Jul 2025 21:34:03 +0500 Subject: [PATCH 08/20] fix build by passing correct env --- .github/workflows/bun-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/bun-ci.yml b/.github/workflows/bun-ci.yml index dbcf6f9c..1bd00ca5 100644 --- a/.github/workflows/bun-ci.yml +++ b/.github/workflows/bun-ci.yml @@ -23,6 +23,8 @@ jobs: env: DATABASE_URL: "postgresql://opencut:opencutthegoat@localhost:5432/opencut" + BETTER_AUTH_SECRET: "supersecret" + BETTER_AUTH_URL: "http://localhost:3000" steps: - name: Checkout repository From caf6fef3adb0425e3a89ca318136adc3e5f5d17c Mon Sep 17 00:00:00 2001 From: Sahil Sobhani Date: Wed, 16 Jul 2025 23:34:20 +0530 Subject: [PATCH 09/20] Redesigned the footer for mobile devices: UI Changes --- apps/web/.env.example | 15 --------------- apps/web/src/components/footer.tsx | 8 ++++---- 2 files changed, 4 insertions(+), 19 deletions(-) delete mode 100644 apps/web/.env.example diff --git a/apps/web/.env.example b/apps/web/.env.example deleted file mode 100644 index d63df851..00000000 --- a/apps/web/.env.example +++ /dev/null @@ -1,15 +0,0 @@ -# Environment Variables Example -# Copy this file to .env.local and update the values as needed - -DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut" - -# Better Auth -NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000 -BETTER_AUTH_SECRET=your-secret-key-here - -# Development Environment -NODE_ENV=development - -# Redis -UPSTASH_REDIS_REST_URL=http://localhost:8079 -UPSTASH_REDIS_REST_TOKEN=example_token diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx index 81505d4a..731aa2f7 100644 --- a/apps/web/src/components/footer.tsx +++ b/apps/web/src/components/footer.tsx @@ -34,15 +34,15 @@ export function Footer() {
{/* Brand Section */}
-
+
OpenCut OpenCut
-

+

The open source video editor that gets the job done. Simple, powerful, and works on any platform.

-
+
-
+

Resources

    From 04a10367701a804a4421051b1ff39ef5e09479c2 Mon Sep 17 00:00:00 2001 From: Sahil Sobhani Date: Wed, 16 Jul 2025 23:35:25 +0530 Subject: [PATCH 10/20] added .env.example --- apps/web/.env.example | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 apps/web/.env.example diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 00000000..d63df851 --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1,15 @@ +# Environment Variables Example +# Copy this file to .env.local and update the values as needed + +DATABASE_URL="postgresql://opencut:opencutthegoat@localhost:5432/opencut" + +# Better Auth +NEXT_PUBLIC_BETTER_AUTH_URL=http://localhost:3000 +BETTER_AUTH_SECRET=your-secret-key-here + +# Development Environment +NODE_ENV=development + +# Redis +UPSTASH_REDIS_REST_URL=http://localhost:8079 +UPSTASH_REDIS_REST_TOKEN=example_token From 87dd275e573958af32412147378b73dcf988ed18 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 21:05:28 +0200 Subject: [PATCH 11/20] fix: no more rounded corners in preview --- apps/web/src/components/editor/preview-panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 894b6257..2ecee391 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -347,7 +347,7 @@ export function PreviewPanel() { {hasAnyElements ? (
    Date: Thu, 17 Jul 2025 00:28:35 +0500 Subject: [PATCH 12/20] fix typo --- .github/workflows/bun-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bun-ci.yml b/.github/workflows/bun-ci.yml index 1bd00ca5..29a202b8 100644 --- a/.github/workflows/bun-ci.yml +++ b/.github/workflows/bun-ci.yml @@ -24,7 +24,7 @@ jobs: env: DATABASE_URL: "postgresql://opencut:opencutthegoat@localhost:5432/opencut" BETTER_AUTH_SECRET: "supersecret" - BETTER_AUTH_URL: "http://localhost:3000" + NEXT_PUBLIC_BETTER_AUTH_URL: "http://localhost:3000" steps: - name: Checkout repository From 2b618b189cca5830f314d9a773648e1c59c3c20e Mon Sep 17 00:00:00 2001 From: Zaid-maker <53424436+Zaid-maker@users.noreply.github.com> Date: Thu, 17 Jul 2025 01:10:49 +0500 Subject: [PATCH 13/20] more --- .github/workflows/bun-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/bun-ci.yml b/.github/workflows/bun-ci.yml index 29a202b8..f01b9878 100644 --- a/.github/workflows/bun-ci.yml +++ b/.github/workflows/bun-ci.yml @@ -25,6 +25,8 @@ jobs: DATABASE_URL: "postgresql://opencut:opencutthegoat@localhost:5432/opencut" BETTER_AUTH_SECRET: "supersecret" NEXT_PUBLIC_BETTER_AUTH_URL: "http://localhost:3000" + UPSTASH_REDIS_REST_URL: "https://your-upstash-redis-url" + UPSTASH_REDIS_REST_TOKEN: "your-upstash-redis-token" steps: - name: Checkout repository From 4851c38e74ac93839a116c53687c48ef7ef52b54 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 22:39:33 +0200 Subject: [PATCH 14/20] style(footer): align everything to left --- apps/web/src/components/footer.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/footer.tsx b/apps/web/src/components/footer.tsx index 731aa2f7..5b7f2303 100644 --- a/apps/web/src/components/footer.tsx +++ b/apps/web/src/components/footer.tsx @@ -34,15 +34,15 @@ export function Footer() {
    {/* Brand Section */}
    -
    +
    OpenCut OpenCut
    -

    +

    The open source video editor that gets the job done. Simple, powerful, and works on any platform.

    -
    +
    -
    +

    Resources

      @@ -129,7 +129,7 @@ export function Footer() {
    {/* Bottom Section */} -
    +
    © 2025 OpenCut, All Rights Reserved
    From accad442f48f9d1254d889054dfbce81ea6acda0 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 22:47:17 +0200 Subject: [PATCH 15/20] cleanup --- apps/web/src/app/editor/[project_id]/page.tsx | 11 +++++-- apps/web/src/app/projects/page.tsx | 12 ++++++-- apps/web/src/components/onboarding.tsx | 29 +++++++++++++++++++ apps/web/src/middleware.ts | 4 ++- apps/web/src/stores/project-store.ts | 11 +++++++ 5 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/components/onboarding.tsx diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index 65667106..41bef8bd 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { ResizablePanelGroup, @@ -16,6 +16,7 @@ import { usePanelStore } from "@/stores/panel-store"; import { useProjectStore } from "@/stores/project-store"; import { EditorProvider } from "@/components/editor-provider"; import { usePlaybackControls } from "@/hooks/use-playback-controls"; +import { Onboarding } from "@/components/onboarding"; export default function Editor() { const { @@ -36,12 +37,12 @@ export default function Editor() { const router = useRouter(); const projectId = params.project_id as string; const handledProjectIds = useRef>(new Set()); + const [isOnboardingOpen, setIsOnboardingOpen] = useState(true); usePlaybackControls(); useEffect(() => { const initProject = async () => { - if (!projectId) return; if (activeProject?.id === projectId) { @@ -139,6 +140,12 @@ export default function Editor() {
    + { + setIsOnboardingOpen(false); + }} + /> ); } diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx index 499b999a..9fcb025b 100644 --- a/apps/web/src/app/projects/page.tsx +++ b/apps/web/src/app/projects/page.tsx @@ -1,7 +1,8 @@ -"use client"; +"use client" +import { redirect } from "next/navigation"; import Link from "next/link"; -import { useState } from "react"; +import React, { useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; @@ -28,8 +29,15 @@ import { useProjectStore } from "@/stores/project-store"; import { useRouter } from "next/navigation"; import { DeleteProjectDialog } from "@/components/delete-project-dialog"; import { RenameProjectDialog } from "@/components/rename-project-dialog"; +import { toast } from "sonner"; export default function ProjectsPage() { + + if (process.env.NODE_ENV !== "development") { + toast.error("You are not allowed to access this page"); + redirect("/"); + } + const { createNewProject, savedProjects, diff --git a/apps/web/src/components/onboarding.tsx b/apps/web/src/components/onboarding.tsx new file mode 100644 index 00000000..a2943677 --- /dev/null +++ b/apps/web/src/components/onboarding.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "./ui/dialog"; + +interface OnboardingProps { + isOpen: boolean; + onClose: () => void; +} + +export function Onboarding({ isOpen, onClose }: OnboardingProps) { + return ( + + + + Welcome to Clipture + + Let's get you started with the basics. + + + + + ); +} diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index 1f229e6d..aabc302a 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -3,6 +3,8 @@ import type { NextRequest } from "next/server"; import { env } from "./env"; export async function middleware(request: NextRequest) { + const protectedPaths = ["/editor", "/projects"]; + // Handle fuckcapcut.com domain redirect if (request.headers.get("host") === "fuckcapcut.com") { return NextResponse.redirect("https://opencut.app/why-not-capcut", 301); @@ -10,7 +12,7 @@ export async function middleware(request: NextRequest) { const path = request.nextUrl.pathname; - if (path.startsWith("/editor") && env.NODE_ENV === "production") { + if (protectedPaths.includes(path) && env.NODE_ENV === "production") { const homeUrl = new URL("/", request.url); homeUrl.searchParams.set("redirect", request.url); return NextResponse.redirect(homeUrl); diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index efb9cc71..5017be2d 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -5,6 +5,7 @@ import { toast } from "sonner"; import { useMediaStore } from "./media-store"; import { useTimelineStore } from "./timeline-store"; import { generateUUID } from "@/lib/utils"; +import { env } from "@/env"; interface ProjectStore { activeProject: TProject | null; @@ -36,6 +37,16 @@ export const useProjectStore = create((set, get) => ({ isInitialized: false, createNewProject: async (name: string) => { + try { + if (process.env.NODE_ENV !== "development") { + toast.error("Project creation is disabled outside development environment"); + throw new Error("Not allowed in production"); + } + } catch (error) { + toast.error("Failed to create new project"); + throw error; + } + const newProject: TProject = { id: generateUUID(), name, From 7d1f99c20ca60933984fe08ef83601248c6e0765 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 22:49:10 +0200 Subject: [PATCH 16/20] fix --- apps/web/src/app/projects/page.tsx | 12 ++---------- apps/web/src/stores/project-store.ts | 11 ----------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx index 9fcb025b..499b999a 100644 --- a/apps/web/src/app/projects/page.tsx +++ b/apps/web/src/app/projects/page.tsx @@ -1,8 +1,7 @@ -"use client" -import { redirect } from "next/navigation"; +"use client"; import Link from "next/link"; -import React, { useState } from "react"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; @@ -29,15 +28,8 @@ import { useProjectStore } from "@/stores/project-store"; import { useRouter } from "next/navigation"; import { DeleteProjectDialog } from "@/components/delete-project-dialog"; import { RenameProjectDialog } from "@/components/rename-project-dialog"; -import { toast } from "sonner"; export default function ProjectsPage() { - - if (process.env.NODE_ENV !== "development") { - toast.error("You are not allowed to access this page"); - redirect("/"); - } - const { createNewProject, savedProjects, diff --git a/apps/web/src/stores/project-store.ts b/apps/web/src/stores/project-store.ts index 5017be2d..efb9cc71 100644 --- a/apps/web/src/stores/project-store.ts +++ b/apps/web/src/stores/project-store.ts @@ -5,7 +5,6 @@ import { toast } from "sonner"; import { useMediaStore } from "./media-store"; import { useTimelineStore } from "./timeline-store"; import { generateUUID } from "@/lib/utils"; -import { env } from "@/env"; interface ProjectStore { activeProject: TProject | null; @@ -37,16 +36,6 @@ export const useProjectStore = create((set, get) => ({ isInitialized: false, createNewProject: async (name: string) => { - try { - if (process.env.NODE_ENV !== "development") { - toast.error("Project creation is disabled outside development environment"); - throw new Error("Not allowed in production"); - } - } catch (error) { - toast.error("Failed to create new project"); - throw error; - } - const newProject: TProject = { id: generateUUID(), name, From 36516b874f86daee8dea5b559de4fda2a4c602fa Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 22:50:40 +0200 Subject: [PATCH 17/20] remove onboarding --- apps/web/src/app/editor/[project_id]/page.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx index 41bef8bd..aa5b7315 100644 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ b/apps/web/src/app/editor/[project_id]/page.tsx @@ -140,12 +140,6 @@ export default function Editor() {
    - { - setIsOnboardingOpen(false); - }} - /> ); } From a3c84e4a199add132340fb4f0a5e74e9062d5d5f Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 16 Jul 2025 23:28:26 +0200 Subject: [PATCH 18/20] fix handlebars --- .../web/src/components/landing/handlebars.tsx | 48 ++----------------- 1 file changed, 4 insertions(+), 44 deletions(-) diff --git a/apps/web/src/components/landing/handlebars.tsx b/apps/web/src/components/landing/handlebars.tsx index e2eb2573..36705e90 100644 --- a/apps/web/src/components/landing/handlebars.tsx +++ b/apps/web/src/components/landing/handlebars.tsx @@ -19,7 +19,6 @@ export function Handlebars({ const [leftHandle, setLeftHandle] = useState(0); const [rightHandle, setRightHandle] = useState(maxWidth); const [contentWidth, setContentWidth] = useState(maxWidth); - const [isDragging, setIsDragging] = useState(false); const leftHandleX = useMotionValue(0); const rightHandleX = useMotionValue(maxWidth); @@ -34,27 +33,6 @@ export function Handlebars({ const containerRef = useRef(null); const measureRef = useRef(null); - // Prevent scroll when dragging on mobile - useEffect(() => { - const preventDefault = (e: TouchEvent) => { - if (isDragging) { - e.preventDefault(); - } - }; - - if (isDragging) { - document.addEventListener("touchmove", preventDefault, { - passive: false, - }); - document.body.style.overflow = "hidden"; - } - - return () => { - document.removeEventListener("touchmove", preventDefault); - document.body.style.overflow = ""; - }; - }, [isDragging]); - useEffect(() => { if (!measureRef.current) return; @@ -102,14 +80,6 @@ export function Handlebars({ setRightHandle(newRight); }; - const handleDragStart = () => { - setIsDragging(true); - }; - - const handleDragEnd = () => { - setIsDragging(false); - }; - return (
    - {/* Left Handle */}
    - {/* Right Handle */}
    @@ -201,4 +161,4 @@ export function Handlebars({
    ); -} +}; From 51857f1709e9aaf16efe7e9a12c1d2cf0bcc82c0 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Thu, 17 Jul 2025 11:50:28 +0200 Subject: [PATCH 19/20] docs: what to focus on section --- .github/CONTRIBUTING.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 995dbf19..77d7a6b6 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,6 +15,27 @@ Thank you for your interest in contributing to OpenCut! This document provides g > 1. Upgrade to a recent npm version (v9 or later), which has full workspace protocol support. > 2. Use an alternative package manager such as **bun** or **pnpm**. +## What to Focus On + +**🎯 Good Areas to Contribute:** +- Timeline functionality and UI improvements +- Project management features +- Performance optimizations +- Bug fixes in existing functionality +- UI/UX improvements +- Documentation and testing + +**⚠️ Areas to Avoid:** +- Preview panel enhancements (text fonts, stickers, effects) +- Export functionality improvements +- Preview rendering optimizations + +**Why?** We're currently planning a major refactor of the preview system. The current preview renders DOM elements (HTML), but we're moving to a binary rendering approach similar to CapCut. This new system will ensure consistency between preview and export, and provide much better performance and quality. + +The current HTML-based preview is essentially a prototype - the binary approach will be the "real deal." To avoid wasted effort, please focus on other areas of the application until this refactor is complete. + +If you're unsure whether your idea falls into the preview category, feel free to ask us [directly in discord](https://discord.gg/zmR9N35cjK) or create a GitHub issue! + ## Development Setup ### Prerequisites From 7d4cbca1190edc12ff9ae17029613b77b08436e0 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Thu, 17 Jul 2025 11:51:20 +0200 Subject: [PATCH 20/20] docs: README --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9ce027f9..ad9a6d38 100644 --- a/README.md +++ b/README.md @@ -129,11 +129,13 @@ The application will be available at [http://localhost:3000](http://localhost:30 ## Contributing -**Note**: We're currently moving at an extremely fast pace with rapid development and breaking changes. While we appreciate the interest, it's recommended to wait until the project stabilizes before contributing to avoid conflicts and wasted effort. +We welcome contributions! While we're actively developing and refactoring certain areas, there are plenty of opportunities to contribute effectively. -## Visit [CONTRIBUTING.md](.github/CONTRIBUTING.md) +**🎯 Focus areas:** Timeline functionality, project management, performance, bug fixes, and UI improvements outside the preview panel. -We welcome contributions! Please see our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instructions and development guidelines. +**⚠️ Avoid for now:** Preview panel enhancements (fonts, stickers, effects) and export functionality - we're refactoring these with a new binary rendering approach. + +See our [Contributing Guide](.github/CONTRIBUTING.md) for detailed setup instructions, development guidelines, and complete focus area guidance. **Quick start for contributors:**