From ae1393b81ceb21dc9b49835e1a15f5e7823ba535 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:59:06 +0300 Subject: [PATCH 01/21] wip --- .../caps/components/CapCard/CapCard.tsx | 18 ++- .../components/CapCard/CapCardButtons.tsx | 67 ++++------- .../caps/components/SettingsDialog.tsx | 104 ++++++++++++++++++ packages/ui/src/components/Switch.tsx | 2 +- 4 files changed, 141 insertions(+), 50 deletions(-) create mode 100644 apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx diff --git a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx index 160bbd9708..cfe50f6562 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -10,6 +10,7 @@ import type { Video } from "@cap/web-domain"; import { faCheck, faCopy, + faDownload, faEllipsis, faLock, faTrash, @@ -34,6 +35,7 @@ import { VideoThumbnail } from "@/components/VideoThumbnail"; import { useEffectMutation } from "@/lib/EffectRuntime"; import { withRpc } from "@/lib/Rpcs"; import { PasswordDialog } from "../PasswordDialog"; +import { SettingsDialog } from "../SettingsDialog"; import { SharingDialog } from "../SharingDialog"; import { CapCardAnalytics } from "./CapCardAnalytics"; import { CapCardButtons } from "./CapCardButtons"; @@ -99,6 +101,7 @@ export const CapCard = ({ }: CapCardProps) => { const [isSharingDialogOpen, setIsSharingDialogOpen] = useState(false); const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false); + const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [passwordProtected, setPasswordProtected] = useState( cap.hasPassword || false, @@ -266,6 +269,10 @@ export const CapCard = ({ return ( <> + setIsSettingsDialogOpen(false)} + /> setIsSharingDialogOpen(false)} @@ -316,11 +323,10 @@ export const CapCard = ({ @@ -347,6 +353,14 @@ export const CapCard = ({ + + +

Download

+
{ toast.promise(duplicateMutation.mutateAsync(), { diff --git a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButtons.tsx b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButtons.tsx index 3232c92b92..7e62237e2e 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButtons.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButtons.tsx @@ -1,6 +1,6 @@ import { buildEnv, NODE_ENV } from "@cap/env"; import { Button } from "@cap/ui"; -import { faDownload, faLink } from "@fortawesome/free-solid-svg-icons"; +import { faGear, faLink } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import clsx from "clsx"; import type { ReactNode } from "react"; @@ -18,21 +18,19 @@ interface ButtonConfig { export interface CapCardButtonsProps { capId: string; copyPressed: boolean; - isDownloading: boolean; handleCopy: (url: string) => void; - handleDownload: () => void; customDomain?: string | null; domainVerified?: boolean; + setIsSettingsDialogOpen: (isOpen: boolean) => void; } export const CapCardButtons: React.FC = ({ capId, copyPressed, - isDownloading, handleCopy, - handleDownload, customDomain, domainVerified, + setIsSettingsDialogOpen, }) => { const { webUrl } = usePublicEnv(); return ( @@ -40,14 +38,13 @@ export const CapCardButtons: React.FC = ({ {buttons( capId, copyPressed, - isDownloading, handleCopy, - handleDownload, webUrl, customDomain, domainVerified, + setIsSettingsDialogOpen, ).map((button, index) => ( - + + + + + + ); +}; diff --git a/packages/ui/src/components/Switch.tsx b/packages/ui/src/components/Switch.tsx index 61bf461ed5..950853b915 100644 --- a/packages/ui/src/components/Switch.tsx +++ b/packages/ui/src/components/Switch.tsx @@ -14,7 +14,7 @@ const Switch = React.forwardRef< "w-11 h-6 p-[0.125rem]", "bg-gray-5 data-[state=checked]:bg-blue-500", "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500", - "disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-gray-200", + "disabled:cursor-not-allowed disabled:opacity-40 disabled:bg-gray-4", className, )} {...props} From 12e7ae42f93af097a733290dca8a875a730d50f5 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:01:08 +0300 Subject: [PATCH 02/21] wip --- .../caps/components/CapCard/CapCard.tsx | 110 +++++---- .../caps/components/CapCard/CapCardButton.tsx | 2 + .../[spaceId]/components/SharedCapCard.tsx | 2 + apps/web/app/(site)/Navbar.tsx | 6 +- package.json | 1 + pnpm-lock.yaml | 230 +++++++++--------- 6 files changed, 195 insertions(+), 156 deletions(-) diff --git a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx index bf979e2c32..0426a740cc 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { VideoMetadata } from "@cap/database/types"; import { DropdownMenu, @@ -12,6 +14,7 @@ import { faCopy, faDownload, faEllipsis, + faGear, faLink, faLock, faTrash, @@ -35,6 +38,7 @@ import { VideoThumbnail } from "@/components/VideoThumbnail"; import { useEffectMutation } from "@/lib/EffectRuntime"; import { withRpc } from "@/lib/Rpcs"; import { PasswordDialog } from "../PasswordDialog"; +import { SettingsDialog } from "../SettingsDialog"; import { SharingDialog } from "../SharingDialog"; import { CapCardAnalytics } from "./CapCardAnalytics"; import { CapCardButton } from "./CapCardButton"; @@ -106,6 +110,7 @@ export const CapCard = ({ ); const [copyPressed, setCopyPressed] = useState(false); const [isDragging, setIsDragging] = useState(false); + const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false); const { isSubscribed, setUpgradeModalOpen } = useDashboardContext(); const [confirmOpen, setConfirmOpen] = useState(false); @@ -278,6 +283,10 @@ export const CapCard = ({ onSharingUpdated={handleSharingUpdated} isPublic={cap.public} /> + setIsSettingsDialogOpen(false)} + /> setIsPasswordDialogOpen(false)} @@ -316,6 +325,17 @@ export const CapCard = ({ "top-2 right-2 flex-col gap-2 z-[20]", )} > + { + e.stopPropagation(); + setIsSettingsDialogOpen(true); + }} + className="delay-0" + icon={() => { + return ; + }} + /> { @@ -347,51 +367,51 @@ export const CapCard = ({ ); }} /> - { - e.stopPropagation(); - handleDownload(); - }} - disabled={downloadMutation.isPending} - className="delay-25" - icon={() => { - return downloadMutation.isPending ? ( -
- -
- ) : ( - - ); - }} - /> {isOwner && ( - + + { + e.stopPropagation(); + handleDownload(); + }} + disabled={downloadMutation.isPending} + className="delay-25" + icon={() => { + return downloadMutation.isPending ? ( +
+ +
+ ) : ( + + ); + }} + />
- + { toast.promise(duplicateMutation.mutateAsync(), { diff --git a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx index 6489430682..50b5b2c6c3 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Button } from "@cap/ui"; import clsx from "clsx"; import type { MouseEvent, ReactNode } from "react"; diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx index 943c3484a0..5bfbcaf61b 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx @@ -1,3 +1,5 @@ +"use client"; + import type { VideoMetadata } from "@cap/database/types"; import type { Video } from "@cap/web-domain"; import { faBuilding, faUser } from "@fortawesome/free-solid-svg-icons"; diff --git a/apps/web/app/(site)/Navbar.tsx b/apps/web/app/(site)/Navbar.tsx index 3d7a82f618..048c9828a0 100644 --- a/apps/web/app/(site)/Navbar.tsx +++ b/apps/web/app/(site)/Navbar.tsx @@ -138,7 +138,7 @@ export const Navbar = () => { { href={sublink.href} className="block p-3 space-y-1 leading-none no-underline rounded-md transition-all duration-200 outline-none select-none hover:bg-gray-2 hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground" > -
+
{sublink.icon && sublink.icon} - + {sublink.label}
diff --git a/package.json b/package.json index 1fd394ce4f..96ff28d229 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@clack/prompts": "^0.10.0", "@effect/language-service": "^0.34.0", "dotenv-cli": "latest", + "prettier": "^3.5.3", "turbo": "^2.3.4", "typescript": "^5.8.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02c721ab6b..5c9ac9ad61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: dotenv-cli: specifier: latest version: 10.0.0 + prettier: + specifier: ^3.5.3 + version: 3.5.3 turbo: specifier: ^2.3.4 version: 2.5.3 @@ -304,31 +307,31 @@ importers: version: 1.9.0(react@19.1.1) '@storybook/addon-essentials': specifier: ^8.2.7 - version: 8.6.12(@types/react@19.1.13)(storybook@8.6.12(prettier@3.5.3)) + version: 8.6.12(@types/react@19.1.13)(storybook@8.6.12(prettier@3.6.2)) '@storybook/addon-interactions': specifier: ^8.2.7 - version: 8.6.12(storybook@8.6.12(prettier@3.5.3)) + version: 8.6.12(storybook@8.6.12(prettier@3.6.2)) '@storybook/addon-links': specifier: ^8.2.7 - version: 8.6.12(react@19.1.1)(storybook@8.6.12(prettier@3.5.3)) + version: 8.6.12(react@19.1.1)(storybook@8.6.12(prettier@3.6.2)) '@storybook/blocks': specifier: ^8.2.7 - version: 8.6.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.6.12(prettier@3.5.3)) + version: 8.6.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.6.12(prettier@3.6.2)) '@storybook/docs-tools': specifier: ^8.2.7 - version: 8.6.12(storybook@8.6.12(prettier@3.5.3)) + version: 8.6.12(storybook@8.6.12(prettier@3.6.2)) '@storybook/testing-library': specifier: ^0.2.2 version: 0.2.2 storybook: specifier: ^8.2.7 - version: 8.6.12(prettier@3.5.3) + version: 8.6.12(prettier@3.6.2) storybook-solidjs: specifier: ^1.0.0-beta.2 - version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3)) + version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.6.2)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.6.2)) storybook-solidjs-vite: specifier: ^1.0.0-beta.2 - version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(esbuild@0.25.4)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4)) + version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.6.2)))(esbuild@0.25.4)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.6.2))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4)) typescript: specifier: ^5.8.3 version: 5.8.3 @@ -689,7 +692,7 @@ importers: version: 15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next-auth: specifier: ^4.24.5 - version: 4.24.11(next@15.5.4(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 4.24.11(next@15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next-contentlayer2: specifier: ^0.5.3 version: 0.5.8(acorn@8.15.0)(contentlayer2@0.5.8(acorn@8.15.0)(esbuild@0.25.5))(esbuild@0.25.5)(next@15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -931,7 +934,7 @@ importers: version: 1.13.4(eslint@8.57.1) eslint-plugin-prettier: specifier: ^4.2.1 - version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.5.3) + version: 4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2) eslint-plugin-react: specifier: ^7.32.2 version: 7.37.5(eslint@8.57.1) @@ -991,7 +994,7 @@ importers: version: 15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next-auth: specifier: ^4.24.5 - version: 4.24.11(next@15.5.4(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 4.24.11(next@15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-email: specifier: ^4.0.16 version: 4.0.16(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -1215,7 +1218,7 @@ importers: version: 8.5.3 storybook-solidjs: specifier: ^1.0.0-beta.2 - version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3)) + version: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.6.2)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.6.2)) tailwind-scrollbar: specifier: ^3.1.0 version: 3.1.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.17)(typescript@5.8.3))) @@ -11777,6 +11780,11 @@ packages: engines: {node: '>=14'} hasBin: true + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + pretty-bytes@6.1.1: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} @@ -18584,7 +18592,7 @@ snapshots: '@react-email/render@1.1.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': dependencies: html-to-text: 9.0.5 - prettier: 3.5.3 + prettier: 3.6.2 react: 19.1.1 react-dom: 19.1.1(react@19.1.1) react-promise-suspense: 0.3.4 @@ -19386,114 +19394,114 @@ snapshots: '@standard-schema/spec@1.0.0': {} - '@storybook/addon-actions@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/addon-actions@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 '@types/uuid': 9.0.8 dequal: 2.0.3 polished: 4.3.1 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) uuid: 9.0.1 - '@storybook/addon-backgrounds@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/addon-backgrounds@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) ts-dedent: 2.2.0 - '@storybook/addon-controls@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/addon-controls@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 dequal: 2.0.3 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) ts-dedent: 2.2.0 - '@storybook/addon-docs@8.6.12(@types/react@19.1.13)(storybook@8.6.12(prettier@3.5.3))': + '@storybook/addon-docs@8.6.12(@types/react@19.1.13)(storybook@8.6.12(prettier@3.6.2))': dependencies: '@mdx-js/react': 3.1.0(@types/react@19.1.13)(react@19.1.1) - '@storybook/blocks': 8.6.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.6.12(prettier@3.5.3)) - '@storybook/csf-plugin': 8.6.12(storybook@8.6.12(prettier@3.5.3)) - '@storybook/react-dom-shim': 8.6.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.6.12(prettier@3.5.3)) + '@storybook/blocks': 8.6.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.6.12(prettier@3.6.2)) + '@storybook/csf-plugin': 8.6.12(storybook@8.6.12(prettier@3.6.2)) + '@storybook/react-dom-shim': 8.6.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.6.12(prettier@3.6.2)) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-essentials@8.6.12(@types/react@19.1.13)(storybook@8.6.12(prettier@3.5.3))': - dependencies: - '@storybook/addon-actions': 8.6.12(storybook@8.6.12(prettier@3.5.3)) - '@storybook/addon-backgrounds': 8.6.12(storybook@8.6.12(prettier@3.5.3)) - '@storybook/addon-controls': 8.6.12(storybook@8.6.12(prettier@3.5.3)) - '@storybook/addon-docs': 8.6.12(@types/react@19.1.13)(storybook@8.6.12(prettier@3.5.3)) - '@storybook/addon-highlight': 8.6.12(storybook@8.6.12(prettier@3.5.3)) - '@storybook/addon-measure': 8.6.12(storybook@8.6.12(prettier@3.5.3)) - '@storybook/addon-outline': 8.6.12(storybook@8.6.12(prettier@3.5.3)) - '@storybook/addon-toolbars': 8.6.12(storybook@8.6.12(prettier@3.5.3)) - '@storybook/addon-viewport': 8.6.12(storybook@8.6.12(prettier@3.5.3)) - storybook: 8.6.12(prettier@3.5.3) + '@storybook/addon-essentials@8.6.12(@types/react@19.1.13)(storybook@8.6.12(prettier@3.6.2))': + dependencies: + '@storybook/addon-actions': 8.6.12(storybook@8.6.12(prettier@3.6.2)) + '@storybook/addon-backgrounds': 8.6.12(storybook@8.6.12(prettier@3.6.2)) + '@storybook/addon-controls': 8.6.12(storybook@8.6.12(prettier@3.6.2)) + '@storybook/addon-docs': 8.6.12(@types/react@19.1.13)(storybook@8.6.12(prettier@3.6.2)) + '@storybook/addon-highlight': 8.6.12(storybook@8.6.12(prettier@3.6.2)) + '@storybook/addon-measure': 8.6.12(storybook@8.6.12(prettier@3.6.2)) + '@storybook/addon-outline': 8.6.12(storybook@8.6.12(prettier@3.6.2)) + '@storybook/addon-toolbars': 8.6.12(storybook@8.6.12(prettier@3.6.2)) + '@storybook/addon-viewport': 8.6.12(storybook@8.6.12(prettier@3.6.2)) + storybook: 8.6.12(prettier@3.6.2) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-highlight@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/addon-highlight@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) - '@storybook/addon-interactions@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/addon-interactions@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.6.12(storybook@8.6.12(prettier@3.5.3)) - '@storybook/test': 8.6.12(storybook@8.6.12(prettier@3.5.3)) + '@storybook/instrumenter': 8.6.12(storybook@8.6.12(prettier@3.6.2)) + '@storybook/test': 8.6.12(storybook@8.6.12(prettier@3.6.2)) polished: 4.3.1 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) ts-dedent: 2.2.0 - '@storybook/addon-links@8.6.12(react@19.1.1)(storybook@8.6.12(prettier@3.5.3))': + '@storybook/addon-links@8.6.12(react@19.1.1)(storybook@8.6.12(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) ts-dedent: 2.2.0 optionalDependencies: react: 19.1.1 - '@storybook/addon-measure@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/addon-measure@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) tiny-invariant: 1.3.3 - '@storybook/addon-outline@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/addon-outline@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) ts-dedent: 2.2.0 - '@storybook/addon-toolbars@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/addon-toolbars@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) - '@storybook/addon-viewport@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/addon-viewport@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: memoizerific: 1.11.3 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) - '@storybook/blocks@8.6.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.6.12(prettier@3.5.3))': + '@storybook/blocks@8.6.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.6.12(prettier@3.6.2))': dependencies: '@storybook/icons': 1.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) ts-dedent: 2.2.0 optionalDependencies: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - '@storybook/builder-vite@10.0.0-beta.7(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4))': + '@storybook/builder-vite@10.0.0-beta.7(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.6.2))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4))': dependencies: - '@storybook/csf-plugin': 10.0.0-beta.7(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4)) - storybook: 8.6.12(prettier@3.5.3) + '@storybook/csf-plugin': 10.0.0-beta.7(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.6.2))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4)) + storybook: 8.6.12(prettier@3.6.2) ts-dedent: 2.2.0 vite: 6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: @@ -19501,9 +19509,9 @@ snapshots: - rollup - webpack - '@storybook/core@8.6.12(prettier@3.5.3)(storybook@8.6.12(prettier@3.5.3))': + '@storybook/core@8.6.12(prettier@3.6.2)(storybook@8.6.12(prettier@3.6.2))': dependencies: - '@storybook/theming': 8.6.12(storybook@8.6.12(prettier@3.5.3)) + '@storybook/theming': 8.6.12(storybook@8.6.12(prettier@3.6.2)) better-opn: 3.0.2 browser-assert: 1.2.1 esbuild: 0.25.4 @@ -19515,16 +19523,16 @@ snapshots: util: 0.12.5 ws: 8.18.2 optionalDependencies: - prettier: 3.5.3 + prettier: 3.6.2 transitivePeerDependencies: - bufferutil - storybook - supports-color - utf-8-validate - '@storybook/csf-plugin@10.0.0-beta.7(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4))': + '@storybook/csf-plugin@10.0.0-beta.7(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.6.2))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4))': dependencies: - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) unplugin: 2.3.10 optionalDependencies: esbuild: 0.25.4 @@ -19532,18 +19540,18 @@ snapshots: vite: 6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1) webpack: 5.101.3(esbuild@0.25.4) - '@storybook/csf-plugin@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/csf-plugin@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) unplugin: 1.16.1 - '@storybook/docs-tools@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/docs-tools@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) - '@storybook/docs-tools@9.0.0-alpha.1(storybook@8.6.12(prettier@3.5.3))': + '@storybook/docs-tools@9.0.0-alpha.1(storybook@8.6.12(prettier@3.6.2))': dependencies: - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) '@storybook/global@5.0.0': {} @@ -19552,32 +19560,32 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - '@storybook/instrumenter@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/instrumenter@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 '@vitest/utils': 2.1.9 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) - '@storybook/preview-api@9.0.0-alpha.1(storybook@8.6.12(prettier@3.5.3))': + '@storybook/preview-api@9.0.0-alpha.1(storybook@8.6.12(prettier@3.6.2))': dependencies: - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) - '@storybook/react-dom-shim@8.6.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.6.12(prettier@3.5.3))': + '@storybook/react-dom-shim@8.6.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.6.12(prettier@3.6.2))': dependencies: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) - '@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/test@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.6.12(storybook@8.6.12(prettier@3.5.3)) + '@storybook/instrumenter': 8.6.12(storybook@8.6.12(prettier@3.6.2)) '@testing-library/dom': 10.4.0 '@testing-library/jest-dom': 6.5.0 '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) '@vitest/expect': 2.0.5 '@vitest/spy': 2.0.5 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) '@storybook/testing-library@0.2.2': dependencies: @@ -19585,13 +19593,13 @@ snapshots: '@testing-library/user-event': 14.6.1(@testing-library/dom@9.3.4) ts-dedent: 2.2.0 - '@storybook/theming@8.6.12(storybook@8.6.12(prettier@3.5.3))': + '@storybook/theming@8.6.12(storybook@8.6.12(prettier@3.6.2))': dependencies: - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) - '@storybook/types@9.0.0-alpha.1(storybook@8.6.12(prettier@3.5.3))': + '@storybook/types@9.0.0-alpha.1(storybook@8.6.12(prettier@3.6.2))': dependencies: - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) '@stripe/stripe-js@3.5.0': {} @@ -22759,8 +22767,8 @@ snapshots: '@typescript-eslint/parser': 7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) eslint: 9.30.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.30.1(jiti@2.4.2)) eslint-plugin-react: 7.37.5(eslint@9.30.1(jiti@2.4.2)) eslint-plugin-react-hooks: 5.2.0(eslint@9.30.1(jiti@2.4.2)) @@ -22788,33 +22796,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@5.5.0) - eslint: 9.30.1(jiti@2.4.2) + eslint: 8.57.1 get-tsconfig: 4.10.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.13 unrs-resolver: 1.7.2 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.30.1(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@5.5.0) - eslint: 8.57.1 + eslint: 9.30.1(jiti@2.4.2) get-tsconfig: 4.10.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.13 unrs-resolver: 1.7.2 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -22840,14 +22848,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) eslint: 9.30.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.30.1(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -22909,7 +22917,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -22920,7 +22928,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.30.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.30.1(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -22976,10 +22984,10 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.5.3): + eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.6.2): dependencies: eslint: 8.57.1 - prettier: 3.5.3 + prettier: 3.6.2 prettier-linter-helpers: 1.0.0 optionalDependencies: eslint-config-prettier: 8.10.0(eslint@8.57.1) @@ -25880,7 +25888,7 @@ snapshots: p-wait-for: 5.0.2 qs: 6.14.0 - next-auth@4.24.11(next@15.5.4(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + next-auth@4.24.11(next@15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: '@babel/runtime': 7.27.1 '@panva/hkdf': 1.2.1 @@ -26743,6 +26751,8 @@ snapshots: prettier@3.5.3: {} + prettier@3.6.2: {} + pretty-bytes@6.1.1: {} pretty-format@27.5.1: @@ -27924,14 +27934,14 @@ snapshots: stoppable@1.1.0: {} - storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(esbuild@0.25.4)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4)): + storybook-solidjs-vite@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.6.2)))(esbuild@0.25.4)(rollup@4.40.2)(solid-js@1.9.6)(storybook@8.6.12(prettier@3.6.2))(vite-plugin-solid@2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1)))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4)): dependencies: - '@storybook/builder-vite': 10.0.0-beta.7(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.5.3))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4)) - '@storybook/types': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.5.3)) + '@storybook/builder-vite': 10.0.0-beta.7(esbuild@0.25.4)(rollup@4.40.2)(storybook@8.6.12(prettier@3.6.2))(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1))(webpack@5.101.3(esbuild@0.25.4)) + '@storybook/types': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.6.2)) magic-string: 0.30.17 solid-js: 1.9.6 - storybook: 8.6.12(prettier@3.5.3) - storybook-solidjs: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3)) + storybook: 8.6.12(prettier@3.6.2) + storybook-solidjs: 1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.6.2)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.6.2)) vite: 6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1) vite-plugin-solid: 2.11.6(@testing-library/jest-dom@6.5.0)(solid-js@1.9.6)(vite@6.3.5(@types/node@22.15.17)(jiti@2.4.2)(terser@5.44.0)(yaml@2.8.1)) transitivePeerDependencies: @@ -27940,25 +27950,25 @@ snapshots: - rollup - webpack - storybook-solidjs@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.5.3)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.5.3)): + storybook-solidjs@1.0.0-beta.7(@storybook/test@8.6.12(storybook@8.6.12(prettier@3.6.2)))(solid-js@1.9.6)(storybook@8.6.12(prettier@3.6.2)): dependencies: '@babel/standalone': 7.27.2 - '@storybook/docs-tools': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.5.3)) + '@storybook/docs-tools': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.6.2)) '@storybook/global': 5.0.0 - '@storybook/preview-api': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.5.3)) - '@storybook/types': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.5.3)) + '@storybook/preview-api': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.6.2)) + '@storybook/types': 9.0.0-alpha.1(storybook@8.6.12(prettier@3.6.2)) '@types/babel__standalone': link:.yarn/cache/null async-mutex: 0.5.0 solid-js: 1.9.6 - storybook: 8.6.12(prettier@3.5.3) + storybook: 8.6.12(prettier@3.6.2) optionalDependencies: - '@storybook/test': 8.6.12(storybook@8.6.12(prettier@3.5.3)) + '@storybook/test': 8.6.12(storybook@8.6.12(prettier@3.6.2)) - storybook@8.6.12(prettier@3.5.3): + storybook@8.6.12(prettier@3.6.2): dependencies: - '@storybook/core': 8.6.12(prettier@3.5.3)(storybook@8.6.12(prettier@3.5.3)) + '@storybook/core': 8.6.12(prettier@3.6.2)(storybook@8.6.12(prettier@3.6.2)) optionalDependencies: - prettier: 3.5.3 + prettier: 3.6.2 transitivePeerDependencies: - bufferutil - supports-color From c31a74b035a128bbe9e4282c6ef4c45113a5972c Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Fri, 26 Sep 2025 11:40:40 +0300 Subject: [PATCH 03/21] wip --- .../dashboard/_components/Navbar/Items.tsx | 2 +- .../caps/components/CapCard/CapCardButton.tsx | 2 +- apps/web/package.json | 2 +- pnpm-lock.yaml | 31 +++++++++++-------- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx b/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx index ab0346e08f..7c8a3061b7 100644 --- a/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx +++ b/apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx @@ -101,7 +101,7 @@ const AdminNavItems = ({ toggleMobileNav }: Props) => { position="right" content={activeOrg?.organization.name ?? "No organization found"} > - +
- - diff --git a/apps/web/app/(org)/dashboard/caps/page.tsx b/apps/web/app/(org)/dashboard/caps/page.tsx index 98f4753f4f..6f143b8ae8 100644 --- a/apps/web/app/(org)/dashboard/caps/page.tsx +++ b/apps/web/app/(org)/dashboard/caps/page.tsx @@ -176,6 +176,7 @@ export default async function CapsPage(props: { hasActiveUpload: sql`${videoUploads.videoId} IS NOT NULL`.mapWith( Boolean, ), + settings: videos.settings, }) .from(videos) .leftJoin(comments, eq(videos.id, comments.videoId)) @@ -231,6 +232,7 @@ export default async function CapsPage(props: { ...videoWithoutEffectiveDate, id: Video.VideoId.make(video.id), foldersData, + settings: video.settings, sharedOrganizations: Array.isArray(video.sharedOrganizations) ? video.sharedOrganizations.filter( (organization) => organization.id !== null, diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index 28602e4217..09ea80a4f8 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -44,6 +44,14 @@ type VideoWithOrganizationInfo = typeof videos.$inferSelect & { sharedOrganizations?: { id: string; name: string }[]; hasPassword?: boolean; ownerIsPro?: boolean; + settings?: { + disableSummary?: boolean; + disableCaptions?: boolean; + disableChapters?: boolean; + disableReactions?: boolean; + disableTranscript?: boolean; + disableComments?: boolean; + }; }; interface ShareProps { @@ -278,6 +286,7 @@ export const Share = ({ setOptimisticComments={setOptimisticComments} handleCommentSuccess={handleCommentSuccess} views={views} + settings={data.settings} onSeek={handleSeek} videoId={data.id} aiData={aiData} @@ -292,6 +301,7 @@ export const Share = ({ diff --git a/apps/web/app/s/[videoId]/_components/Sidebar.tsx b/apps/web/app/s/[videoId]/_components/Sidebar.tsx index bb806ff7bf..5b71476e72 100644 --- a/apps/web/app/s/[videoId]/_components/Sidebar.tsx +++ b/apps/web/app/s/[videoId]/_components/Sidebar.tsx @@ -30,6 +30,14 @@ interface SidebarProps { setCommentsData: React.Dispatch>; views: MaybePromise; onSeek?: (time: number) => void; + settings?: { + disableSummary?: boolean; + disableCaptions?: boolean; + disableChapters?: boolean; + disableReactions?: boolean; + disableTranscript?: boolean; + disableComments?: boolean; + }; videoId: Video.VideoId; aiData?: { title?: string | null; @@ -75,6 +83,7 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( handleCommentSuccess, setOptimisticComments, views, + settings, onSeek, videoId, aiData, @@ -91,13 +100,18 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( const [activeTab, setActiveTab] = useState("activity"); const [[page, direction], setPage] = useState([0, 0]); - const hasExistingAiData = - aiData?.summary || (aiData?.chapters && aiData.chapters.length > 0); - const tabs = [ - { id: "activity", label: "Comments" }, - { id: "summary", label: "Summary" }, - { id: "transcript", label: "Transcript" }, + { + id: "activity", + label: "Comments", + disabled: settings?.disableComments, + }, + { id: "summary", label: "Summary", disabled: settings?.disableSummary }, + { + id: "transcript", + label: "Transcript", + disabled: settings?.disableTranscript, + }, ]; const paginate = (tabId: TabType) => { @@ -159,38 +173,40 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>(
- {tabs.map((tab) => ( - - ))} + + {tab.label} + + {activeTab === tab.id && ( + + )} + + ))}
diff --git a/apps/web/app/s/[videoId]/_components/Toolbar.tsx b/apps/web/app/s/[videoId]/_components/Toolbar.tsx index 1f67514d29..91da682e1f 100644 --- a/apps/web/app/s/[videoId]/_components/Toolbar.tsx +++ b/apps/web/app/s/[videoId]/_components/Toolbar.tsx @@ -15,6 +15,7 @@ interface ToolbarProps { user: typeof userSelectProps | null; onOptimisticComment?: (comment: CommentType) => void; onCommentSuccess?: (comment: CommentType) => void; + disableReactions?: boolean; } export const Toolbar = ({ @@ -22,6 +23,7 @@ export const Toolbar = ({ user, onOptimisticComment, onCommentSuccess, + disableReactions, }: ToolbarProps) => { const [commentBoxOpen, setCommentBoxOpen] = useState(false); const [comment, setComment] = useState(""); @@ -185,6 +187,10 @@ export const Toolbar = ({ setCommentBoxOpen(true); }; + if (disableReactions) { + return null; + } + return ( <> ) { skipProcessing: videos.skipProcessing, transcriptionStatus: videos.transcriptionStatus, source: videos.source, + settings: videos.settings, width: videos.width, height: videos.height, duration: videos.duration, diff --git a/packages/database/schema.ts b/packages/database/schema.ts index ccf63a2cce..40ecec96f2 100644 --- a/packages/database/schema.ts +++ b/packages/database/schema.ts @@ -253,6 +253,14 @@ export const videos = mysqlTable( fps: int("fps"), metadata: json("metadata").$type(), public: boolean("public").notNull().default(true), + settings: json("settings").$type<{ + disableSummary?: boolean; + disableCaptions?: boolean; + disableChapters?: boolean; + disableReactions?: boolean; + disableTranscript?: boolean; + disableComments?: boolean; + }>(), transcriptionStatus: varchar("transcriptionStatus", { length: 255 }).$type< "PROCESSING" | "COMPLETE" | "ERROR" >(), From 9efe4ae61d8e4251ff4a6a210641e6e7b99e492a Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:53:04 +0300 Subject: [PATCH 05/21] more settings --- apps/web/app/s/[videoId]/Share.tsx | 2 + .../s/[videoId]/_components/ShareVideo.tsx | 347 +++++++++--------- .../app/s/[videoId]/_components/Sidebar.tsx | 1 + .../s/[videoId]/_components/tabs/Summary.tsx | 36 +- 4 files changed, 200 insertions(+), 186 deletions(-) diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index 057d0a3376..db07de1b27 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -289,6 +289,8 @@ export const Share = ({ data={{ ...data, transcriptionStatus }} user={user} comments={comments} + areChaptersDisabled={data.settings?.disableChapters} + areCaptionsDisabled={data.settings?.disableCaptions} chapters={aiData?.chapters || []} aiProcessing={aiData?.processing || false} ref={playerRef} diff --git a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx index 30e0b78566..13306df35f 100644 --- a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx @@ -40,190 +40,197 @@ export const ShareVideo = forwardRef< user: typeof userSelectProps | null; comments: MaybePromise; chapters?: { title: string; start: number }[]; + areChaptersDisabled?: boolean; + areCaptionsDisabled?: boolean; aiProcessing?: boolean; } ->(({ data, comments, chapters = [] }, ref) => { - const videoRef = useRef(null); - useImperativeHandle(ref, () => videoRef.current as HTMLVideoElement, []); - - const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); - const [transcriptData, setTranscriptData] = useState([]); - const [subtitleUrl, setSubtitleUrl] = useState(null); - const [chaptersUrl, setChaptersUrl] = useState(null); - const [commentsData, setCommentsData] = useState([]); - - const { data: transcriptContent, error: transcriptError } = useTranscript( - data.id, - data.transcriptionStatus, - ); - - // Handle comments data - useEffect(() => { - if (comments) { - if (Array.isArray(comments)) { - setCommentsData(comments); - } else { - comments.then(setCommentsData); +>( + ( + { data, comments, chapters = [], areCaptionsDisabled, areChaptersDisabled }, + ref, + ) => { + const videoRef = useRef(null); + useImperativeHandle(ref, () => videoRef.current as HTMLVideoElement, []); + + const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); + const [transcriptData, setTranscriptData] = useState([]); + const [subtitleUrl, setSubtitleUrl] = useState(null); + const [chaptersUrl, setChaptersUrl] = useState(null); + const [commentsData, setCommentsData] = useState([]); + + const { data: transcriptContent, error: transcriptError } = useTranscript( + data.id, + data.transcriptionStatus, + ); + + // Handle comments data + useEffect(() => { + if (comments) { + if (Array.isArray(comments)) { + setCommentsData(comments); + } else { + comments.then(setCommentsData); + } } - } - }, [comments]); - - // Handle seek functionality - const handleSeek = (time: number) => { - if (videoRef.current) { - videoRef.current.currentTime = time; - } - }; - - useEffect(() => { - if (transcriptContent) { - const parsed = parseVTT(transcriptContent); - setTranscriptData(parsed); - } else if (transcriptError) { - console.error( - "[Transcript] Transcript error from React Query:", - transcriptError.message, - ); - } - }, [transcriptContent, transcriptError]); - - // Handle subtitle URL creation - useEffect(() => { - if ( - data.transcriptionStatus === "COMPLETE" && - transcriptData && - transcriptData.length > 0 - ) { - const vttContent = formatTranscriptAsVTT(transcriptData); - const blob = new Blob([vttContent], { type: "text/vtt" }); - const newUrl = URL.createObjectURL(blob); + }, [comments]); - // Clean up previous URL - if (subtitleUrl) { - URL.revokeObjectURL(subtitleUrl); + // Handle seek functionality + const handleSeek = (time: number) => { + if (videoRef.current) { + videoRef.current.currentTime = time; } + }; - setSubtitleUrl(newUrl); - - return () => { - URL.revokeObjectURL(newUrl); - }; - } else { - // Clean up if no longer needed - if (subtitleUrl) { - URL.revokeObjectURL(subtitleUrl); - setSubtitleUrl(null); + useEffect(() => { + if (transcriptContent) { + const parsed = parseVTT(transcriptContent); + setTranscriptData(parsed); + } else if (transcriptError) { + console.error( + "[Transcript] Transcript error from React Query:", + transcriptError.message, + ); } - } - }, [data.transcriptionStatus, transcriptData]); - - // Handle chapters URL creation - useEffect(() => { - if (chapters?.length > 0) { - const vttContent = formatChaptersAsVTT(chapters); - const blob = new Blob([vttContent], { type: "text/vtt" }); - const newUrl = URL.createObjectURL(blob); - - // Clean up previous URL - if (chaptersUrl) { - URL.revokeObjectURL(chaptersUrl); + }, [transcriptContent, transcriptError]); + + // Handle subtitle URL creation + useEffect(() => { + if ( + data.transcriptionStatus === "COMPLETE" && + transcriptData && + transcriptData.length > 0 + ) { + const vttContent = formatTranscriptAsVTT(transcriptData); + const blob = new Blob([vttContent], { type: "text/vtt" }); + const newUrl = URL.createObjectURL(blob); + + // Clean up previous URL + if (subtitleUrl) { + URL.revokeObjectURL(subtitleUrl); + } + + setSubtitleUrl(newUrl); + + return () => { + URL.revokeObjectURL(newUrl); + }; + } else { + // Clean up if no longer needed + if (subtitleUrl) { + URL.revokeObjectURL(subtitleUrl); + setSubtitleUrl(null); + } } + }, [data.transcriptionStatus, transcriptData]); - setChaptersUrl(newUrl); + // Handle chapters URL creation + useEffect(() => { + if (chapters?.length > 0) { + const vttContent = formatChaptersAsVTT(chapters); + const blob = new Blob([vttContent], { type: "text/vtt" }); + const newUrl = URL.createObjectURL(blob); - return () => { - URL.revokeObjectURL(newUrl); - }; - } else { - // Clean up if no longer needed - if (chaptersUrl) { - URL.revokeObjectURL(chaptersUrl); - setChaptersUrl(null); + // Clean up previous URL + if (chaptersUrl) { + URL.revokeObjectURL(chaptersUrl); + } + + setChaptersUrl(newUrl); + + return () => { + URL.revokeObjectURL(newUrl); + }; + } else { + // Clean up if no longer needed + if (chaptersUrl) { + URL.revokeObjectURL(chaptersUrl); + setChaptersUrl(null); + } } + }, [chapters]); + + let videoSrc: string; + let enableCrossOrigin = false; + + if (data.source.type === "desktopMP4") { + videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=mp4`; + // Start with CORS enabled for desktopMP4, but CapVideoPlayer will dynamically disable if needed + enableCrossOrigin = true; + } else if ( + NODE_ENV === "development" || + ((data.skipProcessing === true || data.jobStatus !== "COMPLETE") && + data.source.type === "MediaConvert") + ) { + videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=master`; + } else if (data.source.type === "MediaConvert") { + videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=video`; + } else { + videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=video`; } - }, [chapters]); - - let videoSrc: string; - let enableCrossOrigin = false; - - if (data.source.type === "desktopMP4") { - videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=mp4`; - // Start with CORS enabled for desktopMP4, but CapVideoPlayer will dynamically disable if needed - enableCrossOrigin = true; - } else if ( - NODE_ENV === "development" || - ((data.skipProcessing === true || data.jobStatus !== "COMPLETE") && - data.source.type === "MediaConvert") - ) { - videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=master`; - } else if (data.source.type === "MediaConvert") { - videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=video`; - } else { - videoSrc = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=video`; - } - return ( - <> -
- {data.source.type === "desktopMP4" ? ( - ({ - id: comment.id, - type: comment.type, - timestamp: comment.timestamp, - content: comment.content, - authorName: comment.authorName, - }))} - onSeek={handleSeek} - /> - ) : ( - - )} -
- - {!data.ownerIsPro && ( -
-
{ - e.stopPropagation(); - setUpgradeModalOpen(true); - }} - > -
-
- -
+ return ( + <> +
+ {data.source.type === "desktopMP4" ? ( + ({ + id: comment.id, + type: comment.type, + timestamp: comment.timestamp, + content: comment.content, + authorName: comment.authorName, + }))} + onSeek={handleSeek} + /> + ) : ( + + )} +
-
-

- Remove watermark -

+ {!data.ownerIsPro && ( +
+
{ + e.stopPropagation(); + setUpgradeModalOpen(true); + }} + > +
+
+ +
+ +
+

+ Remove watermark +

+
-
- )} - - - ); -}); + )} + + + ); + }, +); diff --git a/apps/web/app/s/[videoId]/_components/Sidebar.tsx b/apps/web/app/s/[videoId]/_components/Sidebar.tsx index 5b71476e72..590fced811 100644 --- a/apps/web/app/s/[videoId]/_components/Sidebar.tsx +++ b/apps/web/app/s/[videoId]/_components/Sidebar.tsx @@ -155,6 +155,7 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( { @@ -34,24 +35,24 @@ const formatTime = (time: number) => { const SkeletonLoader = () => (
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
+
{[1, 2, 3, 4].map((i) => (
-
-
+
+
))}
@@ -62,6 +63,7 @@ const SkeletonLoader = () => ( export const Summary: React.FC = ({ onSeek, initialAiData, + isSummaryDisabled = false, aiGenerationEnabled = false, user, }) => { @@ -100,8 +102,8 @@ export const Summary: React.FC = ({ return (
-
-
+
+
= ({ />
-

+

Unlock Cap AI

-

+

Upgrade to Cap Pro to access AI-powered features including automatic titles, video summaries, and intelligent chapter generation. @@ -139,6 +141,8 @@ export const Summary: React.FC = ({ ); } + if (isSummaryDisabled) return null; + if (isLoading || aiData?.processing) { return (

@@ -203,10 +207,10 @@ export const Summary: React.FC = ({ {aiData.chapters.map((chapter) => (
handleSeek(chapter.start)} > - + {formatTime(chapter.start)} {chapter.title} From 19dae6d4eef3106b2dfcbecde3b53dc1abadc5c9 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Mon, 29 Sep 2025 16:31:52 +0300 Subject: [PATCH 06/21] setup org-wide settings and more --- apps/web/actions/organization/settings.ts | 41 ++++ apps/web/actions/videos/settings.ts | 9 +- apps/web/app/(org)/dashboard/Contexts.tsx | 11 +- .../(org)/dashboard/_components/MobileTab.tsx | 2 +- .../caps/components/CapCard/CapCard.tsx | 24 +- .../caps/components/SettingsDialog.tsx | 30 ++- .../web/app/(org)/dashboard/dashboard-data.ts | 14 ++ apps/web/app/(org)/dashboard/layout.tsx | 7 + .../settings/organization/Organization.tsx | 5 + .../components/CapSettingsCard.tsx | 218 +++++++++--------- .../dashboard/settings/organization/page.tsx | 1 - apps/web/app/s/[videoId]/Share.tsx | 27 ++- .../[videoId]/_components/CapVideoPlayer.tsx | 13 +- .../[videoId]/_components/HLSVideoPlayer.tsx | 23 +- .../s/[videoId]/_components/ShareVideo.tsx | 4 + .../app/s/[videoId]/_components/Sidebar.tsx | 40 ++-- .../app/s/[videoId]/_components/Toolbar.tsx | 5 +- apps/web/app/s/[videoId]/page.tsx | 28 ++- apps/web/lib/transcribe.ts | 15 ++ .../database/migrations/meta/_journal.json | 129 ++++++----- packages/database/schema.ts | 8 + 21 files changed, 404 insertions(+), 250 deletions(-) create mode 100644 apps/web/actions/organization/settings.ts diff --git a/apps/web/actions/organization/settings.ts b/apps/web/actions/organization/settings.ts new file mode 100644 index 0000000000..ff3fddb1f1 --- /dev/null +++ b/apps/web/actions/organization/settings.ts @@ -0,0 +1,41 @@ +"use server"; + +import { db } from "@cap/database"; +import { getCurrentUser } from "@cap/database/auth/session"; +import { organizations } from "@cap/database/schema"; +import { eq } from "drizzle-orm"; + +export async function updateOrganizationSettings(settings: { + disableSummary?: boolean; + disableCaptions?: boolean; + disableChapters?: boolean; + disableReactions?: boolean; + disableTranscript?: boolean; + disableComments?: boolean; +}) { + const user = await getCurrentUser(); + + if (!user) { + throw new Error("Unauthorized"); + } + + if (!settings) { + throw new Error("Settings are required"); + } + + const organization = await db() + .select() + .from(organizations) + .where(eq(organizations.id, user.activeOrganizationId)); + + if (!organization) { + throw new Error("Organization not found"); + } + + await db() + .update(organizations) + .set({ settings }) + .where(eq(organizations.id, user.activeOrganizationId)); + + return { success: true }; +} diff --git a/apps/web/actions/videos/settings.ts b/apps/web/actions/videos/settings.ts index bc5d361361..430b50f18a 100644 --- a/apps/web/actions/videos/settings.ts +++ b/apps/web/actions/videos/settings.ts @@ -8,7 +8,7 @@ import { eq } from "drizzle-orm"; export async function updateVideoSettings( videoId: Video.VideoId, - settings: { + videoSettings: { disableSummary?: boolean; disableCaptions?: boolean; disableChapters?: boolean; @@ -19,7 +19,7 @@ export async function updateVideoSettings( ) { const user = await getCurrentUser(); - if (!user || !videoId || !settings) { + if (!user || !videoId || !videoSettings) { throw new Error("Missing required data for updating video settings"); } @@ -36,7 +36,10 @@ export async function updateVideoSettings( throw new Error("You don't have permission to update this video settings"); } - await db().update(videos).set({ settings }).where(eq(videos.id, videoId)); + await db() + .update(videos) + .set({ settings: videoSettings }) + .where(eq(videos.id, videoId)); return { success: true }; } diff --git a/apps/web/app/(org)/dashboard/Contexts.tsx b/apps/web/app/(org)/dashboard/Contexts.tsx index 922117d905..1a9ae838f4 100644 --- a/apps/web/app/(org)/dashboard/Contexts.tsx +++ b/apps/web/app/(org)/dashboard/Contexts.tsx @@ -6,11 +6,17 @@ import Cookies from "js-cookie"; import { usePathname } from "next/navigation"; import { createContext, useContext, useEffect, useState } from "react"; import { UpgradeModal } from "@/components/UpgradeModal"; -import type { Organization, Spaces, UserPreferences } from "./dashboard-data"; +import type { + Organization, + OrganizationSettings, + Spaces, + UserPreferences, +} from "./dashboard-data"; type SharedContext = { organizationData: Organization[] | null; activeOrganization: Organization | null; + organizationSettings: OrganizationSettings | null; spacesData: Spaces[] | null; userSpaces: Spaces[] | null; sharedSpaces: Spaces[] | null; @@ -50,6 +56,7 @@ export function DashboardContexts({ spacesData, user, isSubscribed, + organizationSettings, userPreferences, anyNewNotifications, initialTheme, @@ -62,6 +69,7 @@ export function DashboardContexts({ spacesData: SharedContext["spacesData"]; user: SharedContext["user"]; isSubscribed: SharedContext["isSubscribed"]; + organizationSettings: SharedContext["organizationSettings"]; userPreferences: SharedContext["userPreferences"]; anyNewNotifications: boolean; initialTheme: ITheme; @@ -154,6 +162,7 @@ export function DashboardContexts({ spacesData, anyNewNotifications, userPreferences, + organizationSettings, userSpaces, sharedSpaces, activeSpace, diff --git a/apps/web/app/(org)/dashboard/_components/MobileTab.tsx b/apps/web/app/(org)/dashboard/_components/MobileTab.tsx index b4cd329016..b474227c5a 100644 --- a/apps/web/app/(org)/dashboard/_components/MobileTab.tsx +++ b/apps/web/app/(org)/dashboard/_components/MobileTab.tsx @@ -44,7 +44,7 @@ const MobileTab = () => { } }); return ( -
+
{open && } diff --git a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx index fbb37b7a43..1ba5ef76fb 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -336,17 +336,19 @@ export const CapCard = ({ "top-2 right-2 flex-col gap-2 z-[20]", )} > - { - e.stopPropagation(); - setIsSettingsDialogOpen(true); - }} - className="delay-0" - icon={() => { - return ; - }} - /> + {isOwner && ( + { + e.stopPropagation(); + setIsSettingsDialogOpen(true); + }} + className="delay-0" + icon={() => { + return ; + }} + /> + )} { diff --git a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx index 85c32604da..2b7479c048 100644 --- a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx @@ -12,23 +12,17 @@ import type { Video } from "@cap/web-domain"; import { faGear } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import clsx from "clsx"; -import { useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import { toast } from "sonner"; import { updateVideoSettings } from "@/actions/videos/settings"; import { useDashboardContext } from "../../Contexts"; +import type { OrganizationSettings } from "../../dashboard-data"; interface SettingsDialogProps { isOpen: boolean; onClose: () => void; capId: Video.VideoId; - settingsData?: { - disableComments?: boolean; - disableSummary?: boolean; - disableCaptions?: boolean; - disableChapters?: boolean; - disableReactions?: boolean; - disableTranscript?: boolean; - }; + settingsData?: OrganizationSettings; } const options = [ @@ -75,7 +69,7 @@ export const SettingsDialog = ({ }: SettingsDialogProps) => { const { user } = useDashboardContext(); const [saveLoading, setSaveLoading] = useState(false); - const [settings, setSettings] = useState({ + const [settings, setSettings] = useState({ disableComments: settingsData?.disableComments, disableSummary: settingsData?.disableSummary, disableCaptions: settingsData?.disableCaptions, @@ -101,6 +95,13 @@ export const SettingsDialog = ({ onClose(); }; + const toggleSettingHandler = useCallback((value: string) => { + setSettings((prev) => ({ + ...prev, + [value as keyof typeof settings]: !prev?.[value as keyof typeof settings], + })); + }, []); + return ( @@ -114,7 +115,7 @@ export const SettingsDialog = ({ {options.map((option) => (
toggleSettingHandler(option.value)} checked={settings?.[option.value as keyof typeof settings]} - onCheckedChange={(checked) => - setSettings({ - ...settings, - [option.value as keyof typeof settings]: checked, - }) - } />
))} diff --git a/apps/web/app/(org)/dashboard/dashboard-data.ts b/apps/web/app/(org)/dashboard/dashboard-data.ts index 7b8392c383..1afca61392 100644 --- a/apps/web/app/(org)/dashboard/dashboard-data.ts +++ b/apps/web/app/(org)/dashboard/dashboard-data.ts @@ -25,6 +25,9 @@ export type Organization = { totalInvites: number; }; +export type OrganizationSettings = + (typeof organizations.$inferSelect)["settings"]; + export type Spaces = Omit< typeof spaces.$inferSelect, "createdAt" | "updatedAt" @@ -40,6 +43,7 @@ export async function getDashboardData(user: typeof userSelectProps) { const organizationsWithMembers = await db() .select({ organization: organizations, + settings: organizations.settings, member: organizationMembers, user: { id: users.id, @@ -78,6 +82,7 @@ export async function getDashboardData(user: typeof userSelectProps) { let anyNewNotifications = false; let spacesData: Spaces[] = []; + let organizationSettings: OrganizationSettings = null; // Find active organization ID @@ -88,6 +93,7 @@ export async function getDashboardData(user: typeof userSelectProps) { if (!activeOrganizationId && organizationIds.length > 0) { activeOrganizationId = organizationIds[0]; } + // Only fetch spaces for the active organization if (activeOrganizationId) { @@ -105,6 +111,12 @@ export async function getDashboardData(user: typeof userSelectProps) { anyNewNotifications = !!notification; + const [organizationSetting] = await db() + .select({ settings: organizations.settings }) + .from(organizations) + .where(eq(organizations.id, activeOrganizationId)); + organizationSettings = organizationSetting?.settings || null; + spacesData = await db() .select({ id: spaces.id, @@ -261,6 +273,7 @@ export async function getDashboardData(user: typeof userSelectProps) { return { organizationSelect, + organizationSettings, spacesData, anyNewNotifications, userPreferences, @@ -272,6 +285,7 @@ export async function getDashboardData(user: typeof userSelectProps) { spacesData: [], anyNewNotifications: false, userPreferences: null, + organizationSettings: null, }; } } diff --git a/apps/web/app/(org)/dashboard/layout.tsx b/apps/web/app/(org)/dashboard/layout.tsx index 336837c4b5..d4aa909341 100644 --- a/apps/web/app/(org)/dashboard/layout.tsx +++ b/apps/web/app/(org)/dashboard/layout.tsx @@ -10,6 +10,7 @@ import { UploadingProvider } from "./caps/UploadingContext"; import { getDashboardData, type Organization, + type OrganizationSettings, type Spaces, type UserPreferences, } from "./dashboard-data"; @@ -32,18 +33,21 @@ export default async function DashboardLayout({ } let organizationSelect: Organization[] = []; + let organizationSettings: OrganizationSettings = null; let spacesData: Spaces[] = []; let anyNewNotifications = false; let userPreferences: UserPreferences; try { const dashboardData = await getDashboardData(user); organizationSelect = dashboardData.organizationSelect; + organizationSettings = dashboardData.organizationSettings; userPreferences = dashboardData.userPreferences?.preferences || null; spacesData = dashboardData.spacesData; anyNewNotifications = dashboardData.anyNewNotifications; } catch (error) { console.error("Failed to load dashboard data", error); organizationSelect = []; + organizationSettings = null; spacesData = []; anyNewNotifications = false; userPreferences = null; @@ -67,9 +71,12 @@ export default async function DashboardLayout({ const sidebar = (await cookies()).get("sidebarCollapsed")?.value ?? "false"; const referClicked = (await cookies()).get("referClicked")?.value ?? "false"; + console.log(organizationSettings, "organizationSettings"); + return ( {
+
+ +
+ { - const [activeTab, setActiveTab] = useState("Notifications"); + const { user, organizationSettings } = useDashboardContext(); + const [settings, setSettings] = useState( + organizationSettings || { + disableComments: false, + disableSummary: false, + disableCaptions: false, + disableChapters: false, + disableReactions: false, + disableTranscript: false, + }, + ); + + const isUserPro = userIsPro(user); + + const debouncedUpdateSettings = useDebounce(settings, 1000); + + const updateSettings = useCallback((newSettings: OrganizationSettings) => { + if (!newSettings) return; + try { + setSettings(newSettings); + updateOrganizationSettings(newSettings); + } catch (error) { + console.error("Error updating organization settings:", error); + toast.error("Failed to update settings"); + // Revert the local state on error + setSettings(organizationSettings); + } + }, []); + + useEffect(() => { + if (debouncedUpdateSettings !== organizationSettings) { + try { + updateSettings(debouncedUpdateSettings); + } catch (error) { + console.error("Error updating organization settings:", error); + toast.error("Failed to update settings"); + setSettings(organizationSettings); + } + } + }, [debouncedUpdateSettings, organizationSettings, updateSettings]); + + const handleToggle = (key: keyof OrganizationSettings) => { + setSettings((prev) => ({ + ...prev, + [key]: !prev?.[key], + })); + }; + return ( Cap Settings - Enable or disable specific settings for your organization. - Notifications, videos, etc... + Enable or disable specific settings for your organization. These + settings will be applied as defaults for new caps. -
-
-

- Coming Soon -

-
-
- {["Notifications", "Videos"].map((setting) => ( - setActiveTab(setting)} - className={clsx("relative cursor-pointer")} +
+ {options.map((option) => ( +
+
-

+

{option.label}

+ {option.pro && ( +

+ Pro +

)} - > - {setting} -

- {/** Indicator */} - {activeTab === setting && ( - - )} - - ))} -
-
- {activeTab === "Videos" ? ( - - {VideoTabSettings.map((setting, index) => ( - -

{setting.label}

- -
- ))} -
- ) : ( - - {NotificationTabSettings.map((setting, index) => ( - -

{setting.label}

- -
- ))} -
- )} -
+
+

{option.description}

+
+ { + handleToggle(option.value as keyof OrganizationSettings); + }} + checked={settings?.[option.value as keyof typeof settings]} + /> +
+ ))}
); diff --git a/apps/web/app/(org)/dashboard/settings/organization/page.tsx b/apps/web/app/(org)/dashboard/settings/organization/page.tsx index 48bed4f14e..b2c0e29f13 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/page.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/page.tsx @@ -4,7 +4,6 @@ import { organizationMembers, organizations } from "@cap/database/schema"; import { and, eq } from "drizzle-orm"; import type { Metadata } from "next"; import { redirect } from "next/navigation"; -import { getDashboardData } from "../../dashboard-data"; import { Organization } from "./Organization"; export const metadata: Metadata = { diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index db07de1b27..53435c06e2 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -17,6 +17,7 @@ import { getVideoStatus, type VideoStatusResult, } from "@/actions/videos/get-status"; +import type { OrganizationSettings } from "@/app/(org)/dashboard/dashboard-data"; import { ShareVideo } from "./_components/ShareVideo"; import { Sidebar } from "./_components/Sidebar"; import { Toolbar } from "./_components/Toolbar"; @@ -44,14 +45,7 @@ type VideoWithOrganizationInfo = typeof videos.$inferSelect & { sharedOrganizations?: { id: string; name: string }[]; hasPassword?: boolean; ownerIsPro?: boolean; - settings?: { - disableSummary?: boolean; - disableCaptions?: boolean; - disableChapters?: boolean; - disableReactions?: boolean; - disableTranscript?: boolean; - disableComments?: boolean; - }; + orgSettings?: OrganizationSettings | null; }; interface ShareProps { @@ -61,6 +55,7 @@ interface ShareProps { views: MaybePromise; customDomain: string | null; domainVerified: boolean; + videoSettings?: OrganizationSettings | null; userOrganizations?: { id: string; name: string }[]; initialAiData?: { title?: string | null; @@ -154,6 +149,7 @@ export const Share = ({ views, initialAiData, aiGenerationEnabled, + videoSettings, }: ShareProps) => { const effectiveDate: Date = data.metadata?.customCreatedAt ? new Date(data.metadata.customCreatedAt) @@ -279,6 +275,15 @@ export const Share = ({ }, 100); }, []); + const areChaptersDisabled = + videoSettings?.disableChapters ?? + data.orgSettings?.disableChapters ?? + false; + const areCaptionsDisabled = + videoSettings?.disableCaptions ?? + data.orgSettings?.disableCaptions ?? + false; + return (
@@ -289,8 +294,8 @@ export const Share = ({ data={{ ...data, transcriptionStatus }} user={user} comments={comments} - areChaptersDisabled={data.settings?.disableChapters} - areCaptionsDisabled={data.settings?.disableCaptions} + areChaptersDisabled={areChaptersDisabled} + areCaptionsDisabled={areCaptionsDisabled} chapters={aiData?.chapters || []} aiProcessing={aiData?.processing || false} ref={playerRef} @@ -314,6 +319,7 @@ export const Share = ({ createdAt: effectiveDate, transcriptionStatus, }} + videoSettings={videoSettings} user={user} commentsData={commentsData} setCommentsData={setCommentsData} @@ -321,7 +327,6 @@ export const Share = ({ setOptimisticComments={setOptimisticComments} handleCommentSuccess={handleCommentSuccess} views={views} - settings={data.settings} onSeek={handleSeek} videoId={data.id} aiData={aiData} diff --git a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx index 7422d5912b..d4de6808f4 100644 --- a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx +++ b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx @@ -8,6 +8,7 @@ import clsx from "clsx"; import { AnimatePresence, motion } from "framer-motion"; import { AlertTriangleIcon } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; +import type { OrganizationSettings } from "@/app/(org)/dashboard/dashboard-data"; import CommentStamp from "./CommentStamp"; import ProgressCircle, { useUploadProgress } from "./ProgressCircle"; import { @@ -35,6 +36,7 @@ interface Props { videoId: Video.VideoId; chaptersSrc: string; captionsSrc: string; + disableCaptions: boolean; videoRef: React.RefObject; mediaPlayerClassName?: string; autoplay?: boolean; @@ -55,6 +57,7 @@ export function CapVideoPlayer({ videoId, chaptersSrc, captionsSrc, + disableCaptions, videoRef, mediaPlayerClassName, autoplay = false, @@ -650,10 +653,12 @@ export function CapVideoPlayer({
- + {!disableCaptions && ( + + )} diff --git a/apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx b/apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx index a8dc133bec..6c1397f387 100644 --- a/apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx +++ b/apps/web/app/s/[videoId]/_components/HLSVideoPlayer.tsx @@ -37,6 +37,7 @@ interface Props { captionsSrc: string; videoRef: React.RefObject; mediaPlayerClassName?: string; + disableCaptions: boolean; autoplay?: boolean; hasActiveUpload?: boolean; } @@ -50,6 +51,7 @@ export function HLSVideoPlayer({ mediaPlayerClassName, autoplay = false, hasActiveUpload, + disableCaptions, }: Props) { const hlsInstance = useRef(null); const [currentCue, setCurrentCue] = useState(""); @@ -363,8 +365,15 @@ export function HLSVideoPlayer({ playsInline autoPlay={autoplay} > - - + {chaptersSrc && } + {captionsSrc && ( + + )} {currentCue && toggleCaptions && (
- + {!disableCaptions && ( + + )} diff --git a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx index 13306df35f..18f6bb360f 100644 --- a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx @@ -10,6 +10,7 @@ import { useRef, useState, } from "react"; +import type { OrganizationSettings } from "@/app/(org)/dashboard/dashboard-data"; import { UpgradeModal } from "@/components/UpgradeModal"; import { CapVideoPlayer } from "./CapVideoPlayer"; import { HLSVideoPlayer } from "./HLSVideoPlayer"; @@ -36,6 +37,7 @@ export const ShareVideo = forwardRef< data: typeof videos.$inferSelect & { ownerIsPro?: boolean; hasActiveUpload?: boolean; + orgSettings?: OrganizationSettings | null; }; user: typeof userSelectProps | null; comments: MaybePromise; @@ -176,6 +178,7 @@ export const ShareVideo = forwardRef< videoId={data.id} mediaPlayerClassName="w-full h-full max-w-full max-h-full rounded-xl" videoSrc={videoSrc} + disableCaptions={areCaptionsDisabled ?? false} chaptersSrc={areChaptersDisabled ? "" : chaptersUrl || ""} captionsSrc={areCaptionsDisabled ? "" : subtitleUrl || ""} videoRef={videoRef} @@ -195,6 +198,7 @@ export const ShareVideo = forwardRef< videoId={data.id} mediaPlayerClassName="w-full h-full max-w-full max-h-full rounded-xl" videoSrc={videoSrc} + disableCaptions={areCaptionsDisabled ?? false} chaptersSrc={areChaptersDisabled ? "" : chaptersUrl || ""} captionsSrc={areCaptionsDisabled ? "" : subtitleUrl || ""} videoRef={videoRef} diff --git a/apps/web/app/s/[videoId]/_components/Sidebar.tsx b/apps/web/app/s/[videoId]/_components/Sidebar.tsx index 590fced811..0f857de5b0 100644 --- a/apps/web/app/s/[videoId]/_components/Sidebar.tsx +++ b/apps/web/app/s/[videoId]/_components/Sidebar.tsx @@ -2,8 +2,10 @@ import type { userSelectProps } from "@cap/database/auth/session"; import type { comments as commentsSchema, videos } from "@cap/database/schema"; import { classNames } from "@cap/utils"; import type { Video } from "@cap/web-domain"; +import clsx from "clsx"; import { AnimatePresence, motion } from "framer-motion"; import { forwardRef, Suspense, useState } from "react"; +import type { OrganizationSettings } from "@/app/(org)/dashboard/dashboard-data"; import { Activity } from "./tabs/Activity"; import { Settings } from "./tabs/Settings"; import { Summary } from "./tabs/Summary"; @@ -18,6 +20,7 @@ type CommentType = typeof commentsSchema.$inferSelect & { type VideoWithOrganizationInfo = typeof videos.$inferSelect & { organizationMembers?: string[]; organizationId?: string; + orgSettings?: OrganizationSettings | null; }; interface SidebarProps { @@ -30,14 +33,7 @@ interface SidebarProps { setCommentsData: React.Dispatch>; views: MaybePromise; onSeek?: (time: number) => void; - settings?: { - disableSummary?: boolean; - disableCaptions?: boolean; - disableChapters?: boolean; - disableReactions?: boolean; - disableTranscript?: boolean; - disableComments?: boolean; - }; + videoSettings?: OrganizationSettings | null; videoId: Video.VideoId; aiData?: { title?: string | null; @@ -83,7 +79,7 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( handleCommentSuccess, setOptimisticComments, views, - settings, + videoSettings, onSeek, videoId, aiData, @@ -104,13 +100,21 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( { id: "activity", label: "Comments", - disabled: settings?.disableComments, + disabled: + videoSettings?.disableComments ?? data.orgSettings?.disableComments, + }, + { + id: "summary", + label: "Summary", + disabled: + videoSettings?.disableSummary ?? data.orgSettings?.disableSummary, }, - { id: "summary", label: "Summary", disabled: settings?.disableSummary }, { id: "transcript", label: "Transcript", - disabled: settings?.disableTranscript, + disabled: + videoSettings?.disableTranscript ?? + data.orgSettings?.disableTranscript, }, ]; @@ -155,7 +159,7 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( void }, SidebarProps>( } }; + const allTabsDisabled = tabs.every((tab) => tab.disabled); + return (
-
+
{tabs .filter((tab) => !tab.disabled) .map((tab) => (
{ handleToggle(option.value as keyof OrganizationSettings); }} diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index 53435c06e2..51d1698531 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -275,14 +275,17 @@ export const Share = ({ }, 100); }, []); - const areChaptersDisabled = - videoSettings?.disableChapters ?? - data.orgSettings?.disableChapters ?? - false; - const areCaptionsDisabled = - videoSettings?.disableCaptions ?? - data.orgSettings?.disableCaptions ?? - false; + const isDisabled = (setting: keyof NonNullable) => + videoSettings?.[setting] ?? data.orgSettings?.[setting] ?? false; + + const areChaptersDisabled = isDisabled("disableChapters"); + const areCaptionsDisabled = isDisabled("disableCaptions"); + const areCommentStampsDisabled = isDisabled("disableComments"); + const areReactionStampsDisabled = isDisabled("disableReactions"); + const allSettingsDisabled = + isDisabled("disableComments") && + isDisabled("disableSummary") && + isDisabled("disableTranscript"); return (
@@ -296,6 +299,8 @@ export const Share = ({ comments={comments} areChaptersDisabled={areChaptersDisabled} areCaptionsDisabled={areCaptionsDisabled} + areCommentStampsDisabled={areCommentStampsDisabled} + areReactionStampsDisabled={areReactionStampsDisabled} chapters={aiData?.chapters || []} aiProcessing={aiData?.processing || false} ref={playerRef} @@ -312,28 +317,30 @@ export const Share = ({
-
- -
+ {!allSettingsDisabled && ( +
+ +
+ )}
@@ -341,7 +348,10 @@ export const Share = ({ diff --git a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx index d4de6808f4..aaf7614a2d 100644 --- a/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx +++ b/apps/web/app/s/[videoId]/_components/CapVideoPlayer.tsx @@ -8,7 +8,6 @@ import clsx from "clsx"; import { AnimatePresence, motion } from "framer-motion"; import { AlertTriangleIcon } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; -import type { OrganizationSettings } from "@/app/(org)/dashboard/dashboard-data"; import CommentStamp from "./CommentStamp"; import ProgressCircle, { useUploadProgress } from "./ProgressCircle"; import { @@ -36,12 +35,14 @@ interface Props { videoId: Video.VideoId; chaptersSrc: string; captionsSrc: string; - disableCaptions: boolean; + disableCaptions?: boolean; videoRef: React.RefObject; mediaPlayerClassName?: string; autoplay?: boolean; enableCrossOrigin?: boolean; hasActiveUpload: boolean | undefined; + disableCommentStamps?: boolean; + disableReactionStamps?: boolean; comments?: Array<{ id: string; timestamp: number | null; @@ -64,6 +65,8 @@ export function CapVideoPlayer({ enableCrossOrigin = false, hasActiveUpload, comments = [], + disableCommentStamps = false, + disableReactionStamps = false, onSeek, }: Props) { const [currentCue, setCurrentCue] = useState(""); @@ -608,11 +611,17 @@ export function CapVideoPlayer({ {mainControlsVisible && markersReady && - comments - .filter( - (comment) => comment && comment.timestamp !== null && comment.id, - ) - .map((comment) => { + (() => { + const filteredComments = comments.filter( + (comment) => + comment && + comment.timestamp !== null && + comment.id && + !(disableCommentStamps && comment.type === "text") && + !(disableReactionStamps && comment.type === "emoji"), + ); + + return filteredComments.map((comment) => { const position = (Number(comment.timestamp) / duration) * 100; const containerPadding = 20; const availableWidth = `calc(100% - ${containerPadding * 2}px)`; @@ -629,7 +638,8 @@ export function CapVideoPlayer({ hoveredComment={hoveredComment} /> ); - })} + }); + })()} ; mediaPlayerClassName?: string; - disableCaptions: boolean; + disableCaptions?: boolean; autoplay?: boolean; hasActiveUpload?: boolean; } diff --git a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx index 18f6bb360f..0b8a6ef6c6 100644 --- a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx @@ -44,11 +44,21 @@ export const ShareVideo = forwardRef< chapters?: { title: string; start: number }[]; areChaptersDisabled?: boolean; areCaptionsDisabled?: boolean; + areCommentStampsDisabled?: boolean; + areReactionStampsDisabled?: boolean; aiProcessing?: boolean; } >( ( - { data, comments, chapters = [], areCaptionsDisabled, areChaptersDisabled }, + { + data, + comments, + chapters = [], + areCaptionsDisabled, + areChaptersDisabled, + areCommentStampsDisabled, + areReactionStampsDisabled, + }, ref, ) => { const videoRef = useRef(null); @@ -179,6 +189,8 @@ export const ShareVideo = forwardRef< mediaPlayerClassName="w-full h-full max-w-full max-h-full rounded-xl" videoSrc={videoSrc} disableCaptions={areCaptionsDisabled ?? false} + disableCommentStamps={areCommentStampsDisabled ?? false} + disableReactionStamps={areReactionStampsDisabled ?? false} chaptersSrc={areChaptersDisabled ? "" : chaptersUrl || ""} captionsSrc={areCaptionsDisabled ? "" : subtitleUrl || ""} videoRef={videoRef} diff --git a/apps/web/app/s/[videoId]/_components/Sidebar.tsx b/apps/web/app/s/[videoId]/_components/Sidebar.tsx index 0f857de5b0..b3e15dac6c 100644 --- a/apps/web/app/s/[videoId]/_components/Sidebar.tsx +++ b/apps/web/app/s/[videoId]/_components/Sidebar.tsx @@ -93,7 +93,20 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( data.organizationMembers?.includes(user?.id ?? "")), ); - const [activeTab, setActiveTab] = useState("activity"); + const defaultTab = !( + videoSettings?.disableComments ?? data.orgSettings?.disableComments + ) + ? "activity" + : !(videoSettings?.disableSummary ?? data.orgSettings?.disableSummary) + ? "summary" + : !( + videoSettings?.disableTranscript ?? + data.orgSettings?.disableTranscript + ) + ? "transcript" + : "activity"; + + const [activeTab, setActiveTab] = useState(defaultTab); const [[page, direction], setPage] = useState([0, 0]); const tabs = [ @@ -143,6 +156,11 @@ export const Sidebar = forwardRef<{ scrollToBottom: () => void }, SidebarProps>( ref={ref} views={views} comments={commentsData} + commentsDisabled={ + videoSettings?.disableComments ?? + data.orgSettings?.disableComments ?? + false + } setComments={setCommentsData} user={user} optimisticComments={optimisticComments} diff --git a/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx b/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx index 03153a3b30..e636bb2de3 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Activity/Comments.tsx @@ -1,6 +1,8 @@ import type { userSelectProps } from "@cap/database/auth/session"; import { Button } from "@cap/ui"; import type { Video } from "@cap/web-domain"; +import { faCommentSlash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useSearchParams } from "next/navigation"; import type React from "react"; import { @@ -33,6 +35,7 @@ export const Comments = Object.assign( handleCommentSuccess: (comment: CommentType) => void; onSeek?: (time: number) => void; setShowAuthOverlay: (v: boolean) => void; + commentsDisabled: boolean; } >((props, ref) => { const { @@ -41,6 +44,7 @@ export const Comments = Object.assign( setComments, handleCommentSuccess, onSeek, + commentsDisabled, } = props; const commentParams = useSearchParams().get("comment"); const replyParams = useSearchParams().get("reply"); @@ -184,12 +188,22 @@ export const Comments = Object.assign( return ( - {rootComments.length === 0 ? ( + {commentsDisabled ? ( +
+ } + commentsDisabled={commentsDisabled} + /> +
+ ) : rootComments.length === 0 ? ( ) : (
@@ -238,24 +252,26 @@ export const Comments = Object.assign( {props.children}
-
- {props.user ? ( - - ) : ( - - )} -
+ {!props.commentInputProps?.disabled && ( +
+ {props.user ? ( + + ) : ( + + )} +
+ )} ), Skeleton: (props: { diff --git a/apps/web/app/s/[videoId]/_components/tabs/Activity/EmptyState.tsx b/apps/web/app/s/[videoId]/_components/tabs/Activity/EmptyState.tsx index 50d4452e20..4d1b093594 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Activity/EmptyState.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Activity/EmptyState.tsx @@ -1,12 +1,30 @@ import { LoadingSpinner } from "@cap/ui"; +import type { FontAwesomeIconProps } from "@fortawesome/react-fontawesome"; +import type { ReactElement } from "react"; +import React from "react"; -const EmptyState = () => ( +const EmptyState = ({ + commentsDisabled, + icon, +}: { + commentsDisabled?: boolean; + icon?: ReactElement; +}) => (
-
- -

No comments yet

+ {!commentsDisabled && } + {icon && ( +
+ {React.cloneElement(icon, { className: "text-gray-12 size-8" })} +
+ )} +
+

+ {commentsDisabled ? "Disabled" : "No comments yet"} +

- Be the first to share your thoughts! + {commentsDisabled + ? "Comments are disabled for this video" + : "Be the first to share your thoughts!"}

diff --git a/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx b/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx index ef7c6c3d57..c9cce9d037 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Activity/index.tsx @@ -3,13 +3,7 @@ import type { userSelectProps } from "@cap/database/auth/session"; import type { Video } from "@cap/web-domain"; import type React from "react"; -import { - forwardRef, - type JSX, - type RefObject, - Suspense, - useState, -} from "react"; +import { forwardRef, type JSX, Suspense, useState } from "react"; import { CapCardAnalytics } from "@/app/(org)/dashboard/caps/components/CapCard/CapCardAnalytics"; import type { CommentType } from "../../../Share"; import { AuthOverlay } from "../../AuthOverlay"; @@ -27,6 +21,7 @@ interface ActivityProps { optimisticComments: CommentType[]; setOptimisticComments: (newComment: CommentType) => void; isOwnerOrMember: boolean; + commentsDisabled: boolean; } export const Activity = Object.assign( @@ -41,6 +36,7 @@ export const Activity = Object.assign( optimisticComments, setOptimisticComments, setComments, + commentsDisabled, ...props }, ref, @@ -71,6 +67,7 @@ export const Activity = Object.assign( videoId={videoId} setShowAuthOverlay={setShowAuthOverlay} onSeek={props.onSeek} + commentsDisabled={commentsDisabled} /> )} diff --git a/apps/web/app/s/[videoId]/_components/tabs/Summary.tsx b/apps/web/app/s/[videoId]/_components/tabs/Summary.tsx index a3f5056e28..8dc2bc6ec4 100644 --- a/apps/web/app/s/[videoId]/_components/tabs/Summary.tsx +++ b/apps/web/app/s/[videoId]/_components/tabs/Summary.tsx @@ -3,6 +3,8 @@ import type { userSelectProps } from "@cap/database/auth/session"; import { Button } from "@cap/ui"; import { userIsPro } from "@cap/utils"; +import { faRectangleList } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useEffect, useState } from "react"; interface Chapter { @@ -156,22 +158,12 @@ export const Summary: React.FC = ({ if (!aiData?.summary && (!aiData?.chapters || aiData.chapters.length === 0)) { return (
-
- - - -

+ +
+

No summary available

diff --git a/apps/web/lib/transcribe.ts b/apps/web/lib/transcribe.ts index d73fe79781..38777a0112 100644 --- a/apps/web/lib/transcribe.ts +++ b/apps/web/lib/transcribe.ts @@ -1,11 +1,11 @@ import { db } from "@cap/database"; -import { s3Buckets, videos } from "@cap/database/schema"; +import { organizations, s3Buckets, videos } from "@cap/database/schema"; import { serverEnv } from "@cap/env"; import { S3Buckets } from "@cap/web-backend"; import type { Video } from "@cap/web-domain"; import { createClient } from "@deepgram/sdk"; import { eq } from "drizzle-orm"; -import { Effect, Option } from "effect"; +import { Option } from "effect"; import { generateAiMetadata } from "@/actions/videos/generate-ai-metadata"; import { runPromise } from "./server"; @@ -38,9 +38,12 @@ export async function transcribeVideo( .select({ video: videos, bucket: s3Buckets, + settings: videos.settings, + orgSettings: organizations.settings, }) .from(videos) .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id)) + .leftJoin(organizations, eq(videos.orgId, organizations.id)) .where(eq(videos.id, videoId)); if (query.length === 0) { @@ -58,6 +61,24 @@ export async function transcribeVideo( return { success: false, message: "Video information is missing" }; } + if ( + video.settings?.disableTranscript ?? + result.orgSettings?.disableTranscript ?? + false + ) { + console.log( + `[transcribeVideo] Transcription disabled for video ${videoId}`, + ); + await db() + .update(videos) + .set({ transcriptionStatus: "ERROR" }) + .where(eq(videos.id, videoId)); + return { + success: true, + message: "Transcription disabled for video - skipping transcription", + }; + } + if ( video.transcriptionStatus === "COMPLETE" || video.transcriptionStatus === "PROCESSING" From 2e7523d7b996d6c16c96bd59a53bc0ff333e3f79 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:57:38 +0300 Subject: [PATCH 09/21] text --- .../web/app/(org)/dashboard/caps/components/SettingsDialog.tsx | 3 +-- .../settings/organization/components/CapSettingsCard.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx index dd63facb05..50506c968b 100644 --- a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx @@ -56,8 +56,7 @@ const options = [ { label: "Disable transcript", value: "disableTranscript", - description: - "Remove the transcript for this cap, this also disables chapters and summary", + description: "This also disables chapters and summary", pro: true, }, ]; diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx index 67e6083319..f904d64493 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx @@ -41,8 +41,7 @@ const options = [ { label: "Disable transcript", value: "disableTranscript", - description: - "Remove the transcript for caps, this also disables chapters and summary", + description: "This also disables chapters and summary", pro: true, }, ]; From 339548cfb9639739596b6bd33ee13aeb653e5593 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:32:00 +0300 Subject: [PATCH 10/21] show download button for non-owners --- .../dashboard/caps/components/CapCard/CapCard.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx index afc54c689b..ab869baff6 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -336,7 +336,7 @@ export const CapCard = ({ "top-2 right-2 flex-col gap-2 z-[20]", )} > - {isOwner && ( + {isOwner ? ( { @@ -348,6 +348,18 @@ export const CapCard = ({ return ; }} /> + ) : ( + { + e.stopPropagation(); + handleDownload(); + }} + className="delay-0" + icon={() => ( + + )} + /> )} Date: Tue, 7 Oct 2025 10:25:39 +0300 Subject: [PATCH 11/21] skipped transcription status and conditionally render chapters and summary --- apps/web/actions/videos/get-status.ts | 15 ++-- apps/web/app/s/[videoId]/Share.tsx | 56 +++----------- .../[videoId]/_components/SummaryChapters.tsx | 73 +++++++++++++++++++ .../_components/utils/transcript-utils.ts | 14 ++++ apps/web/lib/transcribe.ts | 21 ++++-- packages/database/schema.ts | 2 +- packages/web-api-contract-effect/src/index.ts | 8 +- packages/web-domain/src/Video.ts | 2 +- 8 files changed, 124 insertions(+), 67 deletions(-) create mode 100644 apps/web/app/s/[videoId]/_components/SummaryChapters.tsx diff --git a/apps/web/actions/videos/get-status.ts b/apps/web/actions/videos/get-status.ts index eb4a0d8cd2..93d2daacd4 100644 --- a/apps/web/actions/videos/get-status.ts +++ b/apps/web/actions/videos/get-status.ts @@ -14,8 +14,10 @@ import { generateAiMetadata } from "./generate-ai-metadata"; const MAX_AI_PROCESSING_TIME = 10 * 60 * 1000; +type TranscriptionStatus = "PROCESSING" | "COMPLETE" | "ERROR" | "SKIPPED"; + export interface VideoStatusResult { - transcriptionStatus: "PROCESSING" | "COMPLETE" | "ERROR" | null; + transcriptionStatus: TranscriptionStatus | null; aiTitle: string | null; aiProcessing: boolean; summary: string | null; @@ -124,10 +126,7 @@ export async function getVideoStatus( return { transcriptionStatus: - (updatedVideo.transcriptionStatus as - | "PROCESSING" - | "COMPLETE" - | "ERROR") || null, + (updatedVideo.transcriptionStatus as TranscriptionStatus) || null, aiProcessing: false, aiTitle: updatedMetadata.aiTitle || null, summary: updatedMetadata.summary || null, @@ -214,8 +213,7 @@ export async function getVideoStatus( return { transcriptionStatus: - (video.transcriptionStatus as "PROCESSING" | "COMPLETE" | "ERROR") || - null, + (video.transcriptionStatus as TranscriptionStatus) || null, aiProcessing: true, aiTitle: metadata.aiTitle || null, summary: metadata.summary || null, @@ -232,8 +230,7 @@ export async function getVideoStatus( return { transcriptionStatus: - (video.transcriptionStatus as "PROCESSING" | "COMPLETE" | "ERROR") || - null, + (video.transcriptionStatus as TranscriptionStatus) || null, aiProcessing: metadata.aiProcessing || false, aiTitle: metadata.aiTitle || null, summary: metadata.summary || null, diff --git a/apps/web/app/s/[videoId]/Share.tsx b/apps/web/app/s/[videoId]/Share.tsx index 51d1698531..fb28603fc0 100644 --- a/apps/web/app/s/[videoId]/Share.tsx +++ b/apps/web/app/s/[videoId]/Share.tsx @@ -20,16 +20,9 @@ import { import type { OrganizationSettings } from "@/app/(org)/dashboard/dashboard-data"; import { ShareVideo } from "./_components/ShareVideo"; import { Sidebar } from "./_components/Sidebar"; +import SummaryChapters from "./_components/SummaryChapters"; import { Toolbar } from "./_components/Toolbar"; -const formatTime = (time: number) => { - const minutes = Math.floor(time / 60); - const seconds = Math.floor(time % 60); - return `${minutes.toString().padStart(2, "0")}:${seconds - .toString() - .padStart(2, "0")}`; -}; - type CommentWithAuthor = typeof commentsSchema.$inferSelect & { authorName: string | null; }; @@ -279,6 +272,7 @@ export const Share = ({ videoSettings?.[setting] ?? data.orgSettings?.[setting] ?? false; const areChaptersDisabled = isDisabled("disableChapters"); + const isSummaryDisabled = isDisabled("disableSummary"); const areCaptionsDisabled = isDisabled("disableCaptions"); const areCommentStampsDisabled = isDisabled("disableComments"); const areReactionStampsDisabled = isDisabled("disableReactions"); @@ -391,45 +385,13 @@ export const Share = ({

)} - {!aiLoading && - (aiData?.summary || - (aiData?.chapters && aiData.chapters.length > 0)) && ( -
- {aiData?.summary && ( - <> -

Summary

-
- - Generated by Cap AI - -
-

- {aiData.summary} -

- - )} - - {aiData?.chapters && aiData.chapters.length > 0 && ( -
-

Chapters

-
- {aiData.chapters.map((chapter) => ( -
handleSeek(chapter.start)} - > - - {formatTime(chapter.start)} - - {chapter.title} -
- ))} -
-
- )} -
- )} +

); diff --git a/apps/web/app/s/[videoId]/_components/SummaryChapters.tsx b/apps/web/app/s/[videoId]/_components/SummaryChapters.tsx new file mode 100644 index 0000000000..bf7673c049 --- /dev/null +++ b/apps/web/app/s/[videoId]/_components/SummaryChapters.tsx @@ -0,0 +1,73 @@ +import { formatTimeMinutes } from "./utils/transcript-utils"; + +interface SummaryChaptersProps { + isSummaryDisabled: boolean; + areChaptersDisabled: boolean; + handleSeek: (time: number) => void; + aiData: { + title: string | null; + summary: string | null; + chapters: + | { + title: string; + start: number; + }[] + | null; + processing: boolean; + }; + aiLoading: boolean; +} + +const SummaryChapters = ({ + isSummaryDisabled, + areChaptersDisabled, + handleSeek, + aiData, + aiLoading, +}: SummaryChaptersProps) => { + const hasSummary = !isSummaryDisabled && !!aiData?.summary; + const hasChapters = + !areChaptersDisabled && + Array.isArray(aiData?.chapters) && + aiData.chapters.length > 0; + + if (aiLoading || (!hasSummary && !hasChapters)) return null; + + return ( +
+ {hasSummary && ( + <> +

Summary

+
+ + Generated by Cap AI + +
+

{aiData.summary}

+ + )} + + {hasChapters && ( +
+

Chapters

+
+ {aiData.chapters?.map((chapter) => ( +
handleSeek(chapter.start)} + > + + {formatTimeMinutes(chapter.start)} + + {chapter.title} +
+ ))} +
+
+ )} +
+ ); +}; + +export default SummaryChapters; diff --git a/apps/web/app/s/[videoId]/_components/utils/transcript-utils.ts b/apps/web/app/s/[videoId]/_components/utils/transcript-utils.ts index dba8952c58..b5331b9400 100644 --- a/apps/web/app/s/[videoId]/_components/utils/transcript-utils.ts +++ b/apps/web/app/s/[videoId]/_components/utils/transcript-utils.ts @@ -20,6 +20,20 @@ export const formatTime = (seconds: number): string => { .padStart(3, "0")}`; }; +/** + * Formats time in seconds to minutes:seconds format + * @param time - Time in seconds + * @returns Time in minutes:seconds format + */ + +export const formatTimeMinutes = (time: number) => { + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${minutes.toString().padStart(2, "0")}:${seconds + .toString() + .padStart(2, "0")}`; +}; + /** * Formats transcript entries as VTT format for subtitles */ diff --git a/apps/web/lib/transcribe.ts b/apps/web/lib/transcribe.ts index 38777a0112..0a3be8cfae 100644 --- a/apps/web/lib/transcribe.ts +++ b/apps/web/lib/transcribe.ts @@ -63,19 +63,26 @@ export async function transcribeVideo( if ( video.settings?.disableTranscript ?? - result.orgSettings?.disableTranscript ?? - false + result.orgSettings?.disableTranscript ) { console.log( `[transcribeVideo] Transcription disabled for video ${videoId}`, ); - await db() - .update(videos) - .set({ transcriptionStatus: "ERROR" }) - .where(eq(videos.id, videoId)); + try { + await db() + .update(videos) + .set({ transcriptionStatus: "SKIPPED" }) + .where(eq(videos.id, videoId)); + } catch (err) { + console.error(`[transcribeVideo] Failed to mark as skipped:`, err); + return { + success: false, + message: "Transcription disabled, but failed to update status", + }; + } return { success: true, - message: "Transcription disabled for video - skipping transcription", + message: "Transcription disabled for video — skipping transcription", }; } diff --git a/packages/database/schema.ts b/packages/database/schema.ts index 8ac3534257..73c0eb8492 100644 --- a/packages/database/schema.ts +++ b/packages/database/schema.ts @@ -275,7 +275,7 @@ export const videos = mysqlTable( disableComments?: boolean; }>(), transcriptionStatus: varchar("transcriptionStatus", { length: 255 }).$type< - "PROCESSING" | "COMPLETE" | "ERROR" + "PROCESSING" | "COMPLETE" | "ERROR" | "SKIPPED" >(), source: json("source") .$type< diff --git a/packages/web-api-contract-effect/src/index.ts b/packages/web-api-contract-effect/src/index.ts index c5b583daa3..34a896bca1 100644 --- a/packages/web-api-contract-effect/src/index.ts +++ b/packages/web-api-contract-effect/src/index.ts @@ -4,12 +4,16 @@ import { HttpApiError, HttpApiGroup, HttpApiMiddleware, - HttpServerError, } from "@effect/platform"; import { Context, Data } from "effect"; import * as Schema from "effect/Schema"; -const TranscriptionStatus = Schema.Literal("PROCESSING", "COMPLETE", "ERROR"); +const TranscriptionStatus = Schema.Literal( + "PROCESSING", + "COMPLETE", + "ERROR", + "SKIPPED", +); const OSType = Schema.Literal("macos", "windows"); const LicenseType = Schema.Literal("yearly", "lifetime"); diff --git a/packages/web-domain/src/Video.ts b/packages/web-domain/src/Video.ts index 3480f8cdbf..8ea8c66b65 100644 --- a/packages/web-domain/src/Video.ts +++ b/packages/web-domain/src/Video.ts @@ -26,7 +26,7 @@ export class Video extends Schema.Class
@@ -187,11 +187,7 @@ export const SettingsDialog = ({ )) } onCheckedChange={() => toggleSettingHandler(option.value)} - checked={ - // If org disabled (showing "Enable X"), switch shows if enabled (!effectiveValue) - // If org enabled (showing "Disable X"), switch shows if disabled (effectiveValue) - orgValue ? !effectiveValue : effectiveValue - } + checked={!effectiveValue} />
); diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx index f904d64493..0f07ad51fd 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx @@ -14,7 +14,7 @@ const options = [ { label: "Disable comments", value: "disableComments", - description: "Prevent viewers from commenting on caps", + description: "Allow viewers to comment on caps", }, { label: "Disable summary", @@ -25,7 +25,7 @@ const options = [ { label: "Disable captions", value: "disableCaptions", - description: "Prevent viewers from using captions for caps", + description: "Allow viewers to use captions for caps", }, { label: "Disable chapters", @@ -36,12 +36,12 @@ const options = [ { label: "Disable reactions", value: "disableReactions", - description: "Prevent viewers from reacting to caps", + description: "Allow viewers to react to caps", }, { label: "Disable transcript", value: "disableTranscript", - description: "This also disables chapters and summary", + description: "This also allows chapters and summary", pro: true, }, ]; @@ -100,7 +100,7 @@ const CapSettingsCard = () => { // Inline the update logic to avoid circular dependency try { - await updateOrganizationSettings(debouncedUpdateSettings); + updateOrganizationSettings(debouncedUpdateSettings); // Show a toast for each changed setting changedKeys.forEach((changedKey) => { @@ -172,7 +172,9 @@ const CapSettingsCard = () => { className={clsx("flex flex-col flex-1", option.pro && "gap-1")} >
-

{option.label}

+

+ {option.label.replace("Disable", "Enable")} +

{option.pro && (

Pro @@ -192,7 +194,7 @@ const CapSettingsCard = () => { onCheckedChange={() => { handleToggle(option.value as keyof OrganizationSettings); }} - checked={settings?.[option.value as keyof typeof settings]} + checked={!settings?.[option.value as keyof typeof settings]} />

))} diff --git a/apps/web/app/embed/[videoId]/page.tsx b/apps/web/app/embed/[videoId]/page.tsx index ff12bcb7b8..04a5550a62 100644 --- a/apps/web/app/embed/[videoId]/page.tsx +++ b/apps/web/app/embed/[videoId]/page.tsx @@ -123,6 +123,7 @@ export default async function EmbedVideoPage( name: videos.name, ownerId: videos.ownerId, orgId: videos.orgId, + settings: videos.settings, createdAt: videos.createdAt, updatedAt: videos.updatedAt, bucket: videos.bucket, @@ -182,7 +183,7 @@ export default async function EmbedVideoPage( Effect.catchTags({ PolicyDenied: () => Effect.succeed( -
+

This video is private

If you own this video, please sign in{" "} @@ -237,7 +238,7 @@ async function EmbedContent({ !user.email.endsWith(`@${organization[0].allowedEmailDomain}`) ) { return ( -

+

Access Restricted

This video is only accessible to members of this organization. @@ -284,7 +285,7 @@ async function EmbedContent({ if (video.isScreenshot === true) { return ( -

+

Screenshots cannot be embedded

); From e319cb779f110ecb5e9bccb784d4c3401da9ca28 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:09:05 +0300 Subject: [PATCH 15/21] review points --- apps/web/actions/organization/settings.ts | 2 +- .../organization/components/CapSettingsCard.tsx | 13 ++++++++++--- .../[videoId]/_components/utils/transcript-utils.ts | 6 ------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/web/actions/organization/settings.ts b/apps/web/actions/organization/settings.ts index 8efb262f50..212b712fb1 100644 --- a/apps/web/actions/organization/settings.ts +++ b/apps/web/actions/organization/settings.ts @@ -24,7 +24,7 @@ export async function updateOrganizationSettings(settings: { throw new Error("Settings are required"); } - const organization = await db() + const [organization] = await db() .select() .from(organizations) .where(eq(organizations.id, user.activeOrganizationId)); diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx index 0f07ad51fd..13dfee4a1f 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx @@ -70,9 +70,16 @@ const CapSettingsCard = () => { // Update lastSavedSettings when organizationSettings changes externally useEffect(() => { - if (organizationSettings) { - lastSavedSettings.current = organizationSettings; - } + const next = organizationSettings ?? { + disableComments: false, + disableSummary: false, + disableCaptions: false, + disableChapters: false, + disableReactions: false, + disableTranscript: false, + }; + setSettings(next); + lastSavedSettings.current = next; }, [organizationSettings]); useEffect(() => { diff --git a/apps/web/app/s/[videoId]/_components/utils/transcript-utils.ts b/apps/web/app/s/[videoId]/_components/utils/transcript-utils.ts index b5331b9400..d24b1cd5b3 100644 --- a/apps/web/app/s/[videoId]/_components/utils/transcript-utils.ts +++ b/apps/web/app/s/[videoId]/_components/utils/transcript-utils.ts @@ -20,12 +20,6 @@ export const formatTime = (seconds: number): string => { .padStart(3, "0")}`; }; -/** - * Formats time in seconds to minutes:seconds format - * @param time - Time in seconds - * @returns Time in minutes:seconds format - */ - export const formatTimeMinutes = (time: number) => { const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); From a4429f70f28aff4513b41d60debeccec5fa3abd2 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:20:02 +0300 Subject: [PATCH 16/21] cleanup --- .../caps/components/CapCard/CapCard.tsx | 46 ------------------- .../caps/components/SettingsDialog.tsx | 20 +++++--- .../web/app/(org)/dashboard/dashboard-data.ts | 7 +-- .../components/CapSettingsCard.tsx | 12 +---- 4 files changed, 19 insertions(+), 66 deletions(-) diff --git a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx index 4957eb1d3f..8191e4fd97 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -407,52 +407,6 @@ export const CapCard = ({ ); }} /> - {!isOwner && ( - { - e.stopPropagation(); - handleDownload(); - }} - disabled={ - downloadMutation.isPending || - (enableBetaUploadProgress && cap.hasActiveUpload) - } - className="delay-25" - icon={() => { - return downloadMutation.isPending ? ( -
- -
- ) : ( - - ); - }} - /> - )} {isOwner && ( diff --git a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx index b52eadd05c..af37bfff16 100644 --- a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx @@ -25,7 +25,12 @@ interface SettingsDialogProps { settingsData?: OrganizationSettings; } -const options = [ +const options: { + label: string; + value: keyof NonNullable; + description: string; + pro?: boolean; +}[] = [ { label: "Disable comments", value: "disableComments", @@ -56,7 +61,7 @@ const options = [ { label: "Disable transcript", value: "disableTranscript", - description: "This also allows chapters and summary", + description: "Required for summary and chapters", pro: true, }, ]; @@ -81,18 +86,21 @@ export const SettingsDialog = ({ const isUserPro = userIsPro(user); const saveHandler = async () => { - setSaveLoading(true); - if (!settings) return; try { - await updateVideoSettings(capId, settings); + setSaveLoading(true); + if (!settings) return; + const payload = Object.fromEntries( + Object.entries(settings).filter(([, v]) => v !== undefined), + ) as Partial; + await updateVideoSettings(capId, payload); toast.success("Settings updated successfully"); + onClose(); } catch (error) { console.error("Error updating video settings:", error); toast.error("Failed to update settings"); } finally { setSaveLoading(false); } - onClose(); }; const toggleSettingHandler = useCallback( diff --git a/apps/web/app/(org)/dashboard/dashboard-data.ts b/apps/web/app/(org)/dashboard/dashboard-data.ts index 0e46c6d694..b41b75741c 100644 --- a/apps/web/app/(org)/dashboard/dashboard-data.ts +++ b/apps/web/app/(org)/dashboard/dashboard-data.ts @@ -25,8 +25,9 @@ export type Organization = { totalInvites: number; }; -export type OrganizationSettings = - (typeof organizations.$inferSelect)["settings"]; +export type OrganizationSettings = NonNullable< + (typeof organizations.$inferSelect)["settings"] +>; export type Spaces = Omit< typeof spaces.$inferSelect, @@ -83,7 +84,7 @@ export async function getDashboardData(user: typeof userSelectProps) { let anyNewNotifications = false; let spacesData: Spaces[] = []; - let organizationSettings: OrganizationSettings = null; + let organizationSettings: OrganizationSettings | null = null; // Find active organization ID diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx index 13dfee4a1f..f245dc8142 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx @@ -59,7 +59,6 @@ const CapSettingsCard = () => { }, ); - // Track the last saved settings to compare against const lastSavedSettings = useRef( organizationSettings || settings, ); @@ -68,7 +67,6 @@ const CapSettingsCard = () => { const debouncedUpdateSettings = useDebounce(settings, 1000); - // Update lastSavedSettings when organizationSettings changes externally useEffect(() => { const next = organizationSettings ?? { disableComments: false, @@ -88,7 +86,6 @@ const CapSettingsCard = () => { debouncedUpdateSettings !== lastSavedSettings.current ) { const handleUpdate = async () => { - // Find ALL settings that changed const changedKeys: Array = []; for (const key of Object.keys(debouncedUpdateSettings) as Array< keyof OrganizationSettings @@ -100,16 +97,13 @@ const CapSettingsCard = () => { } } - // Guard: if no actual changes, do nothing (prevents loops) if (changedKeys.length === 0) { return; } - // Inline the update logic to avoid circular dependency try { - updateOrganizationSettings(debouncedUpdateSettings); + await updateOrganizationSettings(debouncedUpdateSettings); - // Show a toast for each changed setting changedKeys.forEach((changedKey) => { const option = options.find((opt) => opt.value === changedKey); const isDisabled = debouncedUpdateSettings[changedKey]; @@ -122,12 +116,10 @@ const CapSettingsCard = () => { ); }); - // Update the last saved settings reference lastSavedSettings.current = debouncedUpdateSettings; } catch (error) { console.error("Error updating organization settings:", error); toast.error("Failed to update settings"); - // Revert the local state on error if (organizationSettings) { setSettings(organizationSettings); } @@ -142,7 +134,6 @@ const CapSettingsCard = () => { setSettings((prev) => { const newValue = !prev?.[key]; - // If disabling transcript, also disable summary and chapters since they depend on it if (key === "disableTranscript" && newValue === true) { return { ...prev, @@ -193,7 +184,6 @@ const CapSettingsCard = () => { Date: Tue, 7 Oct 2025 12:28:21 +0300 Subject: [PATCH 17/21] better messaging --- .../caps/components/SettingsDialog.tsx | 32 +++++++------------ apps/web/app/(org)/dashboard/layout.tsx | 2 +- .../components/CapSettingsCard.tsx | 22 ++++++------- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx index af37bfff16..90125eb670 100644 --- a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx @@ -32,36 +32,36 @@ const options: { pro?: boolean; }[] = [ { - label: "Disable comments", + label: "Enable comments", value: "disableComments", description: "Allow viewers to comment on this cap", }, { - label: "Disable summary", + label: "Enable summary", value: "disableSummary", - description: "Remove the summary for this cap (requires transcript)", + description: "Show AI-generated summary (requires transcript)", pro: true, }, { - label: "Disable captions", + label: "Enable captions", value: "disableCaptions", description: "Allow viewers to use captions for this cap", }, { - label: "Disable chapters", + label: "Enable chapters", value: "disableChapters", - description: "Remove the chapters for this cap (requires transcript)", + description: "Show AI-generated chapters (requires transcript)", pro: true, }, { - label: "Disable reactions", + label: "Enable reactions", value: "disableReactions", description: "Allow viewers to react to this cap", }, { - label: "Disable transcript", + label: "Enable transcript", value: "disableTranscript", - description: "Required for summary and chapters", + description: "Enabling this also allows generating summary and chapters", pro: true, }, ]; @@ -86,9 +86,9 @@ export const SettingsDialog = ({ const isUserPro = userIsPro(user); const saveHandler = async () => { + if (!settings) return; + setSaveLoading(true); try { - setSaveLoading(true); - if (!settings) return; const payload = Object.fromEntries( Object.entries(settings).filter(([, v]) => v !== undefined), ) as Partial; @@ -110,12 +110,8 @@ export const SettingsDialog = ({ const currentValue = prev?.[key]; const orgValue = organizationSettings?.[key] ?? false; - // If using org default, set to opposite of org value - // If org disabled it (true), enabling means setting to false - // If org enabled it (false), disabling means setting to true const newValue = currentValue === undefined ? !orgValue : !currentValue; - // If disabling transcript, also disable summary and chapters since they depend on it if (key === "disableTranscript" && newValue === true) { return { ...prev, @@ -134,7 +130,6 @@ export const SettingsDialog = ({ [organizationSettings], ); - // Helper to get the effective value (considering org defaults) const getEffectiveValue = (key: keyof OrganizationSettings) => { const videoValue = settings?.[key]; const orgValue = organizationSettings?.[key] ?? false; @@ -169,9 +164,7 @@ export const SettingsDialog = ({ )} >
-

- {option.label.replace("Disable", "Enable")} -

+

{option.label}

{option.pro && (

Pro @@ -188,7 +181,6 @@ export const SettingsDialog = ({ { className={clsx("flex flex-col flex-1", option.pro && "gap-1")} >

-

- {option.label.replace("Disable", "Enable")} -

+

{option.label}

{option.pro && (

Pro From f6614dde299c99c815da102f09bc3340207b29e3 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:30:53 +0300 Subject: [PATCH 18/21] description abit too long --- apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx | 2 +- .../settings/organization/components/CapSettingsCard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx index 90125eb670..53a406c785 100644 --- a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx @@ -61,7 +61,7 @@ const options: { { label: "Enable transcript", value: "disableTranscript", - description: "Enabling this also allows generating summary and chapters", + description: "Enabling this also allows summary and chapters", pro: true, }, ]; diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx index 5514b26e93..50f71bbc72 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx @@ -41,7 +41,7 @@ const options = [ { label: "Enable transcript", value: "disableTranscript", - description: "Enabling this also allows generating chapters and summary", + description: "Enabling this also allows chapters and summary", pro: true, }, ]; From 1ee233e073ac11e3e371ab1de5e833bae64cb0e0 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:43:44 +0300 Subject: [PATCH 19/21] fix label --- .../settings/organization/components/CapSettingsCard.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx index 50f71bbc72..a0f826c727 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx @@ -108,9 +108,7 @@ const CapSettingsCard = () => { const option = options.find((opt) => opt.value === changedKey); const isDisabled = debouncedUpdateSettings[changedKey]; const action = isDisabled ? "disabled" : "enabled"; - const label = - option?.label.replace(/^Disable /, "").toLowerCase() || - changedKey; + const label = option?.label.split(" ")[1] || changedKey; toast.success( `${label.charAt(0).toUpperCase()}${label.slice(1)} ${action}`, ); From 734029f36446cc820aeb5a1c1d6d7943602d441c Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:02:38 +0300 Subject: [PATCH 20/21] make sure dialog resets, fix pro text color --- .../caps/components/SettingsDialog.tsx | 33 +++++++++++++------ .../components/CapSettingsCard.tsx | 2 +- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx index 53a406c785..06226dd17e 100644 --- a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx @@ -12,7 +12,7 @@ import type { Video } from "@cap/web-domain"; import { faGear } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import clsx from "clsx"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { updateVideoSettings } from "@/actions/videos/settings"; import { useDashboardContext } from "../../Contexts"; @@ -74,14 +74,27 @@ export const SettingsDialog = ({ }: SettingsDialogProps) => { const { user, organizationSettings } = useDashboardContext(); const [saveLoading, setSaveLoading] = useState(false); - const [settings, setSettings] = useState({ - disableComments: settingsData?.disableComments, - disableSummary: settingsData?.disableSummary, - disableCaptions: settingsData?.disableCaptions, - disableChapters: settingsData?.disableChapters, - disableReactions: settingsData?.disableReactions, - disableTranscript: settingsData?.disableTranscript, - }); + const buildSettings = useCallback( + (data?: OrganizationSettings): OrganizationSettings => ({ + disableComments: data?.disableComments, + disableSummary: data?.disableSummary, + disableCaptions: data?.disableCaptions, + disableChapters: data?.disableChapters, + disableReactions: data?.disableReactions, + disableTranscript: data?.disableTranscript, + }), + [], + ); + + const [settings, setSettings] = useState( + buildSettings(settingsData), + ); + + useEffect(() => { + if (isOpen) { + setSettings(buildSettings(settingsData)); + } + }, [buildSettings, isOpen, settingsData]); const isUserPro = userIsPro(user); @@ -166,7 +179,7 @@ export const SettingsDialog = ({

{option.label}

{option.pro && ( -

+

Pro

)} diff --git a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx index a0f826c727..ff94679ec0 100644 --- a/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx +++ b/apps/web/app/(org)/dashboard/settings/organization/components/CapSettingsCard.tsx @@ -170,7 +170,7 @@ const CapSettingsCard = () => {

{option.label}

{option.pro && ( -

+

Pro

)} From 86a13ec1b435b3bd5331944e886c44dfd55f27f6 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:06:13 +0300 Subject: [PATCH 21/21] Update SharedCaps.tsx --- .../dashboard/spaces/[spaceId]/SharedCaps.tsx | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx index 2519774669..7201e4572a 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx @@ -281,32 +281,38 @@ export const SharedCaps = ({ )} -

Videos

-
- {data.map((cap) => { - const isOwner = cap.ownerId === currentUserId; - return ( - - setIsDraggingCap({ isOwner, isDragging: true }) - } - onDragEnd={() => setIsDraggingCap({ isOwner, isDragging: false })} - /> - ); - })} -
- {(data.length > limit || data.length === limit || page !== 1) && ( -
- -
+ {data.length > 0 && ( + <> +

Videos

+
+ {data.map((cap) => { + const isOwner = cap.ownerId === currentUserId; + return ( + + setIsDraggingCap({ isOwner, isDragging: true }) + } + onDragEnd={() => + setIsDraggingCap({ isOwner, isDragging: false }) + } + /> + ); + })} +
+ {(data.length > limit || data.length === limit || page !== 1) && ( +
+ +
+ )} + )}
);