diff --git a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx index 80715a66fe..c6c3377fa5 100644 --- a/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/SettingsDialog.tsx @@ -22,6 +22,7 @@ interface SettingsDialogProps { onClose: () => void; capId: Video.VideoId; settingsData?: OrganizationSettings; + isPro?: boolean; } const options: { @@ -70,8 +71,10 @@ export const SettingsDialog = ({ onClose, capId, settingsData, + isPro, }: SettingsDialogProps) => { const { user, organizationSettings } = useDashboardContext(); + const isProUser = isPro ?? user?.isPro ?? false; const [saveLoading, setSaveLoading] = useState(false); const buildSettings = useCallback( (data?: OrganizationSettings): OrganizationSettings => ({ @@ -190,7 +193,7 @@ export const SettingsDialog = ({ void; +} + +export const CapOptionsDropdown: React.FC = ({ + videoId, + videoName, + hasPassword, + isOwnerPro, + settingsData, + onDeleted, +}) => { + const router = useRouter(); + const rpc = useRpcClient(); + + const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false); + const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false); + const [passwordProtected, setPasswordProtected] = useState(hasPassword); + const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); + const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); + + const downloadMutation = useEffectMutation({ + mutationFn: () => + Effect.gen(function* () { + const result = yield* rpc.VideoGetDownloadInfo(videoId); + const httpClient = yield* HttpClient.HttpClient; + if (Option.isSome(result)) { + const fetchResponse = yield* httpClient.get(result.value.downloadUrl); + const blob = yield* fetchResponse.arrayBuffer; + + const blobUrl = window.URL.createObjectURL(new Blob([blob])); + const link = document.createElement("a"); + link.href = blobUrl; + link.download = result.value.fileName; + link.style.display = "none"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + window.URL.revokeObjectURL(blobUrl); + } else { + throw new Error("Failed to get download URL"); + } + }), + }); + + const deleteMutation = useEffectMutation({ + mutationFn: () => rpc.VideoDelete(videoId), + onSuccess: () => { + toast.success("Cap deleted successfully"); + setConfirmDeleteOpen(false); + onDeleted?.(); + router.push("/dashboard/caps"); + }, + onError: () => { + toast.error("Failed to delete cap"); + }, + }); + + const duplicateMutation = useEffectMutation({ + mutationFn: () => rpc.VideoDuplicate(videoId), + onSuccess: () => { + toast.success("Cap duplicated successfully"); + router.refresh(); + }, + }); + + const handleDownload = () => { + if (downloadMutation.isPending) return; + + toast.promise(downloadMutation.mutateAsync(), { + loading: "Preparing download...", + success: "Download started successfully", + error: (error) => { + if (error instanceof Error) { + return error.message; + } + return "Failed to download video - please try again."; + }, + }); + }; + + const handlePasswordUpdated = (protectedStatus: boolean) => { + setPasswordProtected(protectedStatus); + router.refresh(); + }; + + return ( + <> + setIsSettingsDialogOpen(false)} + isPro={isOwnerPro} + /> + setIsPasswordDialogOpen(false)} + videoId={videoId} + hasPassword={passwordProtected} + onPasswordUpdated={handlePasswordUpdated} + /> + } + title="Delete Cap" + description={`Are you sure you want to delete the cap "${videoName}"? This action cannot be undone.`} + confirmLabel={deleteMutation.isPending ? "Deleting..." : "Delete"} + cancelLabel="Cancel" + confirmVariant="destructive" + loading={deleteMutation.isPending} + onConfirm={() => deleteMutation.mutate()} + onCancel={() => setConfirmDeleteOpen(false)} + /> + + + + + + + + + setIsSettingsDialogOpen(true)} + className="flex gap-2 items-center rounded-lg" + > + + Settings + + { + router.push(`/dashboard/analytics?capId=${videoId}`); + }} + className="flex gap-2 items-center rounded-lg" + > + + View analytics + + + + Download + + { + toast.promise(duplicateMutation.mutateAsync(), { + loading: "Duplicating cap...", + success: "Cap duplicated successfully", + error: "Failed to duplicate cap", + }); + }} + disabled={duplicateMutation.isPending} + className="flex gap-2 items-center rounded-lg" + > + + Duplicate + + { + if (!isOwnerPro) setUpgradeModalOpen(true); + else setIsPasswordDialogOpen(true); + }} + className="flex gap-2 items-center rounded-lg" + > + + + {passwordProtected ? "Edit password" : "Add password"} + + + setConfirmDeleteOpen(true)} + className="flex gap-2 items-center rounded-lg" + > + + Delete Cap + + + + + ); +}; diff --git a/apps/web/app/s/[videoId]/_components/ShareHeader.tsx b/apps/web/app/s/[videoId]/_components/ShareHeader.tsx index 4e437a23ec..54b3d206f9 100644 --- a/apps/web/app/s/[videoId]/_components/ShareHeader.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareHeader.tsx @@ -3,12 +3,12 @@ import { buildEnv, NODE_ENV } from "@cap/env"; import { Button } from "@cap/ui"; import { - faChartSimple, faChevronDown, faLock, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Check, Copy, Globe2 } from "lucide-react"; +import { CapOptionsDropdown } from "./CapOptionsDropdown"; import moment from "moment"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -23,6 +23,7 @@ import { Tooltip } from "@/components/Tooltip"; import { UpgradeModal } from "@/components/UpgradeModal"; import { usePublicEnv } from "@/utils/public-env"; import type { VideoData } from "../types"; +import type { OrganizationSettings } from "@/app/(org)/dashboard/dashboard-data"; export const ShareHeader = ({ data, @@ -31,6 +32,7 @@ export const ShareHeader = ({ sharedOrganizations = [], sharedSpaces = [], spacesData = null, + videoSettings, }: { data: VideoData; customDomain?: string | null; @@ -50,6 +52,7 @@ export const ShareHeader = ({ organizationId: string; }[]; spacesData?: Spaces[] | null; + videoSettings?: OrganizationSettings | null; }) => { const user = useCurrentUser(); const { push, refresh } = useRouter(); @@ -179,6 +182,7 @@ export const ShareHeader = ({ }; const userIsOwnerAndNotPro = user?.id === data.owner.id && !data.owner.isPro; + const userIsOwnerAndPro = user?.id === data.owner.id && data.owner.isPro; return ( <> @@ -296,24 +300,13 @@ export const ShareHeader = ({ {user !== null && (
{isOwner && ( - - - + )}