summaryrefslogtreecommitdiff
path: root/apps/web/src/app
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-25 16:47:22 +0100
committerMaze Winther <mazewinther@gmail.com>2026-03-29 15:48:22 +0200
commit8db3bead13441a5882d8689dfc6a9f09c82375d9 (patch)
treee9d0cafa93904ffcbec3188c37961bcc16c6422d /apps/web/src/app
parent39ea298a9ccba3104c549c0ca079c231981811f5 (diff)
downloadopencut-8db3bead13441a5882d8689dfc6a9f09c82375d9.tar.gz
feat: masks, properties refactor, shaders, storage migrations, and more
Diffstat (limited to 'apps/web/src/app')
-rw-r--r--apps/web/src/app/blog/[slug]/page.tsx2
-rw-r--r--apps/web/src/app/blog/page.tsx2
-rw-r--r--apps/web/src/app/globals.css438
-rw-r--r--apps/web/src/app/layout.tsx19
-rw-r--r--apps/web/src/app/privacy/page.tsx121
-rw-r--r--apps/web/src/app/projects/page.tsx20
-rw-r--r--apps/web/src/app/projects/store.ts2
-rw-r--r--apps/web/src/app/terms/page.tsx100
8 files changed, 298 insertions, 406 deletions
diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx
index e1afc3a8..90afd919 100644
--- a/apps/web/src/app/blog/[slug]/page.tsx
+++ b/apps/web/src/app/blog/[slug]/page.tsx
@@ -5,7 +5,7 @@ import { BasePage } from "@/app/base-page";
import Prose from "@/components/ui/prose";
import { Separator } from "@/components/ui/separator";
import { getPosts, getSinglePost, processHtmlContent } from "@/lib/blog/query";
-import type { Author, Post } from "@/types/blog";
+import type { Author, Post } from "@/lib/blog/types";
type PageProps = {
params: Promise<{ slug: string }>;
diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx
index 257a8f23..df0b195d 100644
--- a/apps/web/src/app/blog/page.tsx
+++ b/apps/web/src/app/blog/page.tsx
@@ -3,7 +3,7 @@ import Link from "next/link";
import { BasePage } from "@/app/base-page";
import { Separator } from "@/components/ui/separator";
import { getPosts } from "@/lib/blog/query";
-import type { Post } from "@/types/blog";
+import type { Post } from "@/lib/blog/types";
export const metadata: Metadata = {
title: "Blog - OpenCut",
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index 6d3e984f..21219427 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -8,111 +8,111 @@
@plugin "tailwindcss-animate";
:root {
- --background: hsl(0, 0%, 100%);
- --foreground: hsl(0 0% 11%);
- --card: hsl(0, 0%, 100%);
- --card-foreground: hsl(0 0% 11%);
- --popover: hsl(0, 0%, 100%);
- --popover-hover: hsl(0, 0%, 96%);
- --popover-foreground: hsl(0 0% 2%);
- --primary: #009dff;
- --primary-foreground: hsl(0, 0%, 100%);
- --secondary: hsl(204, 100%, 97%);
- --secondary-border: hsl(204, 100%, 94%);
- --secondary-foreground: hsl(200, 98%, 39%);
- --muted: hsl(0 0% 85.1%);
- --muted-foreground: hsl(0 0% 50%);
- --accent: hsl(0, 0%, 96%);
- --accent-foreground: hsl(0 0% 2%);
- --destructive: hsl(0, 83%, 50%);
- --destructive-foreground: hsl(0, 0%, 100%);
- --constructive: hsl(141, 71%, 48%);
- --constructive-foreground: hsl(0, 0%, 100%);
- --border: hsl(0 0% 91%);
- --input: hsl(0, 0%, 100%);
- --ring: hsl(0, 0%, 55%);
- --chart-1: hsl(220 70% 50%);
- --chart-2: hsl(160 60% 45%);
- --chart-3: hsl(30 80% 55%);
- --chart-4: hsl(280 65% 60%);
- --chart-5: hsl(340 75% 55%);
- --sidebar-background: hsl(0 0% 96.1%);
- --sidebar-foreground: hsl(0 0% 2%);
- --sidebar-primary: hsl(0 0% 2%);
- --sidebar-primary-foreground: hsl(0 0% 91%);
- --sidebar-accent: hsl(0 0% 85.1%);
- --sidebar-accent-foreground: hsl(0 0% 2%);
- --sidebar-border: hsl(0 0% 85.1%);
- --sidebar-ring: hsl(0 0% 16.9%);
- --sidebar: hsl(0 0% 98%);
+ --background: hsl(0, 0%, 100%);
+ --foreground: hsl(0 0% 11%);
+ --card: hsl(0, 0%, 100%);
+ --card-foreground: hsl(0 0% 11%);
+ --popover: hsl(0, 0%, 100%);
+ --popover-hover: hsl(0, 0%, 96%);
+ --popover-foreground: hsl(0 0% 2%);
+ --primary: hsl(200, 90%, 52%);
+ --primary-foreground: hsl(0, 0%, 100%);
+ --secondary: hsl(204, 100%, 97%);
+ --secondary-border: hsl(204, 100%, 94%);
+ --secondary-foreground: hsl(200, 98%, 39%);
+ --muted: hsl(0 0% 85.1%);
+ --muted-foreground: hsl(0 0% 50%);
+ --accent: hsl(0, 0%, 96%);
+ --accent-foreground: hsl(0 0% 2%);
+ --destructive: hsl(0, 83%, 50%);
+ --destructive-foreground: hsl(0, 0%, 100%);
+ --constructive: hsl(141, 71%, 48%);
+ --constructive-foreground: hsl(0, 0%, 100%);
+ --border: hsl(0 0% 91%);
+ --input: hsl(0, 0%, 100%);
+ --ring: hsl(0, 0%, 55%);
+ --chart-1: hsl(220 70% 50%);
+ --chart-2: hsl(160 60% 45%);
+ --chart-3: hsl(30 80% 55%);
+ --chart-4: hsl(280 65% 60%);
+ --chart-5: hsl(340 75% 55%);
+ --sidebar-background: hsl(0 0% 96.1%);
+ --sidebar-foreground: hsl(0 0% 2%);
+ --sidebar-primary: hsl(0 0% 2%);
+ --sidebar-primary-foreground: hsl(0 0% 91%);
+ --sidebar-accent: hsl(0 0% 85.1%);
+ --sidebar-accent-foreground: hsl(0 0% 2%);
+ --sidebar-border: hsl(0 0% 85.1%);
+ --sidebar-ring: hsl(0 0% 16.9%);
+ --sidebar: hsl(0 0% 98%);
}
.panel {
- --background: hsl(216 13% 98%);
- --foreground: hsl(0 0% 13%);
- --card: hsl(0, 0%, 98%);
- --card-foreground: hsl(0 0% 13%);
- --primary-foreground: hsl(0, 0%, 98%);
- --secondary: hsl(204, 100%, 95%);
- --secondary-border: hsl(204, 100%, 92%);
- --secondary-foreground: hsl(200, 98%, 37%);
- --muted: hsl(0 0% 83.1%);
- --muted-foreground: hsl(0 0% 48%);
- --accent: hsl(0, 0%, 93%);
- --accent-foreground: hsl(0 0% 5%);
- --destructive-foreground: hsl(0, 0%, 98%);
- --constructive-foreground: hsl(0, 0%, 98%);
- --border: hsl(0 0% 89%);
- --input: hsl(0 0% 93%);
- --ring: hsl(0, 0%, 53%);
+ --background: hsl(210, 20%, 98%);
+ --foreground: hsl(0 0% 13%);
+ --card: hsl(0, 0%, 98%);
+ --card-foreground: hsl(0 0% 13%);
+ --primary-foreground: hsl(0, 0%, 98%);
+ --secondary: hsl(204, 100%, 95%);
+ --secondary-border: hsl(204, 100%, 92%);
+ --secondary-foreground: hsl(200, 98%, 37%);
+ --muted: hsl(0 0% 83.1%);
+ --muted-foreground: hsl(0 0% 48%);
+ --accent: hsl(0, 0%, 93%);
+ --accent-foreground: hsl(0 0% 5%);
+ --destructive-foreground: hsl(0, 0%, 98%);
+ --constructive-foreground: hsl(0, 0%, 98%);
+ --border: hsl(0 0% 87%);
+ --input: hsl(0 0% 93%);
+ --ring: hsl(0, 0%, 53%);
}
.dark {
- --background: hsl(0, 0%, 5%);
- --foreground: hsl(0 0% 87%);
- --card: hsl(0, 0%, 5%);
- --card-foreground: hsl(0 0% 87%);
- --popover: hsl(0, 0%, 5%);
- --popover-hover: hsl(0, 0%, 22%);
- --popover-foreground: hsl(0 0% 95%);
- --secondary: hsl(204, 100%, 12%);
- --secondary-border: hsl(204, 100%, 15%);
- --secondary-foreground: hsl(200, 98%, 61%);
- --muted: hsl(0 0% 20%);
- --accent: hsl(0, 0%, 14%);
- --accent-foreground: hsl(0 0% 95%);
- --border: hsl(0 0% 16%);
- --input: hsl(0 0% 5%);
- --ring: hsl(0, 0%, 50%);
- --sidebar-background: hsl(0 0% 8%);
- --sidebar-foreground: hsl(0 0% 95%);
- --sidebar-primary: hsl(0 0% 95%);
- --sidebar-primary-foreground: hsl(0 0% 15%);
- --sidebar-accent: hsl(0 0% 20%);
- --sidebar-accent-foreground: hsl(0 0% 95%);
- --sidebar-border: hsl(0 0% 20%);
- --sidebar-ring: hsl(0 0% 83.1%);
- --sidebar: hsl(0 0% 6%);
+ --background: hsl(0, 0%, 5%);
+ --foreground: hsl(0 0% 87%);
+ --card: hsl(0, 0%, 5%);
+ --card-foreground: hsl(0 0% 87%);
+ --popover: hsl(0, 0%, 5%);
+ --popover-hover: hsl(0, 0%, 13%);
+ --popover-foreground: hsl(0 0% 95%);
+ --secondary: hsl(204, 100%, 12%);
+ --secondary-border: hsl(204, 100%, 15%);
+ --secondary-foreground: hsl(200, 98%, 61%);
+ --muted: hsl(0 0% 20%);
+ --accent: hsl(0, 0%, 14%);
+ --accent-foreground: hsl(0 0% 95%);
+ --border: hsl(0 0% 16%);
+ --input: hsl(0 0% 5%);
+ --ring: hsl(0, 0%, 50%);
+ --sidebar-background: hsl(0 0% 8%);
+ --sidebar-foreground: hsl(0 0% 95%);
+ --sidebar-primary: hsl(0 0% 95%);
+ --sidebar-primary-foreground: hsl(0 0% 15%);
+ --sidebar-accent: hsl(0 0% 20%);
+ --sidebar-accent-foreground: hsl(0 0% 95%);
+ --sidebar-border: hsl(0 0% 20%);
+ --sidebar-ring: hsl(0 0% 83.1%);
+ --sidebar: hsl(0 0% 6%);
}
.dark .panel {
- --background: hsl(0 0% 10%);
- --foreground: hsl(0 0% 85%);
- --card: hsl(0, 0%, 10%);
- --card-foreground: hsl(0 0% 85%);
- --secondary: hsl(204, 67%, 9%);
- --secondary-border: hsl(204, 100%, 14%);
- --secondary-foreground: hsl(200, 98%, 63%);
- --muted: hsl(0 0% 22%);
- --accent: hsl(0, 0%, 15%);
- --accent-foreground: hsl(0 0% 93%);
- --border: hsl(0 0% 18%);
- --input: hsl(0 0% 22%);
- --ring: hsl(0, 0%, 52%);
+ --background: hsl(0 0% 10%);
+ --foreground: hsl(0 0% 85%);
+ --card: hsl(0, 0%, 10%);
+ --card-foreground: hsl(0 0% 85%);
+ --secondary: hsl(204, 67%, 9%);
+ --secondary-border: hsl(204, 100%, 14%);
+ --secondary-foreground: hsl(200, 98%, 63%);
+ --muted: hsl(0 0% 22%);
+ --accent: hsl(0, 0%, 15%);
+ --accent-foreground: hsl(0 0% 93%);
+ --border: hsl(0 0% 18%);
+ --input: hsl(0 0% 22%);
+ --ring: hsl(0, 0%, 52%);
}
@layer base {
- /*
+ /*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
@@ -120,165 +120,165 @@
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
- *,
- ::after,
- ::before,
- ::backdrop,
- ::file-selector-button {
- border-color: var(--color-gray-200, currentcolor);
- }
- /* Other default base styles */
- * {
- @apply border-border;
- }
- body {
- @apply bg-background text-foreground;
- /* Prevent back/forward swipe */
- overscroll-behavior-x: contain;
- }
- ::selection {
- @apply bg-primary/35 selection:text-primary-foreground;
- }
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentcolor);
+ }
+ /* Other default base styles */
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ /* Prevent back/forward swipe */
+ overscroll-behavior-x: contain;
+ }
+ ::selection {
+ @apply bg-primary/35 selection:text-primary-foreground;
+ }
}
@theme inline {
- /* Responsive breakpoints */
- --breakpoint-xs: 30rem;
+ /* Responsive breakpoints */
+ --breakpoint-xs: 30rem;
- /* Typography */
- --font-sans: var(--font-inter), sans-serif;
+ /* Typography */
+ --font-sans: var(--font-inter), sans-serif;
- /* Font sizes */
- --text-xs: 0.72rem;
- --text-sm: 0.79rem;
- --text-base: 0.92rem;
- --text-base--line-height: calc(1.5 / 0.95);
- --text-xs--line-height: calc(1 / 0.8);
+ /* Font sizes */
+ --text-xs: 0.72rem;
+ --text-sm: 0.79rem;
+ --text-base: 0.92rem;
+ --text-base--line-height: calc(1.5 / 0.95);
+ --text-xs--line-height: calc(1 / 0.8);
- /* Border radius */
- --radius-lg: 0.82rem;
- --radius-md: 0.65rem;
- --radius-sm: 0.35rem;
+ /* Border radius */
+ --radius-lg: 0.82rem;
+ --radius-md: 0.65rem;
+ --radius-sm: 0.35rem;
- /* Palette mapped to root design tokens */
- --color-background: var(--background);
- --color-foreground: var(--foreground);
+ /* Palette mapped to root design tokens */
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
- --color-card: var(--card);
- --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
- --color-popover: var(--popover);
- --color-popover-hover: var(--popover-hover);
- --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-popover-hover: var(--popover-hover);
+ --color-popover-foreground: var(--popover-foreground);
- --color-primary: var(--primary);
- --color-primary-foreground: var(--primary-foreground);
- --color-secondary: var(--secondary);
- --color-secondary-border: var(--secondary-border);
- --color-secondary-foreground: var(--secondary-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-border: var(--secondary-border);
+ --color-secondary-foreground: var(--secondary-foreground);
- --color-muted: var(--muted);
- --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
- --color-accent: var(--accent);
- --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
- --color-destructive: var(--destructive);
- --color-destructive-foreground: var(--destructive-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
- --color-constructive: var(--constructive);
- --color-constructive-foreground: var(--constructive-foreground);
+ --color-constructive: var(--constructive);
+ --color-constructive-foreground: var(--constructive-foreground);
- --color-border: var(--border);
- --color-input: var(--input);
- --color-ring: var(--ring);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
- /* Chart colors */
- --color-chart-1: var(--chart-1);
- --color-chart-2: var(--chart-2);
- --color-chart-3: var(--chart-3);
- --color-chart-4: var(--chart-4);
- --color-chart-5: var(--chart-5);
+ /* Chart colors */
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
- /* Sidebar */
- --color-sidebar: var(--sidebar-background);
- --color-sidebar-foreground: var(--sidebar-foreground);
- --color-sidebar-primary: var(--sidebar-primary);
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
- --color-sidebar-accent: var(--sidebar-accent);
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
- --color-sidebar-border: var(--sidebar-border);
- --color-sidebar-ring: var(--sidebar-ring);
+ /* Sidebar */
+ --color-sidebar: var(--sidebar-background);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
- /* Animations */
- --animate-accordion-down: accordion-down 0.2s ease-out;
- --animate-accordion-up: accordion-up 0.2s ease-out;
+ /* Animations */
+ --animate-accordion-down: accordion-down 0.2s ease-out;
+ --animate-accordion-up: accordion-up 0.2s ease-out;
- @keyframes accordion-down {
- from {
- height: 0;
- }
- to {
- height: var(--radix-accordion-content-height);
- }
- }
+ @keyframes accordion-down {
+ from {
+ height: 0;
+ }
+ to {
+ height: var(--radix-accordion-content-height);
+ }
+ }
- @keyframes accordion-up {
- from {
- height: var(--radix-accordion-content-height);
- }
- to {
- height: 0;
- }
- }
+ @keyframes accordion-up {
+ from {
+ height: var(--radix-accordion-content-height);
+ }
+ to {
+ height: 0;
+ }
+ }
}
@utility scrollbar-hidden {
- -ms-overflow-style: none;
- scrollbar-width: none;
- &::-webkit-scrollbar {
- display: none;
- }
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
}
@utility scrollbar-x-hidden {
- -ms-overflow-style: none;
- scrollbar-width: none;
- &::-webkit-scrollbar:horizontal {
- display: none;
- }
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar:horizontal {
+ display: none;
+ }
}
@utility scrollbar-y-hidden {
- -ms-overflow-style: none;
- scrollbar-width: none;
- &::-webkit-scrollbar:vertical {
- display: none;
- }
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar:vertical {
+ display: none;
+ }
}
@utility scrollbar-thin {
- &::-webkit-scrollbar {
- width: 6px;
- height: 8px;
- }
- &::-webkit-scrollbar-track {
- background: transparent;
- }
- &::-webkit-scrollbar-thumb {
- background: var(--border);
- border-radius: 4px;
- }
- &::-webkit-scrollbar-thumb:hover {
- background: var(--muted-foreground);
- }
+ &::-webkit-scrollbar {
+ width: 7px;
+ height: 7px;
+ }
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 4px;
+ }
+ &::-webkit-scrollbar-thumb:hover {
+ background: var(--muted-foreground);
+ }
}
@layer base {
- * {
- @apply border-border outline-ring/50;
- }
- body {
- @apply bg-background text-foreground;
- }
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
}
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index c2608bd9..f8e6040f 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -29,11 +29,20 @@ export default function RootLayout({
<head>
<BotIdClient protect={protectedRoutes} />
{process.env.NODE_ENV === "development" && (
- <Script
- src="//unpkg.com/react-scan/dist/auto.global.js"
- crossOrigin="anonymous"
- strategy="beforeInteractive"
- />
+ <>
+ <Script
+ src="//unpkg.com/react-scan/dist/auto.global.js"
+ crossOrigin="anonymous"
+ strategy="beforeInteractive"
+ />
+
+ {/* code to figma */}
+ {/* <script
+ dangerouslySetInnerHTML={{
+ __html: `(function(){var s=document.createElement('script');s.src='https://mcp.figma.com/mcp/html-to-design/capture.js';document.head.appendChild(s);})();`,
+ }}
+ /> */}
+ </>
)}
</head>
<body className={`${siteFont.className} font-sans antialiased`}>
diff --git a/apps/web/src/app/privacy/page.tsx b/apps/web/src/app/privacy/page.tsx
index 4b06405a..0672cfe5 100644
--- a/apps/web/src/app/privacy/page.tsx
+++ b/apps/web/src/app/privacy/page.tsx
@@ -37,7 +37,7 @@ export default function PrivacyPage() {
</AccordionTrigger>
<AccordionContent>
<h3 className="mb-3 text-lg font-medium">
- Your content stays private and encrypted.
+ Your content never leaves your device.
</h3>
<ol className="list-decimal space-y-2 pl-6">
<li>
@@ -45,22 +45,19 @@ export default function PrivacyPage() {
your files
</li>
<li>
- AI features require encrypted uploads - your content is
- encrypted before leaving your device
+ AI features like auto captions run locally in your browser
+ too - nothing is uploaded
</li>
<li>
- We only collect your email and basic profile info for your
- account
+ OpenCut does not currently require an account or login
</li>
<li>Project data stays on your device, not our servers</li>
<li>
- We use analytics to improve the app, but no personal video
+ We use anonymized analytics to improve the app, but no personal video
content is tracked
</li>
- <li>
- You can delete your account anytime and all data gets removed
- </li>
- <li>We don't sell your data or share it with advertisers</li>
+ <li>You can clear local data from your browser at any time</li>
+ <li>We don't sell or share your data with anyone (we don't even have it)</li>
</ol>
<p className="mt-4">
Questions? Email us at{" "}
@@ -78,84 +75,30 @@ export default function PrivacyPage() {
<section className="flex flex-col gap-3">
<h2 className="text-2xl font-semibold">How We Handle Your Content</h2>
<p>
- <strong>Basic video editing happens locally on your device.</strong>{" "}
- For standard editing features, we never upload, store, or have access
- to your video files. Your content remains completely private and under
- your control.
- </p>
- <p>
- <strong>AI features require secure processing:</strong> When you
- choose to use AI features like auto captions, your audio/video content
- is encrypted on your device before being uploaded to our servers for
- processing. We use zero-knowledge encryption, meaning we cannot
- decrypt or view your content.
- </p>
- <p>
- After AI processing is complete, the encrypted content is immediately
- deleted from our servers. Only the results (like generated captions)
- are returned to your device.
+ <strong>
+ All editing and processing happens locally on your device.
+ </strong>{" "}
+ We never upload, store, or have access to your video or audio files.
+ Your content remains completely private and under your control.
+ AI-powered features like auto captions also run in your browser using
+ on-device models - no content ever leaves your device.
</p>
</section>
<section className="flex flex-col gap-3">
- <h2 className="text-2xl font-semibold">Account Information</h2>
- <p>When you create an account, we only collect:</p>
- <ul className="list-disc space-y-2 pl-6">
- <li>Email address (for account access)</li>
- <li>
- Profile information from Google OAuth (if you choose to sign in with
- Google)
- </li>
- </ul>
- <p>
- <strong>We do NOT store your projects on our servers.</strong> All
- project data, including names, thumbnails, and creation dates, is
- stored locally in your browser using IndexedDB.
- </p>
+ <h2 className="text-2xl font-semibold">Accounts & Authentication</h2>
<p>
- We use{" "}
- <a
- href="https://www.better-auth.com"
- target="_blank"
- rel="noopener"
- className="text-primary hover:underline"
- >
- Better Auth
- </a>{" "}
- for secure authentication and follow industry-standard security
- practices.
+ OpenCut does not currently offer user accounts, login, or Google
+ sign-in.
</p>
- </section>
-
- <section className="flex flex-col gap-3">
- <h2 className="text-2xl font-semibold">AI Features & Encryption</h2>
<p>
- When you use AI-powered features (like auto captions, content
- analysis, or enhancement tools), your content needs to be processed on
- our servers. Here's how we protect your privacy:
+ Because there is no account system today, we do not collect account
+ emails, profile information, or OAuth identity data.
</p>
- <ul className="list-disc space-y-2 pl-6">
- <li>
- <strong>Client-side encryption:</strong> Your content is encrypted
- on your device before upload
- </li>
- <li>
- <strong>Zero-knowledge processing:</strong> We cannot decrypt or
- view your original content
- </li>
- <li>
- <strong>Temporary processing:</strong> Encrypted content is deleted
- immediately after processing
- </li>
- <li>
- <strong>Opt-in only:</strong> AI features are optional - basic
- editing remains fully local
- </li>
- </ul>
<p>
- Different AI features may process different types of content (audio
- for captions, video for analysis, etc.), but all follow the same
- zero-knowledge encryption approach.
+ Your projects are never stored on our servers. All project data,
+ including names, thumbnails, and creation dates, is stored locally
+ in your browser using IndexedDB.
</p>
</section>
@@ -171,13 +114,12 @@ export default function PrivacyPage() {
>
Databuddy
</a>{" "}
- for completely anonymized and non-invasive analytics to understand how
- people use OpenCut.
+ for basic, anonymized visitor counts. We do not track clicks,
+ interactions, or how you use the editor.
</p>
<p>
- This helps us improve the editor, but we never collect personal
- information, track individual users, or store any data that could
- identify you.
+ No personal information is collected, no individual users are tracked,
+ and no data that could identify you is stored.
</p>
</section>
@@ -187,7 +129,7 @@ export default function PrivacyPage() {
<ul className="list-disc space-y-2 pl-6">
<li>Save your projects locally on your device</li>
<li>Remember your editor preferences and settings</li>
- <li>Keep you logged in across browser sessions</li>
+ <li>Store app state needed for the editor to work between sessions</li>
</ul>
<p>
All data stays on your device and can be cleared at any time through
@@ -200,10 +142,6 @@ export default function PrivacyPage() {
<p>OpenCut integrates with these services:</p>
<ul className="list-disc space-y-2 pl-6">
<li>
- <strong>Google OAuth:</strong> For optional Google sign-in (governed
- by Google's privacy policy)
- </li>
- <li>
<strong>Vercel:</strong> For hosting and content delivery
</li>
<li>
@@ -216,8 +154,7 @@ export default function PrivacyPage() {
<h2 className="text-2xl font-semibold">Your Rights</h2>
<p>You have complete control over your data:</p>
<ul className="list-disc space-y-2 pl-6">
- <li>Delete your account and all associated data at any time</li>
- <li>Export your project data</li>
+ <li>No account is required to use OpenCut today</li>
<li>Clear local storage to remove all saved projects</li>
<li>Contact us with any privacy concerns</li>
</ul>
@@ -280,7 +217,7 @@ export default function PrivacyPage() {
<Separator />
<p className="text-muted-foreground text-sm">
- Last updated: July 14, 2025
+ Last updated: March 15, 2026
</p>
</BasePage>
);
diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx
index c25d75b9..9618de27 100644
--- a/apps/web/src/app/projects/page.tsx
+++ b/apps/web/src/app/projects/page.tsx
@@ -7,6 +7,7 @@ import type { KeyboardEvent, MouseEvent } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
+import { StoragePersistenceDialog } from "@/components/editor/dialogs/storage-persistence-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
@@ -18,7 +19,7 @@ import type {
TProjectMetadata,
TProjectSortKey,
TProjectSortOption,
-} from "@/types/project";
+} from "@/lib/project/types";
import { formatTimeCode } from "@/lib/time";
import { formatDate } from "@/utils/date";
import { HugeiconsIcon } from "@hugeicons/react";
@@ -86,6 +87,13 @@ const VIEW_MODE_OPTIONS = [
export default function ProjectsPage() {
const { searchQuery, sortKey, sortOrder, viewMode } = useProjectsStore();
const editor = useEditor();
+ const sortOption: TProjectSortOption = `${sortKey}-${sortOrder}`;
+
+ const isLoading = useEditor((e) => e.project.getIsLoading());
+ const isInitialized = useEditor((e) => e.project.getIsInitialized());
+ const projectsToDisplay = useEditor((e) =>
+ e.project.getFilteredAndSortedProjects({ searchQuery, sortOption }),
+ );
useEffect(() => {
if (!editor.project.getIsInitialized()) {
@@ -93,18 +101,10 @@ export default function ProjectsPage() {
}
}, [editor.project]);
- const sortOption: TProjectSortOption = `${sortKey}-${sortOrder}`;
- const projectsToDisplay = editor.project.getFilteredAndSortedProjects({
- searchQuery,
- sortOption,
- });
-
- const isLoading = editor.project.getIsLoading();
- const isInitialized = editor.project.getIsInitialized();
-
return (
<div className="bg-background min-h-screen">
<MigrationDialog />
+ <StoragePersistenceDialog />
<ProjectsHeader />
<ProjectsToolbar projectIds={projectsToDisplay.map((p) => p.id)} />
<main className="mx-auto px-4 pt-2 pb-6 flex flex-col gap-4">
diff --git a/apps/web/src/app/projects/store.ts b/apps/web/src/app/projects/store.ts
index 767f08df..2c79aa0a 100644
--- a/apps/web/src/app/projects/store.ts
+++ b/apps/web/src/app/projects/store.ts
@@ -1,6 +1,6 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
-import type { TProjectSortKey } from "@/types/project";
+import type { TProjectSortKey } from "@/lib/project/types";
export type ProjectsViewMode = "grid" | "list";
diff --git a/apps/web/src/app/terms/page.tsx b/apps/web/src/app/terms/page.tsx
index d5d43ba7..697a89ef 100644
--- a/apps/web/src/app/terms/page.tsx
+++ b/apps/web/src/app/terms/page.tsx
@@ -41,18 +41,17 @@ export default function TermsPage() {
</h3>
<ol className="list-decimal space-y-2 pl-6">
<li>
- Your content stays private - basic editing is local, AI features
- use encrypted uploads
+ Everything runs locally in your browser - nothing is ever
+ uploaded to our servers
</li>
<li>
- We never claim ownership of your content, even when processing
- AI features
+ We never claim ownership of your content
</li>
<li>
Free for personal and commercial use with no watermarks or
restrictions
</li>
- <li>Don't use OpenCut for illegal activities or harassment</li>
+ <li>You're responsible for how you use it - don't break the law</li>
<li>
Service provided "as is" - we can't guarantee perfect uptime
</li>
@@ -61,8 +60,7 @@ export default function TermsPage() {
needed
</li>
<li>
- You can delete your account anytime and keep using your exported
- videos
+ No account required - your exported videos are always yours
</li>
</ol>
<p className="mt-4">
@@ -81,21 +79,17 @@ export default function TermsPage() {
<section className="flex flex-col gap-3">
<h2 className="text-2xl font-semibold">Your Content, Your Rights</h2>
<p>
- <strong>You own everything you create.</strong> OpenCut processes
- basic editing locally on your device. For AI features, content is
- encrypted before upload and we cannot access your original files. We
- make no claims to ownership, licensing, or rights over your videos,
- projects, or any content you create using OpenCut.
+ <strong>You own everything you create.</strong> All editing and
+ processing happens locally on your device. We never see, store, or
+ have access to your files. We make no claims to ownership, licensing,
+ or rights over your videos, projects, or any content you create using
+ OpenCut.
</p>
<ul className="list-disc space-y-2 pl-6">
<li>
- Your content remains private and under your control at all times
+ Your content never leaves your device
</li>
<li>You retain all intellectual property rights to your content</li>
- <li>
- Even when using AI features, we cannot access your unencrypted
- content
- </li>
<li>You can export and use your content however you choose</li>
<li>No watermarks, no licensing restrictions from OpenCut</li>
</ul>
@@ -115,53 +109,26 @@ export default function TermsPage() {
</li>
</ul>
<p>
- <strong>What we ask:</strong> Don't use OpenCut for illegal
- activities, harassment, or creating harmful content. Be respectful of
- others and follow applicable laws.
+ You're responsible for how you use OpenCut and the content you create.
+ Don't use it for anything illegal in your jurisdiction.
</p>
</section>
<section className="flex flex-col gap-3">
- <h2 className="text-2xl font-semibold">
- AI Features and Data Processing
- </h2>
+ <h2 className="text-2xl font-semibold">AI Features</h2>
<p>
- OpenCut offers optional AI-powered features that require server
- processing:
- </p>
- <ul className="list-disc space-y-2 pl-6">
- <li>
- AI features (auto captions, content analysis, etc.) are completely
- optional
- </li>
- <li>Your content is encrypted on your device before any upload</li>
- <li>
- We use zero-knowledge encryption - we cannot decrypt your content
- </li>
- <li>Encrypted content is deleted immediately after processing</li>
- <li>
- You maintain full ownership and control of your content throughout
- </li>
- </ul>
- <p>
- By using AI features, you consent to the temporary, encrypted
- processing of your content as described in our Privacy Policy. You can
- always choose to use only local editing features.
+ AI features like auto captions run entirely in your browser using
+ on-device models. No content is uploaded to any server. These features
+ are optional - you can use OpenCut without them.
</p>
</section>
<section className="flex flex-col gap-3">
- <h2 className="text-2xl font-semibold">Account and Service</h2>
- <p>To use certain features, you may create an account:</p>
- <ul className="list-disc space-y-2 pl-6">
- <li>Provide accurate information when signing up</li>
- <li>Keep your account secure and don't share credentials</li>
- <li>You're responsible for activity under your account</li>
- <li>You can delete your account at any time</li>
- </ul>
+ <h2 className="text-2xl font-semibold">Service</h2>
<p>
- OpenCut is provided "as is" without warranties. While we strive for
- reliability, we can't guarantee uninterrupted service.
+ OpenCut does not currently require an account. The service is provided
+ "as is" without warranties. While we strive for reliability, we can't
+ guarantee uninterrupted service.
</p>
</section>
@@ -189,24 +156,6 @@ export default function TermsPage() {
</section>
<section className="flex flex-col gap-3">
- <h2 className="text-2xl font-semibold">Third-Party Content</h2>
- <p>
- When using OpenCut, make sure you have the right to use any content
- you import:
- </p>
- <ul className="list-disc space-y-2 pl-6">
- <li>Only upload content you own or have permission to use</li>
- <li>
- Respect copyright, trademarks, and other intellectual property
- </li>
- <li>
- Don't use copyrighted music, images, or videos without permission
- </li>
- <li>You're responsible for any claims related to your content</li>
- </ul>
- </section>
-
- <section className="flex flex-col gap-3">
<h2 className="text-2xl font-semibold">Limitations and Liability</h2>
<p>
OpenCut is provided free of charge. To the extent permitted by law:
@@ -239,13 +188,10 @@ export default function TermsPage() {
</section>
<section className="flex flex-col gap-3">
- <h2 className="text-2xl font-semibold">Termination</h2>
+ <h2 className="text-2xl font-semibold">Stopping Use</h2>
<p>You can stop using OpenCut at any time:</p>
<ul className="list-disc space-y-2 pl-6">
- <li>Delete your account through your profile settings</li>
<li>Clear your browser data to remove local projects</li>
- <li>Your content remains yours even if you stop using OpenCut</li>
- <li>We may suspend accounts for violations of these terms</li>
</ul>
</section>
@@ -288,7 +234,7 @@ export default function TermsPage() {
</section>
<Separator />
<p className="text-muted-foreground text-sm">
- Last updated: July 14, 2025
+ Last updated: March 15, 2026
</p>
</BasePage>
);