diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts
index 9edff1c7..c4b7818f 100644
--- a/apps/web/next-env.d.ts
+++ b/apps/web/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json
index 05b84ccc..0c97d4f2 100644
--- a/apps/web/package-lock.json
+++ b/apps/web/package-lock.json
@@ -28,6 +28,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
+ "culori": "^4.0.2",
"dayjs": "^1.11.13",
"drizzle-orm": "^0.44.2",
"embla-carousel-react": "^8.5.1",
@@ -51,6 +52,7 @@
"react-markdown": "^10.1.0",
"react-phone-number-input": "^3.4.11",
"react-resizable-panels": "^2.1.7",
+ "react-window": "^2.2.7",
"recharts": "^2.14.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-parse": "^9.0.1",
@@ -5868,6 +5870,15 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
+ "node_modules/culori": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/culori/-/culori-4.0.2.tgz",
+ "integrity": "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
@@ -9705,6 +9716,16 @@
"react-dom": ">=16.6.0"
}
},
+ "node_modules/react-window": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz",
+ "integrity": "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/recast": {
"version": "0.23.11",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz",
diff --git a/apps/web/package.json b/apps/web/package.json
index 997235cf..9ed3b24a 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -45,6 +45,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
+ "culori": "^4.0.2",
"dayjs": "^1.11.13",
"drizzle-orm": "^0.44.2",
"embla-carousel-react": "^8.5.1",
@@ -68,6 +69,7 @@
"react-markdown": "^10.1.0",
"react-phone-number-input": "^3.4.11",
"react-resizable-panels": "^2.1.7",
+ "react-window": "^2.2.7",
"recharts": "^2.14.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-parse": "^9.0.1",
@@ -98,8 +100,8 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"cross-env": "^7.0.3",
- "drizzle-kit": "^0.31.4",
"dotenv": "^16.5.0",
+ "drizzle-kit": "^0.31.4",
"postcss": "^8",
"storybook": "^8.6.14",
"tailwindcss": "^4.1.11",
diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx
index bc7e0ca0..5c70f49a 100644
--- a/apps/web/src/app/projects/page.tsx
+++ b/apps/web/src/app/projects/page.tsx
@@ -84,7 +84,15 @@ const VIEW_MODE_OPTIONS = [
];
export default function ProjectsPage() {
- const { searchQuery, sortKey, sortOrder, viewMode } = useProjectsStore();
+ const {
+ searchQuery,
+ sortKey,
+ sortOrder,
+ viewMode,
+ selectedProjectIds,
+ setSelectedProjects,
+ clearSelectedProjects,
+ } = useProjectsStore();
const editor = useEditor();
const authStatus = useAuthStore((state) => state.status);
@@ -107,6 +115,31 @@ export default function ProjectsPage() {
const isLoading = editor.project.getIsLoading();
const isInitialized = editor.project.getIsInitialized();
+ useEffect(() => {
+ if (authStatus !== "authenticated") {
+ clearSelectedProjects();
+ return;
+ }
+
+ if (isLoading || !isInitialized) {
+ return;
+ }
+
+ if (selectedProjectIds.length > 0 || projectsToDisplay.length === 0) {
+ return;
+ }
+
+ setSelectedProjects({ projectIds: [projectsToDisplay[0].id] });
+ }, [
+ authStatus,
+ clearSelectedProjects,
+ isInitialized,
+ isLoading,
+ projectsToDisplay,
+ selectedProjectIds.length,
+ setSelectedProjects,
+ ]);
+
return (
diff --git a/apps/web/src/components/editor/panels/assets/views/stickers.tsx b/apps/web/src/components/editor/panels/assets/views/stickers.tsx
index 671effec..b66a8763 100644
--- a/apps/web/src/components/editor/panels/assets/views/stickers.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/stickers.tsx
@@ -34,7 +34,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { OcSlidersVerticalIcon } from "@opencut/ui/icons";
+import { OcSlidersVerticalIcon } from "@/ui/icons";
import type { StickerCategory } from "@/types/stickers";
import { STICKER_CATEGORIES } from "@/constants/sticker-constants";
import { parseStickerId } from "@/lib/stickers/sticker-id";
diff --git a/apps/web/src/components/editor/panels/preview/toolbar.tsx b/apps/web/src/components/editor/panels/preview/toolbar.tsx
index 7f5cd9bc..a5b68f28 100644
--- a/apps/web/src/components/editor/panels/preview/toolbar.tsx
+++ b/apps/web/src/components/editor/panels/preview/toolbar.tsx
@@ -11,7 +11,7 @@ import {
PlayIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
-import { OcSocialIcon } from "@opencut/ui/icons";
+import { OcSocialIcon } from "@/ui/icons";
import { Separator } from "@/components/ui/separator";
export function PreviewToolbar({
diff --git a/apps/web/src/components/editor/panels/properties/sections/blending.tsx b/apps/web/src/components/editor/panels/properties/sections/blending.tsx
index 430efa25..7314783e 100644
--- a/apps/web/src/components/editor/panels/properties/sections/blending.tsx
+++ b/apps/web/src/components/editor/panels/properties/sections/blending.tsx
@@ -6,7 +6,7 @@ import {
DEFAULT_BLEND_MODE,
DEFAULT_OPACITY,
} from "@/constants/timeline-constants";
-import { OcCheckerboardIcon } from "@opencut/ui/icons";
+import { OcCheckerboardIcon } from "@/ui/icons";
import { Fragment, useRef } from "react";
import { Section, SectionContent, SectionHeader } from "../section";
import {
diff --git a/apps/web/src/components/editor/panels/properties/text-properties.tsx b/apps/web/src/components/editor/panels/properties/text-properties.tsx
index cfca52dc..9a8adcb9 100644
--- a/apps/web/src/components/editor/panels/properties/text-properties.tsx
+++ b/apps/web/src/components/editor/panels/properties/text-properties.tsx
@@ -24,7 +24,7 @@ import {
SelectContent,
SelectItem,
} from "@/components/ui/select";
-import { OcFontWeightIcon } from "@opencut/ui/icons";
+import { OcFontWeightIcon } from "@/ui/icons";
import { Label } from "@/components/ui/label";
export function TextProperties({
diff --git a/apps/web/src/lib/auth/api-client.ts b/apps/web/src/lib/auth/api-client.ts
index 964423d4..ae8cd0a1 100644
--- a/apps/web/src/lib/auth/api-client.ts
+++ b/apps/web/src/lib/auth/api-client.ts
@@ -147,6 +147,7 @@ export const backendAuthApi = {
body: { username, password },
}),
register: ({
+ username,
email,
firstname,
lastname,
@@ -154,6 +155,7 @@ export const backendAuthApi = {
role = 1,
organisation_name,
}: {
+ username: string;
email: string;
firstname: string;
lastname: string;
@@ -164,7 +166,7 @@ export const backendAuthApi = {
request<{ message: string; token: string; user: TBackendUser }>({
path: "/auth/user/register",
method: "POST",
- body: { email, firstname, lastname, password, role, organisation_name },
+ body: { username, email, firstname, lastname, password, role, organisation_name },
}),
refresh: () =>
request<{ token: string; user: TBackendUser }>({
diff --git a/apps/web/src/proxy.ts b/apps/web/src/proxy.ts
index 290883e3..58af807d 100644
--- a/apps/web/src/proxy.ts
+++ b/apps/web/src/proxy.ts
@@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server";
const PUBLIC_PATH_PREFIXES = ["/auth", "/privacy", "/terms"];
const PUBLIC_EXACT_PATHS = ["/robots.txt", "/sitemap.xml", "/manifest.json"];
-export async function middleware(request: NextRequest) {
+export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
const isPublicPath =
PUBLIC_EXACT_PATHS.includes(pathname) ||
diff --git a/apps/web/src/services/storage/service.ts b/apps/web/src/services/storage/service.ts
index 4727a173..27b2eacd 100644
--- a/apps/web/src/services/storage/service.ts
+++ b/apps/web/src/services/storage/service.ts
@@ -363,8 +363,16 @@ class StorageService {
}
async clearAllData(): Promise
{
- await this.projectsAdapter.clear();
- // project-specific media and timelines cleaned up when projects are deleted
+ const projectIds = await this.projectsAdapter.list();
+
+ await Promise.all(
+ projectIds.map((projectId) => this.deleteProjectMedia({ projectId })),
+ );
+
+ await Promise.all([
+ this.projectsAdapter.clear(),
+ this.savedSoundsAdapter.clear(),
+ ]);
}
async getStorageInfo(): Promise<{
diff --git a/apps/web/src/stores/auth-store.ts b/apps/web/src/stores/auth-store.ts
index 4d9f3cbf..17c1c885 100644
--- a/apps/web/src/stores/auth-store.ts
+++ b/apps/web/src/stores/auth-store.ts
@@ -3,6 +3,7 @@
import { create } from "zustand";
import { backendAuthApi, type TBackendUser } from "@/lib/auth/api-client";
import { authSession } from "@/lib/auth/session";
+import { storageService } from "@/services/storage/service";
type AuthStatus = "idle" | "loading" | "authenticated" | "unauthenticated";
@@ -125,6 +126,11 @@ export const useAuthStore = create()((set, get) => ({
} catch {
// Clear local state even if backend logout is unavailable/misconfigured.
} finally {
+ try {
+ await storageService.clearAllData();
+ } catch (error) {
+ console.error("Failed to clear local data during logout:", error);
+ }
clearAuthState({ set });
}
},
diff --git a/apps/web/src/ui/icons.tsx b/apps/web/src/ui/icons.tsx
index 86129aae..7d187df8 100644
--- a/apps/web/src/ui/icons.tsx
+++ b/apps/web/src/ui/icons.tsx
@@ -15,3 +15,19 @@ export function OcDataBuddyIcon(props: IconProps) {
return ;
}
+// Compatibility exports for upstream UI components.
+export function OcSocialIcon(props: IconProps) {
+ return ;
+}
+
+export function OcFontWeightIcon(props: IconProps) {
+ return ;
+}
+
+export function OcSlidersVerticalIcon(props: IconProps) {
+ return ;
+}
+
+export function OcCheckerboardIcon(props: IconProps) {
+ return ;
+}