Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface SettingsDialogProps {
onClose: () => void;
capId: Video.VideoId;
settingsData?: OrganizationSettings;
isPro?: boolean;
}

const options: {
Expand Down Expand Up @@ -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 => ({
Expand Down Expand Up @@ -190,7 +193,7 @@ export const SettingsDialog = ({
</div>
<Switch
disabled={
(option.pro && !user.isPro) ||
(option.pro && !isProUser) ||
((key === "disableSummary" || key === "disableChapters") &&
getEffectiveValue(
"disableTranscript" as keyof OrganizationSettings,
Expand Down
239 changes: 239 additions & 0 deletions apps/web/app/s/[videoId]/_components/CapOptionsDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
"use client";

import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@cap/ui";
import type { Video } from "@cap/web-domain";
import { HttpClient } from "@effect/platform";
import {
faChartSimple,
faCopy,
faDownload,
faGear,
faLock,
faTrash,
faUnlock,
faVideo,
faEllipsis
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Effect, Option } from "effect";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { ConfirmationDialog } from "@/app/(org)/dashboard/_components/ConfirmationDialog";
import { PasswordDialog } from "@/app/(org)/dashboard/caps/components/PasswordDialog";
import { SettingsDialog } from "@/app/(org)/dashboard/caps/components/SettingsDialog";
import type { OrganizationSettings } from "@/app/(org)/dashboard/dashboard-data";
import { Tooltip } from "@/components/Tooltip";
import { UpgradeModal } from "@/components/UpgradeModal";
import { useEffectMutation, useRpcClient } from "@/lib/EffectRuntime";

interface ICapOptionsDropdownProps {
videoId: Video.VideoId;
videoName: string;
hasPassword: boolean;
isOwnerPro: boolean;
settingsData?: OrganizationSettings;
onDeleted?: () => void;
}

export const CapOptionsDropdown: React.FC<ICapOptionsDropdownProps> = ({
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 (
<>
<SettingsDialog
isOpen={isSettingsDialogOpen}
settingsData={settingsData}
capId={videoId}
onClose={() => setIsSettingsDialogOpen(false)}
isPro={isOwnerPro}
/>
<PasswordDialog
isOpen={isPasswordDialogOpen}
onClose={() => setIsPasswordDialogOpen(false)}
videoId={videoId}
hasPassword={passwordProtected}
onPasswordUpdated={handlePasswordUpdated}
/>
<ConfirmationDialog
open={confirmDeleteOpen}
icon={<FontAwesomeIcon icon={faVideo} />}
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)}
/>
<UpgradeModal
open={upgradeModalOpen}
onOpenChange={setUpgradeModalOpen}
/>
<DropdownMenu modal={false}>
<Tooltip
content="Options"
className="bg-gray-12 text-gray-1 border-gray-11 shadow-lg"
delayDuration={100}
>
<DropdownMenuTrigger asChild>
<Button
variant="gray"
className="rounded-full flex items-center justify-center"
>
<FontAwesomeIcon icon={faEllipsis} />
</Button>
</DropdownMenuTrigger>
</Tooltip>
<DropdownMenuContent align="end" sideOffset={5}>
<DropdownMenuItem
onClick={() => setIsSettingsDialogOpen(true)}
className="flex gap-2 items-center rounded-lg"
>
<FontAwesomeIcon className="size-3" icon={faGear} />
<span className="text-sm text-gray-12">Settings</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
router.push(`/dashboard/analytics?capId=${videoId}`);
}}
className="flex gap-2 items-center rounded-lg"
>
<FontAwesomeIcon className="size-3" icon={faChartSimple} />
<span className="text-sm text-gray-12">View analytics</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleDownload}
className="flex gap-2 items-center rounded-lg"
>
<FontAwesomeIcon className="size-3" icon={faDownload} />
<span className="text-sm text-gray-12">Download</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
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"
>
<FontAwesomeIcon className="size-3" icon={faCopy} />
<span className="text-sm text-gray-12">Duplicate</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
if (!isOwnerPro) setUpgradeModalOpen(true);
else setIsPasswordDialogOpen(true);
}}
className="flex gap-2 items-center rounded-lg"
>
<FontAwesomeIcon
className="size-3"
icon={passwordProtected ? faLock : faUnlock}
/>
<span className="text-sm text-gray-12">
{passwordProtected ? "Edit password" : "Add password"}
</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setConfirmDeleteOpen(true)}
className="flex gap-2 items-center rounded-lg"
>
<FontAwesomeIcon className="size-3" icon={faTrash} />
<span className="text-sm text-gray-12">Delete Cap</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
);
};
31 changes: 12 additions & 19 deletions apps/web/app/s/[videoId]/_components/ShareHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -31,6 +32,7 @@ export const ShareHeader = ({
sharedOrganizations = [],
sharedSpaces = [],
spacesData = null,
videoSettings,
}: {
data: VideoData;
customDomain?: string | null;
Expand All @@ -50,6 +52,7 @@ export const ShareHeader = ({
organizationId: string;
}[];
spacesData?: Spaces[] | null;
videoSettings?: OrganizationSettings | null;
}) => {
const user = useCurrentUser();
const { push, refresh } = useRouter();
Expand Down Expand Up @@ -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 (
<>
Expand Down Expand Up @@ -296,24 +300,13 @@ export const ShareHeader = ({
{user !== null && (
<div className="hidden md:flex gap-2">
{isOwner && (
<Tooltip
content="View analytics"
className="bg-gray-12 text-gray-1 border-gray-11 shadow-lg"
delayDuration={100}
>
<Button
variant="gray"
className="rounded-full flex items-center justify-center"
onClick={() => {
push(`/dashboard/analytics?capId=${data.id}`);
}}
>
<FontAwesomeIcon
className="size-4 text-gray-12"
icon={faChartSimple}
/>
</Button>
</Tooltip>
<CapOptionsDropdown
videoId={data.id}
videoName={data.name}
hasPassword={data.hasPassword || false}
isOwnerPro={userIsOwnerAndPro}
settingsData={videoSettings || undefined}
/>
)}
<Button
onClick={() => {
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/s/[videoId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ async function AuthorizedContent({
sharedSpaces={sharedSpaces}
userOrganizations={userOrganizations}
spacesData={spacesData}
videoSettings={videoWithOrganizationInfo.settings}
/>

<Share
Expand Down