From bbc9d6aaaae43266e4f4e46c77c51a2bdcaf1bc0 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:16:25 +0300 Subject: [PATCH 1/4] only video owners will be able to perform additional actions on caps --- .../caps/components/CapCard/CapCard.tsx | 1019 +++++++++-------- .../caps/components/CapCard/CapCardButton.tsx | 38 + .../components/CapCard/CapCardButtons.tsx | 173 --- .../[id]/components/FolderVideosSection.tsx | 387 +++---- .../dashboard/spaces/[spaceId]/SharedCaps.tsx | 532 ++++----- .../[spaceId]/components/SharedCapCard.tsx | 135 ++- .../[spaceId]/folder/[folderId]/page.tsx | 127 +- apps/web/app/api/settings/onboarding/route.ts | 6 +- 8 files changed, 1162 insertions(+), 1255 deletions(-) create mode 100644 apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx delete mode 100644 apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButtons.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 7c32959920..3e49a1dbca 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -1,21 +1,22 @@ import type { VideoMetadata } from "@cap/database/types"; import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, } from "@cap/ui"; import type { Video } from "@cap/web-domain"; import { HttpClient } from "@effect/platform"; import { - faCheck, - faCopy, - faEllipsis, - faLock, - faTrash, - faUnlock, - faVideo, + faCheck, + faCopy, + faDownload, + faEllipsis, + faLink, + faLock, + faTrash, + faUnlock, + faVideo, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useMutation } from "@tanstack/react-query"; @@ -25,492 +26,544 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { type PropsWithChildren, useState } from "react"; import { toast } from "sonner"; -import { downloadVideo } from "@/actions/videos/download"; import { ConfirmationDialog } from "@/app/(org)/dashboard/_components/ConfirmationDialog"; import { useDashboardContext } from "@/app/(org)/dashboard/Contexts"; import ProgressCircle, { - useUploadProgress, + useUploadProgress, } from "@/app/s/[videoId]/_components/ProgressCircle"; -import { Tooltip } from "@/components/Tooltip"; import { VideoThumbnail } from "@/components/VideoThumbnail"; import { useEffectMutation } from "@/lib/EffectRuntime"; import { withRpc } from "@/lib/Rpcs"; import { PasswordDialog } from "../PasswordDialog"; import { SharingDialog } from "../SharingDialog"; import { CapCardAnalytics } from "./CapCardAnalytics"; -import { CapCardButtons } from "./CapCardButtons"; import { CapCardContent } from "./CapCardContent"; +import { CapCardButton } from "./CapCardButton"; export interface CapCardProps extends PropsWithChildren { - cap: { - id: Video.VideoId; - ownerId: string; - name: string; - createdAt: Date; - public?: boolean; - totalComments: number; - totalReactions: number; - sharedOrganizations?: { - id: string; - name: string; - iconUrl?: string | null; - }[]; - sharedSpaces?: { - id: string; - name: string; - iconUrl?: string | null; - organizationId: string; - }[]; - ownerName: string | null; - metadata?: VideoMetadata; - hasPassword?: boolean; - hasActiveUpload: boolean | undefined; - duration?: number; - }; - analytics: number; - isLoadingAnalytics: boolean; - onDelete?: () => void; - userId?: string; - sharedCapCard?: boolean; - isSelected?: boolean; - onSelectToggle?: () => void; - customDomain?: string | null; - domainVerified?: boolean; - hideSharedStatus?: boolean; - anyCapSelected?: boolean; - isDeleting?: boolean; - onDragStart?: () => void; - onDragEnd?: () => void; + cap: { + id: Video.VideoId; + ownerId: string; + name: string; + createdAt: Date; + public?: boolean; + totalComments: number; + totalReactions: number; + sharedOrganizations?: { + id: string; + name: string; + iconUrl?: string | null; + }[]; + sharedSpaces?: { + id: string; + name: string; + iconUrl?: string | null; + organizationId: string; + }[]; + ownerName: string | null; + metadata?: VideoMetadata; + hasPassword?: boolean; + hasActiveUpload: boolean | undefined; + duration?: number; + }; + analytics: number; + isLoadingAnalytics: boolean; + onDelete?: () => void; + userId?: string; + sharedCapCard?: boolean; + isSelected?: boolean; + onSelectToggle?: () => void; + customDomain?: string | null; + domainVerified?: boolean; + hideSharedStatus?: boolean; + anyCapSelected?: boolean; + isDeleting?: boolean; + onDragStart?: () => void; + onDragEnd?: () => void; } export const CapCard = ({ - cap, - analytics, - children, - onDelete, - userId, - isLoadingAnalytics, - sharedCapCard = false, - hideSharedStatus = false, - customDomain, - domainVerified, - isSelected = false, - onSelectToggle, - anyCapSelected = false, - isDeleting = false, + cap, + analytics, + children, + onDelete, + userId, + isLoadingAnalytics, + sharedCapCard = false, + hideSharedStatus = false, + customDomain, + domainVerified, + isSelected = false, + onSelectToggle, + anyCapSelected = false, + isDeleting = false, }: CapCardProps) => { - const [isSharingDialogOpen, setIsSharingDialogOpen] = useState(false); - const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [passwordProtected, setPasswordProtected] = useState( - cap.hasPassword || false, - ); - const [copyPressed, setCopyPressed] = useState(false); - const [isDragging, setIsDragging] = useState(false); - const { isSubscribed, setUpgradeModalOpen } = useDashboardContext(); - - const [confirmOpen, setConfirmOpen] = useState(false); - - const router = useRouter(); - - const downloadMutation = useEffectMutation({ - mutationFn: () => - Effect.gen(function* () { - const result = yield* withRpc((r) => r.VideoGetDownloadInfo(cap.id)); - 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 = useMutation({ - mutationFn: async () => { - await onDelete?.(); - }, - onError: (error) => { - console.error("Error deleting cap:", error); - }, - onSettled: () => { - setConfirmOpen(false); - }, - }); - - const duplicateMutation = useEffectMutation({ - mutationFn: () => withRpc((r) => r.VideoDuplicate(cap.id)), - onSuccess: () => { - router.refresh(); - }, - }); - - const handleSharingUpdated = () => { - router.refresh(); - }; - - const handlePasswordUpdated = (protectedStatus: boolean) => { - setPasswordProtected(protectedStatus); - router.refresh(); - }; - - const isOwner = userId === cap.ownerId; - - const uploadProgress = useUploadProgress( - cap.id, - cap.hasActiveUpload || false, - ); - - // Helper function to create a drag preview element - const createDragPreview = (text: string): HTMLElement => { - // Create the element - const element = document.createElement("div"); - - // Add text content - element.textContent = text; - - // Apply Tailwind-like styles directly - element.className = - "px-2 py-1.5 text-sm font-medium rounded-lg shadow-md text-gray-1 bg-gray-12"; - - // Position off-screen - element.style.position = "absolute"; - element.style.top = "-9999px"; - element.style.left = "-9999px"; - - return element; - }; - - const handleDragStart = (e: React.DragEvent) => { - if (anyCapSelected || !isOwner) return; - - // Set the data transfer - e.dataTransfer.setData( - "application/cap", - JSON.stringify({ - id: cap.id, - name: cap.name, - }), - ); - - // Set drag effect to 'move' to avoid showing the + icon - e.dataTransfer.effectAllowed = "move"; - - // Set the drag image using the helper function - try { - const dragPreview = createDragPreview(cap.name); - document.body.appendChild(dragPreview); - e.dataTransfer.setDragImage(dragPreview, 10, 10); - - // Clean up after a short delay - setTimeout(() => document.body.removeChild(dragPreview), 100); - } catch (error) { - console.error("Error setting drag image:", error); - } - - setIsDragging(true); - }; - - const handleDragEnd = () => { - setIsDragging(false); - }; - - const handleCopy = (text: string) => { - navigator.clipboard.writeText(text); - setCopyPressed(true); - setTimeout(() => { - setCopyPressed(false); - }, 2000); - }; - - const handleDownload = async () => { - 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 handleCardClick = (e: React.MouseEvent) => { - if (anyCapSelected) { - e.preventDefault(); - e.stopPropagation(); - if (onSelectToggle) { - onSelectToggle(); - } - } - }; - - const handleSelectClick = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (onSelectToggle) { - onSelectToggle(); - } - }; - - return ( - <> - setIsSharingDialogOpen(false)} - capId={cap.id} - capName={cap.name} - sharedSpaces={cap.sharedSpaces || []} - onSharingUpdated={handleSharingUpdated} - isPublic={cap.public} - /> - setIsPasswordDialogOpen(false)} - videoId={cap.id} - hasPassword={passwordProtected} - onPasswordUpdated={handlePasswordUpdated} - /> -
- {anyCapSelected && !sharedCapCard && ( -
- )} - {!sharedCapCard && ( -
- - - - - - - - - - - { - 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 (!isSubscribed) setUpgradeModalOpen(true); - else setIsPasswordDialogOpen(true); - }} - className="flex gap-2 items-center rounded-lg" - > - -

- {passwordProtected ? "Edit password" : "Add password"} -

-
- { - e.stopPropagation(); - setConfirmOpen(true); - }} - className="flex gap-2 items-center rounded-lg" - > - -

Delete Cap

-
-
-
- } - title="Delete Cap" - description={`Are you sure you want to delete the cap "${cap.name}"? This action cannot be undone.`} - confirmLabel={deleteMutation.isPending ? "Deleting..." : "Delete"} - cancelLabel="Cancel" - loading={deleteMutation.isPending} - onConfirm={() => deleteMutation.mutate()} - onCancel={() => setConfirmOpen(false)} - /> -
- )} - {!sharedCapCard && onSelectToggle && ( -
{ - e.stopPropagation(); - handleSelectClick(e); - }} - > -
- {isSelected && ( - - )} -
-
- )} -
- { - if (isDeleting) { - e.preventDefault(); - } - }} - href={`/s/${cap.id}`} - > - - - {uploadProgress && ( -
- {uploadProgress.status === "failed" ? ( -
-
- -
-

- Upload failed -

-
- ) : ( -
- -
- )} -
- )} -
-
- - {children} - -
-
- - ); + const [isSharingDialogOpen, setIsSharingDialogOpen] = useState(false); + const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [passwordProtected, setPasswordProtected] = useState( + cap.hasPassword || false + ); + const [copyPressed, setCopyPressed] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const { isSubscribed, setUpgradeModalOpen, activeSpace, user } = + useDashboardContext(); + + const [confirmOpen, setConfirmOpen] = useState(false); + + const router = useRouter(); + + const downloadMutation = useEffectMutation({ + mutationFn: () => + Effect.gen(function* () { + const result = yield* withRpc((r) => r.VideoGetDownloadInfo(cap.id)); + 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 = useMutation({ + mutationFn: async () => { + await onDelete?.(); + }, + onError: (error) => { + console.error("Error deleting cap:", error); + }, + onSettled: () => { + setConfirmOpen(false); + }, + }); + + const duplicateMutation = useEffectMutation({ + mutationFn: () => withRpc((r) => r.VideoDuplicate(cap.id)), + onSuccess: () => { + router.refresh(); + }, + }); + + const handleSharingUpdated = () => { + router.refresh(); + }; + + const handlePasswordUpdated = (protectedStatus: boolean) => { + setPasswordProtected(protectedStatus); + router.refresh(); + }; + + const isOwner = userId === cap.ownerId; + + const uploadProgress = useUploadProgress( + cap.id, + cap.hasActiveUpload || false + ); + + // Helper function to create a drag preview element + const createDragPreview = (text: string): HTMLElement => { + // Create the element + const element = document.createElement("div"); + + // Add text content + element.textContent = text; + + // Apply Tailwind-like styles directly + element.className = + "px-2 py-1.5 text-sm font-medium rounded-lg shadow-md text-gray-1 bg-gray-12"; + + // Position off-screen + element.style.position = "absolute"; + element.style.top = "-9999px"; + element.style.left = "-9999px"; + + return element; + }; + + const handleDragStart = (e: React.DragEvent) => { + if (anyCapSelected || !isOwner) return; + + // Set the data transfer + e.dataTransfer.setData( + "application/cap", + JSON.stringify({ + id: cap.id, + name: cap.name, + }) + ); + + // Set drag effect to 'move' to avoid showing the + icon + e.dataTransfer.effectAllowed = "move"; + + // Set the drag image using the helper function + try { + const dragPreview = createDragPreview(cap.name); + document.body.appendChild(dragPreview); + e.dataTransfer.setDragImage(dragPreview, 10, 10); + + // Clean up after a short delay + setTimeout(() => document.body.removeChild(dragPreview), 100); + } catch (error) { + console.error("Error setting drag image:", error); + } + + setIsDragging(true); + }; + + const handleDragEnd = () => { + setIsDragging(false); + }; + + const handleCopy = (text: string) => { + navigator.clipboard.writeText(text); + setCopyPressed(true); + setTimeout(() => { + setCopyPressed(false); + }, 2000); + }; + + const handleDownload = async () => { + 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 handleCardClick = (e: React.MouseEvent) => { + if (anyCapSelected) { + e.preventDefault(); + e.stopPropagation(); + if (onSelectToggle) { + onSelectToggle(); + } + } + }; + + const handleSelectClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (onSelectToggle) { + onSelectToggle(); + } + }; + + return ( + <> + setIsSharingDialogOpen(false)} + capId={cap.id} + capName={cap.name} + sharedSpaces={cap.sharedSpaces || []} + onSharingUpdated={handleSharingUpdated} + isPublic={cap.public} + /> + setIsPasswordDialogOpen(false)} + videoId={cap.id} + hasPassword={passwordProtected} + onPasswordUpdated={handlePasswordUpdated} + /> +
+ {anyCapSelected && !sharedCapCard && ( +
+ )} + +
+ { + e.stopPropagation(); + handleCopy(cap.id); + }} + className="delay-0" + icon={() => { + return !copyPressed ? ( + + ) : ( + + + + ); + }} + /> + { + e.stopPropagation(); + handleDownload(); + }} + disabled={downloadMutation.isPending} + className="delay-25" + icon={() => { + return downloadMutation.isPending ? ( +
+ +
+ ) : ( + + ); + }} + /> + + {isOwner && ( + + + ( + + )} + /> + + + { + 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 (!isSubscribed) setUpgradeModalOpen(true); + else setIsPasswordDialogOpen(true); + }} + className="flex gap-2 items-center rounded-lg" + > + +

+ {passwordProtected ? "Edit password" : "Add password"} +

+
+ { + e.stopPropagation(); + setConfirmOpen(true); + }} + className="flex gap-2 items-center rounded-lg" + > + +

Delete Cap

+
+
+
+ )} + + } + title="Delete Cap" + description={`Are you sure you want to delete the cap "${cap.name}"? This action cannot be undone.`} + confirmLabel={deleteMutation.isPending ? "Deleting..." : "Delete"} + cancelLabel="Cancel" + loading={deleteMutation.isPending} + onConfirm={() => deleteMutation.mutate()} + onCancel={() => setConfirmOpen(false)} + /> +
+ + {!sharedCapCard && onSelectToggle && ( +
{ + e.stopPropagation(); + handleSelectClick(e); + }} + > +
+ {isSelected && ( + + )} +
+
+ )} +
+ { + if (isDeleting) { + e.preventDefault(); + } + }} + href={`/s/${cap.id}`} + > + + + {uploadProgress && ( +
+ {uploadProgress.status === "failed" ? ( +
+
+ +
+

+ Upload failed +

+
+ ) : ( +
+ +
+ )} +
+ )} +
+
+ + {children} + +
+
+ + ); }; diff --git a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx new file mode 100644 index 0000000000..0430b598f1 --- /dev/null +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx @@ -0,0 +1,38 @@ +import { Button } from "@cap/ui"; +import { Tooltip } from "@/components/Tooltip"; +import { type ReactNode } from "react"; +import clsx from "clsx"; + +interface CapCardButtonProps { + tooltipContent: string; + onClick?: (e: React.MouseEvent) => void; + disabled?: boolean; + className: string; + icon: () => ReactNode; +} + +export const CapCardButton = ({ + tooltipContent, + onClick = () => {}, + disabled, + className, + icon, +}: CapCardButtonProps) => { + return ( + + + + ); +}; diff --git a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButtons.tsx b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButtons.tsx deleted file mode 100644 index 3232c92b92..0000000000 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButtons.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { buildEnv, NODE_ENV } from "@cap/env"; -import { Button } from "@cap/ui"; -import { faDownload, faLink } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import clsx from "clsx"; -import type { ReactNode } from "react"; -import { Tooltip } from "@/components/Tooltip"; -import { usePublicEnv } from "@/utils/public-env"; - -interface ButtonConfig { - tooltipContent: string; - onClick: (e: React.MouseEvent) => void; - className: string; - disabled: boolean; - icon: () => ReactNode; -} - -export interface CapCardButtonsProps { - capId: string; - copyPressed: boolean; - isDownloading: boolean; - handleCopy: (url: string) => void; - handleDownload: () => void; - customDomain?: string | null; - domainVerified?: boolean; -} - -export const CapCardButtons: React.FC = ({ - capId, - copyPressed, - isDownloading, - handleCopy, - handleDownload, - customDomain, - domainVerified, -}) => { - const { webUrl } = usePublicEnv(); - return ( - <> - {buttons( - capId, - copyPressed, - isDownloading, - handleCopy, - handleDownload, - webUrl, - customDomain, - domainVerified, - ).map((button, index) => ( - - - - ))} - - ); -}; - -const buttons = ( - capId: string, - copyPressed: boolean, - isDownloading: boolean, - handleCopy: (url: string) => void, - handleDownload: () => void, - webUrl: string, - customDomain?: string | null, - domainVerified?: boolean, -): ButtonConfig[] => [ - { - tooltipContent: "Copy link", - onClick: (e: React.MouseEvent) => { - e.stopPropagation(); - - const getVideoLink = () => { - if (NODE_ENV === "development" && customDomain && domainVerified) { - return `https://${customDomain}/s/${capId}`; - } else if ( - NODE_ENV === "development" && - !customDomain && - !domainVerified - ) { - return `${webUrl}/s/${capId}`; - } else if ( - buildEnv.NEXT_PUBLIC_IS_CAP && - customDomain && - domainVerified - ) { - return `https://${customDomain}/s/${capId}`; - } else if ( - buildEnv.NEXT_PUBLIC_IS_CAP && - !customDomain && - !domainVerified - ) { - return `https://cap.link/${capId}`; - } else { - return `${webUrl}/s/${capId}`; - } - }; - - handleCopy(getVideoLink()); - }, - className: "delay-0", - disabled: false, - icon: () => { - return !copyPressed ? ( - - ) : ( - - - - ); - }, - }, - { - tooltipContent: "Download Cap", - onClick: (e: React.MouseEvent) => { - e.stopPropagation(); - handleDownload(); - }, - className: "delay-25", - disabled: isDownloading, - icon: () => { - return isDownloading ? ( -
- - - - -
- ) : ( - - ); - }, - }, -]; diff --git a/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx b/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx index 07ad8214e4..46b27d403f 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx @@ -14,206 +14,201 @@ import type { VideoData } from "../../../caps/Caps"; import { CapCard } from "../../../caps/components/CapCard/CapCard"; import { SelectedCapsBar } from "../../../caps/components/SelectedCapsBar"; import { UploadPlaceholderCard } from "../../../caps/components/UploadPlaceholderCard"; -import { - useUploadingContext, - useUploadingStatus, -} from "../../../caps/UploadingContext"; +import { useUploadingStatus } from "../../../caps/UploadingContext"; interface FolderVideosSectionProps { - initialVideos: VideoData; - dubApiKeyEnabled: boolean; - cardType?: "shared" | "default"; + initialVideos: VideoData; + dubApiKeyEnabled: boolean; } export default function FolderVideosSection({ - initialVideos, - dubApiKeyEnabled, - cardType = "default", + initialVideos, + dubApiKeyEnabled, }: FolderVideosSectionProps) { - const router = useRouter(); - const { user } = useDashboardContext(); - - const [selectedCaps, setSelectedCaps] = useState([]); - const previousCountRef = useRef(0); - - const { mutate: deleteCaps, isPending: isDeletingCaps } = useEffectMutation({ - mutationFn: Effect.fn(function* (ids: Video.VideoId[]) { - if (ids.length === 0) return; - - const rpc = yield* Rpc; - - const fiber = yield* Effect.gen(function* () { - const results = yield* Effect.all( - ids.map((id) => rpc.VideoDelete(id).pipe(Effect.exit)), - { concurrency: 10 }, - ); - - const successCount = results.filter(Exit.isSuccess).length; - - const errorCount = ids.length - successCount; - - if (successCount > 0 && errorCount > 0) { - return { success: successCount, error: errorCount }; - } else if (successCount > 0) { - return { success: successCount }; - } else { - return yield* Effect.fail( - new Error( - `Failed to delete ${errorCount} cap${errorCount === 1 ? "" : "s"}`, - ), - ); - } - }).pipe(Effect.fork); - - toast.promise(Effect.runPromise(fiber.await.pipe(Effect.flatten)), { - loading: `Deleting ${ids.length} cap${ids.length === 1 ? "" : "s"}...`, - success: (data) => { - if (data.error) { - return `Successfully deleted ${data.success} cap${ - data.success === 1 ? "" : "s" - }, but failed to delete ${data.error} cap${ - data.error === 1 ? "" : "s" - }`; - } - return `Successfully deleted ${data.success} cap${ - data.success === 1 ? "" : "s" - }`; - }, - error: (error) => - error.message || "An error occurred while deleting caps", - }); - - return yield* fiber.await.pipe(Effect.flatten); - }), - onSuccess: () => { - setSelectedCaps([]); - router.refresh(); - }, - }); - - const { mutate: deleteCap, isPending: isDeletingCap } = useEffectMutation({ - mutationFn: (id: Video.VideoId) => withRpc((r) => r.VideoDelete(id)), - onSuccess: () => { - toast.success("Cap deleted successfully"); - router.refresh(); - }, - onError: () => { - toast.error("Failed to delete cap"); - }, - }); - - const handleCapSelection = (capId: Video.VideoId) => { - setSelectedCaps((prev) => { - const newSelection = prev.includes(capId) - ? prev.filter((id) => id !== capId) - : [...prev, capId]; - - previousCountRef.current = prev.length; - - return newSelection; - }); - }; - - const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({ - queryKey: ["analytics", initialVideos.map((video) => video.id)], - queryFn: async () => { - if (!dubApiKeyEnabled || initialVideos.length === 0) { - return {}; - } - - const analyticsPromises = initialVideos.map(async (video) => { - try { - const response = await fetch(`/api/analytics?videoId=${video.id}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - - if (response.ok) { - const responseData = await response.json(); - return { videoId: video.id, count: responseData.count || 0 }; - } - return { videoId: video.id, count: 0 }; - } catch (error) { - console.warn( - `Failed to fetch analytics for video ${video.id}:`, - error, - ); - return { videoId: video.id, count: 0 }; - } - }); - - const results = await Promise.allSettled(analyticsPromises); - const analyticsData: Record = {}; - - results.forEach((result) => { - if (result.status === "fulfilled" && result.value) { - analyticsData[result.value.videoId] = result.value.count; - } - }); - return analyticsData; - }, - refetchOnWindowFocus: false, - refetchOnMount: true, - }); - - const [isUploading, uploadingCapId] = useUploadingStatus(); - const visibleVideos = useMemo( - () => - isUploading && uploadingCapId - ? initialVideos.filter((video) => video.id !== uploadingCapId) - : initialVideos, - [initialVideos, isUploading, uploadingCapId], - ); - - const analytics = analyticsData || {}; - - return ( - <> -
-

Videos

-
-
- {visibleVideos.length === 0 && !isUploading ? ( -

- No videos in this folder yet. Drag and drop into the folder or - upload. -

- ) : ( - <> - {isUploading && ( - - )} - {visibleVideos.map((video) => ( - 0} - isDeleting={isDeletingCaps || isDeletingCap} - onSelectToggle={() => handleCapSelection(video.id)} - onDelete={() => { - if (selectedCaps.length > 0) { - deleteCaps(selectedCaps); - } else { - deleteCap(video.id); - } - }} - /> - ))} - - )} -
- deleteCaps(selectedCaps)} - isDeleting={isDeletingCaps || isDeletingCap} - /> - - ); + const router = useRouter(); + const { user } = useDashboardContext(); + + const [selectedCaps, setSelectedCaps] = useState([]); + const previousCountRef = useRef(0); + + const { mutate: deleteCaps, isPending: isDeletingCaps } = useEffectMutation({ + mutationFn: Effect.fn(function* (ids: Video.VideoId[]) { + if (ids.length === 0) return; + + const rpc = yield* Rpc; + + const fiber = yield* Effect.gen(function* () { + const results = yield* Effect.all( + ids.map((id) => rpc.VideoDelete(id).pipe(Effect.exit)), + { concurrency: 10 } + ); + + const successCount = results.filter(Exit.isSuccess).length; + + const errorCount = ids.length - successCount; + + if (successCount > 0 && errorCount > 0) { + return { success: successCount, error: errorCount }; + } else if (successCount > 0) { + return { success: successCount }; + } else { + return yield* Effect.fail( + new Error( + `Failed to delete ${errorCount} cap${errorCount === 1 ? "" : "s"}` + ) + ); + } + }).pipe(Effect.fork); + + toast.promise(Effect.runPromise(fiber.await.pipe(Effect.flatten)), { + loading: `Deleting ${ids.length} cap${ids.length === 1 ? "" : "s"}...`, + success: (data) => { + if (data.error) { + return `Successfully deleted ${data.success} cap${ + data.success === 1 ? "" : "s" + }, but failed to delete ${data.error} cap${ + data.error === 1 ? "" : "s" + }`; + } + return `Successfully deleted ${data.success} cap${ + data.success === 1 ? "" : "s" + }`; + }, + error: (error) => + error.message || "An error occurred while deleting caps", + }); + + return yield* fiber.await.pipe(Effect.flatten); + }), + onSuccess: () => { + setSelectedCaps([]); + router.refresh(); + }, + }); + + const { mutate: deleteCap, isPending: isDeletingCap } = useEffectMutation({ + mutationFn: (id: Video.VideoId) => withRpc((r) => r.VideoDelete(id)), + onSuccess: () => { + toast.success("Cap deleted successfully"); + router.refresh(); + }, + onError: () => { + toast.error("Failed to delete cap"); + }, + }); + + const handleCapSelection = (capId: Video.VideoId) => { + setSelectedCaps((prev) => { + const newSelection = prev.includes(capId) + ? prev.filter((id) => id !== capId) + : [...prev, capId]; + + previousCountRef.current = prev.length; + + return newSelection; + }); + }; + + const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({ + queryKey: ["analytics", initialVideos.map((video) => video.id)], + queryFn: async () => { + if (!dubApiKeyEnabled || initialVideos.length === 0) { + return {}; + } + + const analyticsPromises = initialVideos.map(async (video) => { + try { + const response = await fetch(`/api/analytics?videoId=${video.id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.ok) { + const responseData = await response.json(); + return { videoId: video.id, count: responseData.count || 0 }; + } + return { videoId: video.id, count: 0 }; + } catch (error) { + console.warn( + `Failed to fetch analytics for video ${video.id}:`, + error + ); + return { videoId: video.id, count: 0 }; + } + }); + + const results = await Promise.allSettled(analyticsPromises); + const analyticsData: Record = {}; + + results.forEach((result) => { + if (result.status === "fulfilled" && result.value) { + analyticsData[result.value.videoId] = result.value.count; + } + }); + return analyticsData; + }, + refetchOnWindowFocus: false, + refetchOnMount: true, + }); + + const [isUploading, uploadingCapId] = useUploadingStatus(); + const visibleVideos = useMemo( + () => + isUploading && uploadingCapId + ? initialVideos.filter((video) => video.id !== uploadingCapId) + : initialVideos, + [initialVideos, isUploading, uploadingCapId] + ); + + const analytics = analyticsData || {}; + + return ( + <> +
+

Videos

+
+
+ {visibleVideos.length === 0 && !isUploading ? ( +

+ No videos in this folder yet. Drag and drop into the folder or + upload. +

+ ) : ( + <> + {isUploading && ( + + )} + {visibleVideos.map((video) => ( + 0} + isDeleting={isDeletingCaps || isDeletingCap} + onSelectToggle={() => handleCapSelection(video.id)} + onDelete={() => { + if (selectedCaps.length > 0) { + deleteCaps(selectedCaps); + } else { + deleteCap(video.id); + } + }} + /> + ))} + + )} +
+ deleteCaps(selectedCaps)} + isDeleting={isDeletingCaps || isDeletingCap} + /> + + ); } diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx index 2519774669..bd5a99cd12 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx @@ -17,297 +17,297 @@ import { AddVideosToOrganizationDialog } from "./components/AddVideosToOrganizat import { EmptySharedCapState } from "./components/EmptySharedCapState"; import { MembersIndicator } from "./components/MembersIndicator"; import { - OrganizationIndicator, - type OrganizationMemberData, + OrganizationIndicator, + type OrganizationMemberData, } from "./components/OrganizationIndicator"; import { SharedCapCard } from "./components/SharedCapCard"; import type { SpaceMemberData } from "./page"; type SharedVideoData = { - id: Video.VideoId; - ownerId: string; - name: string; - createdAt: Date; - totalComments: number; - totalReactions: number; - ownerName: string | null; - metadata?: VideoMetadata; - hasActiveUpload: boolean | undefined; + id: Video.VideoId; + ownerId: string; + name: string; + createdAt: Date; + totalComments: number; + totalReactions: number; + ownerName: string | null; + metadata?: VideoMetadata; + hasActiveUpload: boolean | undefined; }[]; type SpaceData = { - id: string; - name: string; - organizationId: string; - createdById: string; + id: string; + name: string; + organizationId: string; + createdById: string; }; export const SharedCaps = ({ - data, - count, - spaceData, - spaceMembers, - organizationMembers, - currentUserId, - folders, - dubApiKeyEnabled, - organizationData, + data, + count, + spaceData, + spaceMembers, + organizationMembers, + currentUserId, + folders, + dubApiKeyEnabled, + organizationData, }: { - data: SharedVideoData; - count: number; - dubApiKeyEnabled: boolean; - spaceData?: SpaceData; - hideSharedWith?: boolean; - spaceMembers?: SpaceMemberData[]; - organizationMembers?: OrganizationMemberData[]; - currentUserId?: string; - folders?: FolderDataType[]; - organizationData?: { - id: string; - name: string; - ownerId: string; - }; + data: SharedVideoData; + count: number; + dubApiKeyEnabled: boolean; + spaceData?: SpaceData; + hideSharedWith?: boolean; + spaceMembers?: SpaceMemberData[]; + organizationMembers?: OrganizationMemberData[]; + currentUserId?: string; + folders?: FolderDataType[]; + organizationData?: { + id: string; + name: string; + ownerId: string; + }; }) => { - const params = useSearchParams(); - const router = useRouter(); - const page = Number(params.get("page")) || 1; - const { activeOrganization } = useDashboardContext(); - const limit = 15; - const [openNewFolderDialog, setOpenNewFolderDialog] = useState(false); - const totalPages = Math.ceil(count / limit); - const [isDraggingCap, setIsDraggingCap] = useState({ - isOwner: false, - isDragging: false, - }); - const [isAddVideosDialogOpen, setIsAddVideosDialogOpen] = useState(false); - const [ - isAddOrganizationVideosDialogOpen, - setIsAddOrganizationVideosDialogOpen, - ] = useState(false); + const params = useSearchParams(); + const router = useRouter(); + const page = Number(params.get("page")) || 1; + const { activeOrganization } = useDashboardContext(); + const limit = 15; + const [openNewFolderDialog, setOpenNewFolderDialog] = useState(false); + const totalPages = Math.ceil(count / limit); + const [isDraggingCap, setIsDraggingCap] = useState({ + isOwner: false, + isDragging: false, + }); + const [isAddVideosDialogOpen, setIsAddVideosDialogOpen] = useState(false); + const [ + isAddOrganizationVideosDialogOpen, + setIsAddOrganizationVideosDialogOpen, + ] = useState(false); - const isSpaceOwner = spaceData?.createdById === currentUserId; - const isOrgOwner = organizationData?.ownerId === currentUserId; + const isSpaceOwner = spaceData?.createdById === currentUserId; + const isOrgOwner = organizationData?.ownerId === currentUserId; - const spaceMemberCount = spaceMembers?.length || 0; + const spaceMemberCount = spaceMembers?.length || 0; - const organizationMemberCount = organizationMembers?.length || 0; + const organizationMemberCount = organizationMembers?.length || 0; - const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({ - queryKey: ["analytics", data.map((video) => video.id)], - queryFn: async () => { - if (!dubApiKeyEnabled || data.length === 0) { - return {}; - } + const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({ + queryKey: ["analytics", data.map((video) => video.id)], + queryFn: async () => { + if (!dubApiKeyEnabled || data.length === 0) { + return {}; + } - const analyticsPromises = data.map(async (video) => { - try { - const response = await fetch(`/api/analytics?videoId=${video.id}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); + const analyticsPromises = data.map(async (video) => { + try { + const response = await fetch(`/api/analytics?videoId=${video.id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); - if (response.ok) { - const responseData = await response.json(); - return { videoId: video.id, count: responseData.count || 0 }; - } - return { videoId: video.id, count: 0 }; - } catch (error) { - console.warn( - `Failed to fetch analytics for video ${video.id}:`, - error, - ); - return { videoId: video.id, count: 0 }; - } - }); + if (response.ok) { + const responseData = await response.json(); + return { videoId: video.id, count: responseData.count || 0 }; + } + return { videoId: video.id, count: 0 }; + } catch (error) { + console.warn( + `Failed to fetch analytics for video ${video.id}:`, + error + ); + return { videoId: video.id, count: 0 }; + } + }); - const results = await Promise.allSettled(analyticsPromises); - const analyticsData: Record = {}; + const results = await Promise.allSettled(analyticsPromises); + const analyticsData: Record = {}; - results.forEach((result) => { - if (result.status === "fulfilled" && result.value) { - analyticsData[result.value.videoId] = result.value.count; - } - }); + results.forEach((result) => { + if (result.status === "fulfilled" && result.value) { + analyticsData[result.value.videoId] = result.value.count; + } + }); - return analyticsData; - }, - staleTime: 30000, // 30 seconds - refetchOnWindowFocus: false, - }); + return analyticsData; + }, + staleTime: 30000, // 30 seconds + refetchOnWindowFocus: false, + }); - const analytics = analyticsData || {}; + const analytics = analyticsData || {}; - const handleVideosAdded = () => { - router.refresh(); - }; + const handleVideosAdded = () => { + router.refresh(); + }; - if (data.length === 0 && folders?.length === 0) { - return ( -
- {spaceData && spaceMembers && ( - setIsAddVideosDialogOpen(true)} - /> - )} - {organizationData && organizationMembers && !spaceData && ( - setIsAddOrganizationVideosDialogOpen(true)} - /> - )} - setIsAddVideosDialogOpen(true) - : () => setIsAddOrganizationVideosDialogOpen(true) - } - /> - {spaceData && ( - setIsAddVideosDialogOpen(false)} - spaceId={spaceData.id} - spaceName={spaceData.name} - onVideosAdded={handleVideosAdded} - /> - )} - {organizationData && ( - setIsAddOrganizationVideosDialogOpen(false)} - organizationId={organizationData.id} - organizationName={organizationData.name} - onVideosAdded={handleVideosAdded} - /> - )} -
- ); - } + if (data.length === 0 && folders?.length === 0) { + return ( +
+ {spaceData && spaceMembers && ( + setIsAddVideosDialogOpen(true)} + /> + )} + {organizationData && organizationMembers && !spaceData && ( + setIsAddOrganizationVideosDialogOpen(true)} + /> + )} + setIsAddVideosDialogOpen(true) + : () => setIsAddOrganizationVideosDialogOpen(true) + } + /> + {spaceData && ( + setIsAddVideosDialogOpen(false)} + spaceId={spaceData.id} + spaceName={spaceData.name} + onVideosAdded={handleVideosAdded} + /> + )} + {organizationData && ( + setIsAddOrganizationVideosDialogOpen(false)} + organizationId={organizationData.id} + organizationName={organizationData.name} + onVideosAdded={handleVideosAdded} + /> + )} +
+ ); + } - return ( -
- {isDraggingCap.isDragging && ( -
-
-
- -

- {isDraggingCap.isOwner - ? " Drag to a space to share or folder to move" - : "Only the video owner can drag and move the video"} -

-
-
-
- )} - -
- {spaceData && spaceMembers && ( - setIsAddVideosDialogOpen(true)} - /> - )} - {organizationData && organizationMembers && !spaceData && ( - setIsAddOrganizationVideosDialogOpen(true)} - /> - )} - {spaceData && ( - setIsAddVideosDialogOpen(false)} - spaceId={spaceData.id} - spaceName={spaceData.name} - onVideosAdded={handleVideosAdded} - /> - )} - {organizationData && ( - setIsAddOrganizationVideosDialogOpen(false)} - organizationId={organizationData.id} - organizationName={organizationData.name} - onVideosAdded={handleVideosAdded} - /> - )} - -
- {folders && folders.length > 0 && ( - <> -

Folders

-
- {folders.map((folder) => ( - - ))} -
- - )} + return ( +
+ {isDraggingCap.isDragging && ( +
+
+
+ +

+ {isDraggingCap.isOwner + ? " Drag to a space to share or folder to move" + : "Only the video owner can drag and move the video"} +

+
+
+
+ )} + +
+ {spaceData && spaceMembers && ( + setIsAddVideosDialogOpen(true)} + /> + )} + {organizationData && organizationMembers && !spaceData && ( + setIsAddOrganizationVideosDialogOpen(true)} + /> + )} + {spaceData && ( + setIsAddVideosDialogOpen(false)} + spaceId={spaceData.id} + spaceName={spaceData.name} + onVideosAdded={handleVideosAdded} + /> + )} + {organizationData && ( + setIsAddOrganizationVideosDialogOpen(false)} + organizationId={organizationData.id} + organizationName={organizationData.name} + onVideosAdded={handleVideosAdded} + /> + )} + +
+ {folders && folders.length > 0 && ( + <> +

Folders

+
+ {folders.map((folder) => ( + + ))} +
+ + )} -

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) && ( -
- -
- )} -
- ); +

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) && ( +
+ +
+ )} +
+ ); }; 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 9bbab65427..3b3dad6999 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx @@ -5,77 +5,76 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { CapCard } from "../../../caps/components/CapCard/CapCard"; interface SharedCapCardProps { - cap: { - id: Video.VideoId; - ownerId: string; - name: string; - createdAt: Date; - totalComments: number; - totalReactions: number; - ownerName: string | null; - metadata?: VideoMetadata; - hasActiveUpload: boolean | undefined; - }; - analytics: number; - isLoadingAnalytics: boolean; - organizationName: string; - userId?: string; - hideSharedStatus?: boolean; - spaceName?: string; - onDragStart?: () => void; - onDragEnd?: () => void; + cap: { + id: Video.VideoId; + ownerId: string; + name: string; + createdAt: Date; + totalComments: number; + totalReactions: number; + ownerName: string | null; + metadata?: VideoMetadata; + hasActiveUpload: boolean | undefined; + }; + analytics: number; + isLoadingAnalytics: boolean; + organizationName: string; + userId?: string; + hideSharedStatus?: boolean; + spaceName?: string; + onDragStart?: () => void; + onDragEnd?: () => void; } export const SharedCapCard: React.FC = ({ - cap, - analytics, - organizationName, - userId, - hideSharedStatus, - isLoadingAnalytics, - spaceName, - onDragStart, - onDragEnd, + cap, + analytics, + organizationName, + userId, + hideSharedStatus, + isLoadingAnalytics, + spaceName, + onDragStart, + onDragEnd, }) => { - const displayCount = - analytics === 0 - ? Math.max(cap.totalComments, cap.totalReactions) - : analytics; - const isOwner = userId === cap.ownerId; + const displayCount = + analytics === 0 + ? Math.max(cap.totalComments, cap.totalReactions) + : analytics; + const isOwner = userId === cap.ownerId; - return ( -
- -
- {cap.ownerName && ( -
- - {cap.ownerName} -
- )} - {isOwner && ( -
- -

- Shared with{" "} - - {spaceName || organizationName} - -

-
- )} -
-
-
- ); + return ( +
+ +
+ {cap.ownerName && ( +
+ + {cap.ownerName} +
+ )} + {isOwner && ( +
+ +

+ Shared with{" "} + + {spaceName || organizationName} + +

+
+ )} +
+
+
+ ); }; diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx index 466e1f8323..b8c0ff6414 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx @@ -3,80 +3,79 @@ import { serverEnv } from "@cap/env"; import type { Folder } from "@cap/web-domain"; import FolderCard from "@/app/(org)/dashboard/caps/components/Folder"; import { - getChildFolders, - getFolderBreadcrumb, - getVideosByFolderId, + getChildFolders, + getFolderBreadcrumb, + getVideosByFolderId, } from "@/lib/folder"; import { - BreadcrumbItem, - ClientMyCapsLink, - NewSubfolderButton, + BreadcrumbItem, + ClientMyCapsLink, + NewSubfolderButton, } from "../../../../folder/[id]/components"; import FolderVideosSection from "../../../../folder/[id]/components/FolderVideosSection"; const FolderPage = async ({ - params, + params, }: { - params: { spaceId: string; folderId: Folder.FolderId }; + params: { spaceId: string; folderId: Folder.FolderId }; }) => { - const user = await getCurrentUser(); - if (!user) return; + const user = await getCurrentUser(); + if (!user) return; - const [childFolders, breadcrumb, videosData] = await Promise.all([ - getChildFolders(params.folderId), - getFolderBreadcrumb(params.folderId), - getVideosByFolderId(params.folderId), - ]); - const userId = user?.id as string; + const [childFolders, breadcrumb, videosData] = await Promise.all([ + getChildFolders(params.folderId), + getFolderBreadcrumb(params.folderId), + getVideosByFolderId(params.folderId), + ]); + const userId = user?.id as string; - return ( -
-
- -
-
-
- - {breadcrumb.map((folder, index) => ( -
-

/

- -
- ))} -
-
- {/* Display Child Folders */} - {childFolders.length > 0 && ( - <> -

Subfolders

-
- {childFolders.map((folder) => ( - - ))} -
- - )} - {/* Display Videos */} - -
- ); + return ( +
+
+ +
+
+
+ + {breadcrumb.map((folder, index) => ( +
+

/

+ +
+ ))} +
+
+ {/* Display Child Folders */} + {childFolders.length > 0 && ( + <> +

Subfolders

+
+ {childFolders.map((folder) => ( + + ))} +
+ + )} + {/* Display Videos */} + +
+ ); }; export default FolderPage; diff --git a/apps/web/app/api/settings/onboarding/route.ts b/apps/web/app/api/settings/onboarding/route.ts index 3f9255021d..983f1bed76 100644 --- a/apps/web/app/api/settings/onboarding/route.ts +++ b/apps/web/app/api/settings/onboarding/route.ts @@ -47,11 +47,7 @@ export async function POST(request: NextRequest) { ) .limit(1); - console.log("memberButNotOwner", memberButNotOwner); - - const isMemberOfOrganization = memberButNotOwner.length > 0; - - console.log("isMemberOfOrganization", isMemberOfOrganization); + const isMemberOfOrganization = memberButNotOwner.length > 0;; const [organization] = await db() .select() From 2b81bcf8b8187bd21848dbeba777d3c4c676d4ea Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Thu, 25 Sep 2025 12:18:42 +0300 Subject: [PATCH 2/4] formatting --- .../caps/components/CapCard/CapCard.tsx | 1070 ++++++++--------- .../caps/components/CapCard/CapCardButton.tsx | 56 +- .../[id]/components/FolderVideosSection.tsx | 380 +++--- .../dashboard/spaces/[spaceId]/SharedCaps.tsx | 532 ++++---- .../[spaceId]/components/SharedCapCard.tsx | 134 +-- .../[spaceId]/folder/[folderId]/page.tsx | 126 +- apps/web/app/api/settings/onboarding/route.ts | 2 +- 7 files changed, 1150 insertions(+), 1150 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 3e49a1dbca..5cf04f73a0 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -1,22 +1,22 @@ import type { VideoMetadata } from "@cap/database/types"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, } from "@cap/ui"; import type { Video } from "@cap/web-domain"; import { HttpClient } from "@effect/platform"; import { - faCheck, - faCopy, - faDownload, - faEllipsis, - faLink, - faLock, - faTrash, - faUnlock, - faVideo, + faCheck, + faCopy, + faDownload, + faEllipsis, + faLink, + faLock, + faTrash, + faUnlock, + faVideo, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useMutation } from "@tanstack/react-query"; @@ -29,7 +29,7 @@ import { toast } from "sonner"; import { ConfirmationDialog } from "@/app/(org)/dashboard/_components/ConfirmationDialog"; import { useDashboardContext } from "@/app/(org)/dashboard/Contexts"; import ProgressCircle, { - useUploadProgress, + useUploadProgress, } from "@/app/s/[videoId]/_components/ProgressCircle"; import { VideoThumbnail } from "@/components/VideoThumbnail"; import { useEffectMutation } from "@/lib/EffectRuntime"; @@ -41,529 +41,529 @@ import { CapCardContent } from "./CapCardContent"; import { CapCardButton } from "./CapCardButton"; export interface CapCardProps extends PropsWithChildren { - cap: { - id: Video.VideoId; - ownerId: string; - name: string; - createdAt: Date; - public?: boolean; - totalComments: number; - totalReactions: number; - sharedOrganizations?: { - id: string; - name: string; - iconUrl?: string | null; - }[]; - sharedSpaces?: { - id: string; - name: string; - iconUrl?: string | null; - organizationId: string; - }[]; - ownerName: string | null; - metadata?: VideoMetadata; - hasPassword?: boolean; - hasActiveUpload: boolean | undefined; - duration?: number; - }; - analytics: number; - isLoadingAnalytics: boolean; - onDelete?: () => void; - userId?: string; - sharedCapCard?: boolean; - isSelected?: boolean; - onSelectToggle?: () => void; - customDomain?: string | null; - domainVerified?: boolean; - hideSharedStatus?: boolean; - anyCapSelected?: boolean; - isDeleting?: boolean; - onDragStart?: () => void; - onDragEnd?: () => void; + cap: { + id: Video.VideoId; + ownerId: string; + name: string; + createdAt: Date; + public?: boolean; + totalComments: number; + totalReactions: number; + sharedOrganizations?: { + id: string; + name: string; + iconUrl?: string | null; + }[]; + sharedSpaces?: { + id: string; + name: string; + iconUrl?: string | null; + organizationId: string; + }[]; + ownerName: string | null; + metadata?: VideoMetadata; + hasPassword?: boolean; + hasActiveUpload: boolean | undefined; + duration?: number; + }; + analytics: number; + isLoadingAnalytics: boolean; + onDelete?: () => void; + userId?: string; + sharedCapCard?: boolean; + isSelected?: boolean; + onSelectToggle?: () => void; + customDomain?: string | null; + domainVerified?: boolean; + hideSharedStatus?: boolean; + anyCapSelected?: boolean; + isDeleting?: boolean; + onDragStart?: () => void; + onDragEnd?: () => void; } export const CapCard = ({ - cap, - analytics, - children, - onDelete, - userId, - isLoadingAnalytics, - sharedCapCard = false, - hideSharedStatus = false, - customDomain, - domainVerified, - isSelected = false, - onSelectToggle, - anyCapSelected = false, - isDeleting = false, + cap, + analytics, + children, + onDelete, + userId, + isLoadingAnalytics, + sharedCapCard = false, + hideSharedStatus = false, + customDomain, + domainVerified, + isSelected = false, + onSelectToggle, + anyCapSelected = false, + isDeleting = false, }: CapCardProps) => { - const [isSharingDialogOpen, setIsSharingDialogOpen] = useState(false); - const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [passwordProtected, setPasswordProtected] = useState( - cap.hasPassword || false - ); - const [copyPressed, setCopyPressed] = useState(false); - const [isDragging, setIsDragging] = useState(false); - const { isSubscribed, setUpgradeModalOpen, activeSpace, user } = - useDashboardContext(); - - const [confirmOpen, setConfirmOpen] = useState(false); - - const router = useRouter(); - - const downloadMutation = useEffectMutation({ - mutationFn: () => - Effect.gen(function* () { - const result = yield* withRpc((r) => r.VideoGetDownloadInfo(cap.id)); - 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 = useMutation({ - mutationFn: async () => { - await onDelete?.(); - }, - onError: (error) => { - console.error("Error deleting cap:", error); - }, - onSettled: () => { - setConfirmOpen(false); - }, - }); - - const duplicateMutation = useEffectMutation({ - mutationFn: () => withRpc((r) => r.VideoDuplicate(cap.id)), - onSuccess: () => { - router.refresh(); - }, - }); - - const handleSharingUpdated = () => { - router.refresh(); - }; - - const handlePasswordUpdated = (protectedStatus: boolean) => { - setPasswordProtected(protectedStatus); - router.refresh(); - }; - - const isOwner = userId === cap.ownerId; - - const uploadProgress = useUploadProgress( - cap.id, - cap.hasActiveUpload || false - ); - - // Helper function to create a drag preview element - const createDragPreview = (text: string): HTMLElement => { - // Create the element - const element = document.createElement("div"); - - // Add text content - element.textContent = text; - - // Apply Tailwind-like styles directly - element.className = - "px-2 py-1.5 text-sm font-medium rounded-lg shadow-md text-gray-1 bg-gray-12"; - - // Position off-screen - element.style.position = "absolute"; - element.style.top = "-9999px"; - element.style.left = "-9999px"; - - return element; - }; - - const handleDragStart = (e: React.DragEvent) => { - if (anyCapSelected || !isOwner) return; - - // Set the data transfer - e.dataTransfer.setData( - "application/cap", - JSON.stringify({ - id: cap.id, - name: cap.name, - }) - ); - - // Set drag effect to 'move' to avoid showing the + icon - e.dataTransfer.effectAllowed = "move"; - - // Set the drag image using the helper function - try { - const dragPreview = createDragPreview(cap.name); - document.body.appendChild(dragPreview); - e.dataTransfer.setDragImage(dragPreview, 10, 10); - - // Clean up after a short delay - setTimeout(() => document.body.removeChild(dragPreview), 100); - } catch (error) { - console.error("Error setting drag image:", error); - } - - setIsDragging(true); - }; - - const handleDragEnd = () => { - setIsDragging(false); - }; - - const handleCopy = (text: string) => { - navigator.clipboard.writeText(text); - setCopyPressed(true); - setTimeout(() => { - setCopyPressed(false); - }, 2000); - }; - - const handleDownload = async () => { - 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 handleCardClick = (e: React.MouseEvent) => { - if (anyCapSelected) { - e.preventDefault(); - e.stopPropagation(); - if (onSelectToggle) { - onSelectToggle(); - } - } - }; - - const handleSelectClick = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (onSelectToggle) { - onSelectToggle(); - } - }; - - return ( - <> - setIsSharingDialogOpen(false)} - capId={cap.id} - capName={cap.name} - sharedSpaces={cap.sharedSpaces || []} - onSharingUpdated={handleSharingUpdated} - isPublic={cap.public} - /> - setIsPasswordDialogOpen(false)} - videoId={cap.id} - hasPassword={passwordProtected} - onPasswordUpdated={handlePasswordUpdated} - /> -
- {anyCapSelected && !sharedCapCard && ( -
- )} - -
- { - e.stopPropagation(); - handleCopy(cap.id); - }} - className="delay-0" - icon={() => { - return !copyPressed ? ( - - ) : ( - - - - ); - }} - /> - { - e.stopPropagation(); - handleDownload(); - }} - disabled={downloadMutation.isPending} - className="delay-25" - icon={() => { - return downloadMutation.isPending ? ( -
- -
- ) : ( - - ); - }} - /> - - {isOwner && ( - - - ( - - )} - /> - - - { - 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 (!isSubscribed) setUpgradeModalOpen(true); - else setIsPasswordDialogOpen(true); - }} - className="flex gap-2 items-center rounded-lg" - > - -

- {passwordProtected ? "Edit password" : "Add password"} -

-
- { - e.stopPropagation(); - setConfirmOpen(true); - }} - className="flex gap-2 items-center rounded-lg" - > - -

Delete Cap

-
-
-
- )} - - } - title="Delete Cap" - description={`Are you sure you want to delete the cap "${cap.name}"? This action cannot be undone.`} - confirmLabel={deleteMutation.isPending ? "Deleting..." : "Delete"} - cancelLabel="Cancel" - loading={deleteMutation.isPending} - onConfirm={() => deleteMutation.mutate()} - onCancel={() => setConfirmOpen(false)} - /> -
- - {!sharedCapCard && onSelectToggle && ( -
{ - e.stopPropagation(); - handleSelectClick(e); - }} - > -
- {isSelected && ( - - )} -
-
- )} -
- { - if (isDeleting) { - e.preventDefault(); - } - }} - href={`/s/${cap.id}`} - > - - - {uploadProgress && ( -
- {uploadProgress.status === "failed" ? ( -
-
- -
-

- Upload failed -

-
- ) : ( -
- -
- )} -
- )} -
-
- - {children} - -
-
- - ); + const [isSharingDialogOpen, setIsSharingDialogOpen] = useState(false); + const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [passwordProtected, setPasswordProtected] = useState( + cap.hasPassword || false, + ); + const [copyPressed, setCopyPressed] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const { isSubscribed, setUpgradeModalOpen, activeSpace, user } = + useDashboardContext(); + + const [confirmOpen, setConfirmOpen] = useState(false); + + const router = useRouter(); + + const downloadMutation = useEffectMutation({ + mutationFn: () => + Effect.gen(function* () { + const result = yield* withRpc((r) => r.VideoGetDownloadInfo(cap.id)); + 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 = useMutation({ + mutationFn: async () => { + await onDelete?.(); + }, + onError: (error) => { + console.error("Error deleting cap:", error); + }, + onSettled: () => { + setConfirmOpen(false); + }, + }); + + const duplicateMutation = useEffectMutation({ + mutationFn: () => withRpc((r) => r.VideoDuplicate(cap.id)), + onSuccess: () => { + router.refresh(); + }, + }); + + const handleSharingUpdated = () => { + router.refresh(); + }; + + const handlePasswordUpdated = (protectedStatus: boolean) => { + setPasswordProtected(protectedStatus); + router.refresh(); + }; + + const isOwner = userId === cap.ownerId; + + const uploadProgress = useUploadProgress( + cap.id, + cap.hasActiveUpload || false, + ); + + // Helper function to create a drag preview element + const createDragPreview = (text: string): HTMLElement => { + // Create the element + const element = document.createElement("div"); + + // Add text content + element.textContent = text; + + // Apply Tailwind-like styles directly + element.className = + "px-2 py-1.5 text-sm font-medium rounded-lg shadow-md text-gray-1 bg-gray-12"; + + // Position off-screen + element.style.position = "absolute"; + element.style.top = "-9999px"; + element.style.left = "-9999px"; + + return element; + }; + + const handleDragStart = (e: React.DragEvent) => { + if (anyCapSelected || !isOwner) return; + + // Set the data transfer + e.dataTransfer.setData( + "application/cap", + JSON.stringify({ + id: cap.id, + name: cap.name, + }), + ); + + // Set drag effect to 'move' to avoid showing the + icon + e.dataTransfer.effectAllowed = "move"; + + // Set the drag image using the helper function + try { + const dragPreview = createDragPreview(cap.name); + document.body.appendChild(dragPreview); + e.dataTransfer.setDragImage(dragPreview, 10, 10); + + // Clean up after a short delay + setTimeout(() => document.body.removeChild(dragPreview), 100); + } catch (error) { + console.error("Error setting drag image:", error); + } + + setIsDragging(true); + }; + + const handleDragEnd = () => { + setIsDragging(false); + }; + + const handleCopy = (text: string) => { + navigator.clipboard.writeText(text); + setCopyPressed(true); + setTimeout(() => { + setCopyPressed(false); + }, 2000); + }; + + const handleDownload = async () => { + 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 handleCardClick = (e: React.MouseEvent) => { + if (anyCapSelected) { + e.preventDefault(); + e.stopPropagation(); + if (onSelectToggle) { + onSelectToggle(); + } + } + }; + + const handleSelectClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (onSelectToggle) { + onSelectToggle(); + } + }; + + return ( + <> + setIsSharingDialogOpen(false)} + capId={cap.id} + capName={cap.name} + sharedSpaces={cap.sharedSpaces || []} + onSharingUpdated={handleSharingUpdated} + isPublic={cap.public} + /> + setIsPasswordDialogOpen(false)} + videoId={cap.id} + hasPassword={passwordProtected} + onPasswordUpdated={handlePasswordUpdated} + /> +
+ {anyCapSelected && !sharedCapCard && ( +
+ )} + +
+ { + e.stopPropagation(); + handleCopy(cap.id); + }} + className="delay-0" + icon={() => { + return !copyPressed ? ( + + ) : ( + + + + ); + }} + /> + { + e.stopPropagation(); + handleDownload(); + }} + disabled={downloadMutation.isPending} + className="delay-25" + icon={() => { + return downloadMutation.isPending ? ( +
+ +
+ ) : ( + + ); + }} + /> + + {isOwner && ( + + + ( + + )} + /> + + + { + 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 (!isSubscribed) setUpgradeModalOpen(true); + else setIsPasswordDialogOpen(true); + }} + className="flex gap-2 items-center rounded-lg" + > + +

+ {passwordProtected ? "Edit password" : "Add password"} +

+
+ { + e.stopPropagation(); + setConfirmOpen(true); + }} + className="flex gap-2 items-center rounded-lg" + > + +

Delete Cap

+
+
+
+ )} + + } + title="Delete Cap" + description={`Are you sure you want to delete the cap "${cap.name}"? This action cannot be undone.`} + confirmLabel={deleteMutation.isPending ? "Deleting..." : "Delete"} + cancelLabel="Cancel" + loading={deleteMutation.isPending} + onConfirm={() => deleteMutation.mutate()} + onCancel={() => setConfirmOpen(false)} + /> +
+ + {!sharedCapCard && onSelectToggle && ( +
{ + e.stopPropagation(); + handleSelectClick(e); + }} + > +
+ {isSelected && ( + + )} +
+
+ )} +
+ { + if (isDeleting) { + e.preventDefault(); + } + }} + href={`/s/${cap.id}`} + > + + + {uploadProgress && ( +
+ {uploadProgress.status === "failed" ? ( +
+
+ +
+

+ Upload failed +

+
+ ) : ( +
+ +
+ )} +
+ )} +
+
+ + {children} + +
+
+ + ); }; 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 0430b598f1..1b97d73e5b 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx @@ -1,38 +1,38 @@ import { Button } from "@cap/ui"; import { Tooltip } from "@/components/Tooltip"; -import { type ReactNode } from "react"; +import { type ReactNode, type MouseEvent } from "react"; import clsx from "clsx"; interface CapCardButtonProps { - tooltipContent: string; - onClick?: (e: React.MouseEvent) => void; - disabled?: boolean; - className: string; - icon: () => ReactNode; + tooltipContent: string; + onClick?: (e: MouseEvent) => void; + disabled?: boolean; + className: string; + icon: () => ReactNode; } export const CapCardButton = ({ - tooltipContent, - onClick = () => {}, - disabled, - className, - icon, + tooltipContent, + onClick = () => {}, + disabled, + className, + icon, }: CapCardButtonProps) => { - return ( - - - - ); + return ( + + + + ); }; diff --git a/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx b/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx index 46b27d403f..ae33089287 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx @@ -17,198 +17,198 @@ import { UploadPlaceholderCard } from "../../../caps/components/UploadPlaceholde import { useUploadingStatus } from "../../../caps/UploadingContext"; interface FolderVideosSectionProps { - initialVideos: VideoData; - dubApiKeyEnabled: boolean; + initialVideos: VideoData; + dubApiKeyEnabled: boolean; } export default function FolderVideosSection({ - initialVideos, - dubApiKeyEnabled, + initialVideos, + dubApiKeyEnabled, }: FolderVideosSectionProps) { - const router = useRouter(); - const { user } = useDashboardContext(); - - const [selectedCaps, setSelectedCaps] = useState([]); - const previousCountRef = useRef(0); - - const { mutate: deleteCaps, isPending: isDeletingCaps } = useEffectMutation({ - mutationFn: Effect.fn(function* (ids: Video.VideoId[]) { - if (ids.length === 0) return; - - const rpc = yield* Rpc; - - const fiber = yield* Effect.gen(function* () { - const results = yield* Effect.all( - ids.map((id) => rpc.VideoDelete(id).pipe(Effect.exit)), - { concurrency: 10 } - ); - - const successCount = results.filter(Exit.isSuccess).length; - - const errorCount = ids.length - successCount; - - if (successCount > 0 && errorCount > 0) { - return { success: successCount, error: errorCount }; - } else if (successCount > 0) { - return { success: successCount }; - } else { - return yield* Effect.fail( - new Error( - `Failed to delete ${errorCount} cap${errorCount === 1 ? "" : "s"}` - ) - ); - } - }).pipe(Effect.fork); - - toast.promise(Effect.runPromise(fiber.await.pipe(Effect.flatten)), { - loading: `Deleting ${ids.length} cap${ids.length === 1 ? "" : "s"}...`, - success: (data) => { - if (data.error) { - return `Successfully deleted ${data.success} cap${ - data.success === 1 ? "" : "s" - }, but failed to delete ${data.error} cap${ - data.error === 1 ? "" : "s" - }`; - } - return `Successfully deleted ${data.success} cap${ - data.success === 1 ? "" : "s" - }`; - }, - error: (error) => - error.message || "An error occurred while deleting caps", - }); - - return yield* fiber.await.pipe(Effect.flatten); - }), - onSuccess: () => { - setSelectedCaps([]); - router.refresh(); - }, - }); - - const { mutate: deleteCap, isPending: isDeletingCap } = useEffectMutation({ - mutationFn: (id: Video.VideoId) => withRpc((r) => r.VideoDelete(id)), - onSuccess: () => { - toast.success("Cap deleted successfully"); - router.refresh(); - }, - onError: () => { - toast.error("Failed to delete cap"); - }, - }); - - const handleCapSelection = (capId: Video.VideoId) => { - setSelectedCaps((prev) => { - const newSelection = prev.includes(capId) - ? prev.filter((id) => id !== capId) - : [...prev, capId]; - - previousCountRef.current = prev.length; - - return newSelection; - }); - }; - - const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({ - queryKey: ["analytics", initialVideos.map((video) => video.id)], - queryFn: async () => { - if (!dubApiKeyEnabled || initialVideos.length === 0) { - return {}; - } - - const analyticsPromises = initialVideos.map(async (video) => { - try { - const response = await fetch(`/api/analytics?videoId=${video.id}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - - if (response.ok) { - const responseData = await response.json(); - return { videoId: video.id, count: responseData.count || 0 }; - } - return { videoId: video.id, count: 0 }; - } catch (error) { - console.warn( - `Failed to fetch analytics for video ${video.id}:`, - error - ); - return { videoId: video.id, count: 0 }; - } - }); - - const results = await Promise.allSettled(analyticsPromises); - const analyticsData: Record = {}; - - results.forEach((result) => { - if (result.status === "fulfilled" && result.value) { - analyticsData[result.value.videoId] = result.value.count; - } - }); - return analyticsData; - }, - refetchOnWindowFocus: false, - refetchOnMount: true, - }); - - const [isUploading, uploadingCapId] = useUploadingStatus(); - const visibleVideos = useMemo( - () => - isUploading && uploadingCapId - ? initialVideos.filter((video) => video.id !== uploadingCapId) - : initialVideos, - [initialVideos, isUploading, uploadingCapId] - ); - - const analytics = analyticsData || {}; - - return ( - <> -
-

Videos

-
-
- {visibleVideos.length === 0 && !isUploading ? ( -

- No videos in this folder yet. Drag and drop into the folder or - upload. -

- ) : ( - <> - {isUploading && ( - - )} - {visibleVideos.map((video) => ( - 0} - isDeleting={isDeletingCaps || isDeletingCap} - onSelectToggle={() => handleCapSelection(video.id)} - onDelete={() => { - if (selectedCaps.length > 0) { - deleteCaps(selectedCaps); - } else { - deleteCap(video.id); - } - }} - /> - ))} - - )} -
- deleteCaps(selectedCaps)} - isDeleting={isDeletingCaps || isDeletingCap} - /> - - ); + const router = useRouter(); + const { user } = useDashboardContext(); + + const [selectedCaps, setSelectedCaps] = useState([]); + const previousCountRef = useRef(0); + + const { mutate: deleteCaps, isPending: isDeletingCaps } = useEffectMutation({ + mutationFn: Effect.fn(function* (ids: Video.VideoId[]) { + if (ids.length === 0) return; + + const rpc = yield* Rpc; + + const fiber = yield* Effect.gen(function* () { + const results = yield* Effect.all( + ids.map((id) => rpc.VideoDelete(id).pipe(Effect.exit)), + { concurrency: 10 }, + ); + + const successCount = results.filter(Exit.isSuccess).length; + + const errorCount = ids.length - successCount; + + if (successCount > 0 && errorCount > 0) { + return { success: successCount, error: errorCount }; + } else if (successCount > 0) { + return { success: successCount }; + } else { + return yield* Effect.fail( + new Error( + `Failed to delete ${errorCount} cap${errorCount === 1 ? "" : "s"}`, + ), + ); + } + }).pipe(Effect.fork); + + toast.promise(Effect.runPromise(fiber.await.pipe(Effect.flatten)), { + loading: `Deleting ${ids.length} cap${ids.length === 1 ? "" : "s"}...`, + success: (data) => { + if (data.error) { + return `Successfully deleted ${data.success} cap${ + data.success === 1 ? "" : "s" + }, but failed to delete ${data.error} cap${ + data.error === 1 ? "" : "s" + }`; + } + return `Successfully deleted ${data.success} cap${ + data.success === 1 ? "" : "s" + }`; + }, + error: (error) => + error.message || "An error occurred while deleting caps", + }); + + return yield* fiber.await.pipe(Effect.flatten); + }), + onSuccess: () => { + setSelectedCaps([]); + router.refresh(); + }, + }); + + const { mutate: deleteCap, isPending: isDeletingCap } = useEffectMutation({ + mutationFn: (id: Video.VideoId) => withRpc((r) => r.VideoDelete(id)), + onSuccess: () => { + toast.success("Cap deleted successfully"); + router.refresh(); + }, + onError: () => { + toast.error("Failed to delete cap"); + }, + }); + + const handleCapSelection = (capId: Video.VideoId) => { + setSelectedCaps((prev) => { + const newSelection = prev.includes(capId) + ? prev.filter((id) => id !== capId) + : [...prev, capId]; + + previousCountRef.current = prev.length; + + return newSelection; + }); + }; + + const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({ + queryKey: ["analytics", initialVideos.map((video) => video.id)], + queryFn: async () => { + if (!dubApiKeyEnabled || initialVideos.length === 0) { + return {}; + } + + const analyticsPromises = initialVideos.map(async (video) => { + try { + const response = await fetch(`/api/analytics?videoId=${video.id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.ok) { + const responseData = await response.json(); + return { videoId: video.id, count: responseData.count || 0 }; + } + return { videoId: video.id, count: 0 }; + } catch (error) { + console.warn( + `Failed to fetch analytics for video ${video.id}:`, + error, + ); + return { videoId: video.id, count: 0 }; + } + }); + + const results = await Promise.allSettled(analyticsPromises); + const analyticsData: Record = {}; + + results.forEach((result) => { + if (result.status === "fulfilled" && result.value) { + analyticsData[result.value.videoId] = result.value.count; + } + }); + return analyticsData; + }, + refetchOnWindowFocus: false, + refetchOnMount: true, + }); + + const [isUploading, uploadingCapId] = useUploadingStatus(); + const visibleVideos = useMemo( + () => + isUploading && uploadingCapId + ? initialVideos.filter((video) => video.id !== uploadingCapId) + : initialVideos, + [initialVideos, isUploading, uploadingCapId], + ); + + const analytics = analyticsData || {}; + + return ( + <> +
+

Videos

+
+
+ {visibleVideos.length === 0 && !isUploading ? ( +

+ No videos in this folder yet. Drag and drop into the folder or + upload. +

+ ) : ( + <> + {isUploading && ( + + )} + {visibleVideos.map((video) => ( + 0} + isDeleting={isDeletingCaps || isDeletingCap} + onSelectToggle={() => handleCapSelection(video.id)} + onDelete={() => { + if (selectedCaps.length > 0) { + deleteCaps(selectedCaps); + } else { + deleteCap(video.id); + } + }} + /> + ))} + + )} +
+ deleteCaps(selectedCaps)} + isDeleting={isDeletingCaps || isDeletingCap} + /> + + ); } diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx index bd5a99cd12..2519774669 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx @@ -17,297 +17,297 @@ import { AddVideosToOrganizationDialog } from "./components/AddVideosToOrganizat import { EmptySharedCapState } from "./components/EmptySharedCapState"; import { MembersIndicator } from "./components/MembersIndicator"; import { - OrganizationIndicator, - type OrganizationMemberData, + OrganizationIndicator, + type OrganizationMemberData, } from "./components/OrganizationIndicator"; import { SharedCapCard } from "./components/SharedCapCard"; import type { SpaceMemberData } from "./page"; type SharedVideoData = { - id: Video.VideoId; - ownerId: string; - name: string; - createdAt: Date; - totalComments: number; - totalReactions: number; - ownerName: string | null; - metadata?: VideoMetadata; - hasActiveUpload: boolean | undefined; + id: Video.VideoId; + ownerId: string; + name: string; + createdAt: Date; + totalComments: number; + totalReactions: number; + ownerName: string | null; + metadata?: VideoMetadata; + hasActiveUpload: boolean | undefined; }[]; type SpaceData = { - id: string; - name: string; - organizationId: string; - createdById: string; + id: string; + name: string; + organizationId: string; + createdById: string; }; export const SharedCaps = ({ - data, - count, - spaceData, - spaceMembers, - organizationMembers, - currentUserId, - folders, - dubApiKeyEnabled, - organizationData, + data, + count, + spaceData, + spaceMembers, + organizationMembers, + currentUserId, + folders, + dubApiKeyEnabled, + organizationData, }: { - data: SharedVideoData; - count: number; - dubApiKeyEnabled: boolean; - spaceData?: SpaceData; - hideSharedWith?: boolean; - spaceMembers?: SpaceMemberData[]; - organizationMembers?: OrganizationMemberData[]; - currentUserId?: string; - folders?: FolderDataType[]; - organizationData?: { - id: string; - name: string; - ownerId: string; - }; + data: SharedVideoData; + count: number; + dubApiKeyEnabled: boolean; + spaceData?: SpaceData; + hideSharedWith?: boolean; + spaceMembers?: SpaceMemberData[]; + organizationMembers?: OrganizationMemberData[]; + currentUserId?: string; + folders?: FolderDataType[]; + organizationData?: { + id: string; + name: string; + ownerId: string; + }; }) => { - const params = useSearchParams(); - const router = useRouter(); - const page = Number(params.get("page")) || 1; - const { activeOrganization } = useDashboardContext(); - const limit = 15; - const [openNewFolderDialog, setOpenNewFolderDialog] = useState(false); - const totalPages = Math.ceil(count / limit); - const [isDraggingCap, setIsDraggingCap] = useState({ - isOwner: false, - isDragging: false, - }); - const [isAddVideosDialogOpen, setIsAddVideosDialogOpen] = useState(false); - const [ - isAddOrganizationVideosDialogOpen, - setIsAddOrganizationVideosDialogOpen, - ] = useState(false); + const params = useSearchParams(); + const router = useRouter(); + const page = Number(params.get("page")) || 1; + const { activeOrganization } = useDashboardContext(); + const limit = 15; + const [openNewFolderDialog, setOpenNewFolderDialog] = useState(false); + const totalPages = Math.ceil(count / limit); + const [isDraggingCap, setIsDraggingCap] = useState({ + isOwner: false, + isDragging: false, + }); + const [isAddVideosDialogOpen, setIsAddVideosDialogOpen] = useState(false); + const [ + isAddOrganizationVideosDialogOpen, + setIsAddOrganizationVideosDialogOpen, + ] = useState(false); - const isSpaceOwner = spaceData?.createdById === currentUserId; - const isOrgOwner = organizationData?.ownerId === currentUserId; + const isSpaceOwner = spaceData?.createdById === currentUserId; + const isOrgOwner = organizationData?.ownerId === currentUserId; - const spaceMemberCount = spaceMembers?.length || 0; + const spaceMemberCount = spaceMembers?.length || 0; - const organizationMemberCount = organizationMembers?.length || 0; + const organizationMemberCount = organizationMembers?.length || 0; - const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({ - queryKey: ["analytics", data.map((video) => video.id)], - queryFn: async () => { - if (!dubApiKeyEnabled || data.length === 0) { - return {}; - } + const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({ + queryKey: ["analytics", data.map((video) => video.id)], + queryFn: async () => { + if (!dubApiKeyEnabled || data.length === 0) { + return {}; + } - const analyticsPromises = data.map(async (video) => { - try { - const response = await fetch(`/api/analytics?videoId=${video.id}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); + const analyticsPromises = data.map(async (video) => { + try { + const response = await fetch(`/api/analytics?videoId=${video.id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); - if (response.ok) { - const responseData = await response.json(); - return { videoId: video.id, count: responseData.count || 0 }; - } - return { videoId: video.id, count: 0 }; - } catch (error) { - console.warn( - `Failed to fetch analytics for video ${video.id}:`, - error - ); - return { videoId: video.id, count: 0 }; - } - }); + if (response.ok) { + const responseData = await response.json(); + return { videoId: video.id, count: responseData.count || 0 }; + } + return { videoId: video.id, count: 0 }; + } catch (error) { + console.warn( + `Failed to fetch analytics for video ${video.id}:`, + error, + ); + return { videoId: video.id, count: 0 }; + } + }); - const results = await Promise.allSettled(analyticsPromises); - const analyticsData: Record = {}; + const results = await Promise.allSettled(analyticsPromises); + const analyticsData: Record = {}; - results.forEach((result) => { - if (result.status === "fulfilled" && result.value) { - analyticsData[result.value.videoId] = result.value.count; - } - }); + results.forEach((result) => { + if (result.status === "fulfilled" && result.value) { + analyticsData[result.value.videoId] = result.value.count; + } + }); - return analyticsData; - }, - staleTime: 30000, // 30 seconds - refetchOnWindowFocus: false, - }); + return analyticsData; + }, + staleTime: 30000, // 30 seconds + refetchOnWindowFocus: false, + }); - const analytics = analyticsData || {}; + const analytics = analyticsData || {}; - const handleVideosAdded = () => { - router.refresh(); - }; + const handleVideosAdded = () => { + router.refresh(); + }; - if (data.length === 0 && folders?.length === 0) { - return ( -
- {spaceData && spaceMembers && ( - setIsAddVideosDialogOpen(true)} - /> - )} - {organizationData && organizationMembers && !spaceData && ( - setIsAddOrganizationVideosDialogOpen(true)} - /> - )} - setIsAddVideosDialogOpen(true) - : () => setIsAddOrganizationVideosDialogOpen(true) - } - /> - {spaceData && ( - setIsAddVideosDialogOpen(false)} - spaceId={spaceData.id} - spaceName={spaceData.name} - onVideosAdded={handleVideosAdded} - /> - )} - {organizationData && ( - setIsAddOrganizationVideosDialogOpen(false)} - organizationId={organizationData.id} - organizationName={organizationData.name} - onVideosAdded={handleVideosAdded} - /> - )} -
- ); - } + if (data.length === 0 && folders?.length === 0) { + return ( +
+ {spaceData && spaceMembers && ( + setIsAddVideosDialogOpen(true)} + /> + )} + {organizationData && organizationMembers && !spaceData && ( + setIsAddOrganizationVideosDialogOpen(true)} + /> + )} + setIsAddVideosDialogOpen(true) + : () => setIsAddOrganizationVideosDialogOpen(true) + } + /> + {spaceData && ( + setIsAddVideosDialogOpen(false)} + spaceId={spaceData.id} + spaceName={spaceData.name} + onVideosAdded={handleVideosAdded} + /> + )} + {organizationData && ( + setIsAddOrganizationVideosDialogOpen(false)} + organizationId={organizationData.id} + organizationName={organizationData.name} + onVideosAdded={handleVideosAdded} + /> + )} +
+ ); + } - return ( -
- {isDraggingCap.isDragging && ( -
-
-
- -

- {isDraggingCap.isOwner - ? " Drag to a space to share or folder to move" - : "Only the video owner can drag and move the video"} -

-
-
-
- )} - -
- {spaceData && spaceMembers && ( - setIsAddVideosDialogOpen(true)} - /> - )} - {organizationData && organizationMembers && !spaceData && ( - setIsAddOrganizationVideosDialogOpen(true)} - /> - )} - {spaceData && ( - setIsAddVideosDialogOpen(false)} - spaceId={spaceData.id} - spaceName={spaceData.name} - onVideosAdded={handleVideosAdded} - /> - )} - {organizationData && ( - setIsAddOrganizationVideosDialogOpen(false)} - organizationId={organizationData.id} - organizationName={organizationData.name} - onVideosAdded={handleVideosAdded} - /> - )} - -
- {folders && folders.length > 0 && ( - <> -

Folders

-
- {folders.map((folder) => ( - - ))} -
- - )} + return ( +
+ {isDraggingCap.isDragging && ( +
+
+
+ +

+ {isDraggingCap.isOwner + ? " Drag to a space to share or folder to move" + : "Only the video owner can drag and move the video"} +

+
+
+
+ )} + +
+ {spaceData && spaceMembers && ( + setIsAddVideosDialogOpen(true)} + /> + )} + {organizationData && organizationMembers && !spaceData && ( + setIsAddOrganizationVideosDialogOpen(true)} + /> + )} + {spaceData && ( + setIsAddVideosDialogOpen(false)} + spaceId={spaceData.id} + spaceName={spaceData.name} + onVideosAdded={handleVideosAdded} + /> + )} + {organizationData && ( + setIsAddOrganizationVideosDialogOpen(false)} + organizationId={organizationData.id} + organizationName={organizationData.name} + onVideosAdded={handleVideosAdded} + /> + )} + +
+ {folders && folders.length > 0 && ( + <> +

Folders

+
+ {folders.map((folder) => ( + + ))} +
+ + )} -

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) && ( -
- -
- )} -
- ); +

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) && ( +
+ +
+ )} +
+ ); }; 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 3b3dad6999..943c3484a0 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/components/SharedCapCard.tsx @@ -5,76 +5,76 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { CapCard } from "../../../caps/components/CapCard/CapCard"; interface SharedCapCardProps { - cap: { - id: Video.VideoId; - ownerId: string; - name: string; - createdAt: Date; - totalComments: number; - totalReactions: number; - ownerName: string | null; - metadata?: VideoMetadata; - hasActiveUpload: boolean | undefined; - }; - analytics: number; - isLoadingAnalytics: boolean; - organizationName: string; - userId?: string; - hideSharedStatus?: boolean; - spaceName?: string; - onDragStart?: () => void; - onDragEnd?: () => void; + cap: { + id: Video.VideoId; + ownerId: string; + name: string; + createdAt: Date; + totalComments: number; + totalReactions: number; + ownerName: string | null; + metadata?: VideoMetadata; + hasActiveUpload: boolean | undefined; + }; + analytics: number; + isLoadingAnalytics: boolean; + organizationName: string; + userId?: string; + hideSharedStatus?: boolean; + spaceName?: string; + onDragStart?: () => void; + onDragEnd?: () => void; } export const SharedCapCard: React.FC = ({ - cap, - analytics, - organizationName, - userId, - hideSharedStatus, - isLoadingAnalytics, - spaceName, - onDragStart, - onDragEnd, + cap, + analytics, + organizationName, + userId, + hideSharedStatus, + isLoadingAnalytics, + spaceName, + onDragStart, + onDragEnd, }) => { - const displayCount = - analytics === 0 - ? Math.max(cap.totalComments, cap.totalReactions) - : analytics; - const isOwner = userId === cap.ownerId; + const displayCount = + analytics === 0 + ? Math.max(cap.totalComments, cap.totalReactions) + : analytics; + const isOwner = userId === cap.ownerId; - return ( -
- -
- {cap.ownerName && ( -
- - {cap.ownerName} -
- )} - {isOwner && ( -
- -

- Shared with{" "} - - {spaceName || organizationName} - -

-
- )} -
-
-
- ); + return ( +
+ +
+ {cap.ownerName && ( +
+ + {cap.ownerName} +
+ )} + {isOwner && ( +
+ +

+ Shared with{" "} + + {spaceName || organizationName} + +

+
+ )} +
+
+
+ ); }; diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx index b8c0ff6414..065ca71454 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/folder/[folderId]/page.tsx @@ -3,79 +3,79 @@ import { serverEnv } from "@cap/env"; import type { Folder } from "@cap/web-domain"; import FolderCard from "@/app/(org)/dashboard/caps/components/Folder"; import { - getChildFolders, - getFolderBreadcrumb, - getVideosByFolderId, + getChildFolders, + getFolderBreadcrumb, + getVideosByFolderId, } from "@/lib/folder"; import { - BreadcrumbItem, - ClientMyCapsLink, - NewSubfolderButton, + BreadcrumbItem, + ClientMyCapsLink, + NewSubfolderButton, } from "../../../../folder/[id]/components"; import FolderVideosSection from "../../../../folder/[id]/components/FolderVideosSection"; const FolderPage = async ({ - params, + params, }: { - params: { spaceId: string; folderId: Folder.FolderId }; + params: { spaceId: string; folderId: Folder.FolderId }; }) => { - const user = await getCurrentUser(); - if (!user) return; + const user = await getCurrentUser(); + if (!user) return; - const [childFolders, breadcrumb, videosData] = await Promise.all([ - getChildFolders(params.folderId), - getFolderBreadcrumb(params.folderId), - getVideosByFolderId(params.folderId), - ]); - const userId = user?.id as string; + const [childFolders, breadcrumb, videosData] = await Promise.all([ + getChildFolders(params.folderId), + getFolderBreadcrumb(params.folderId), + getVideosByFolderId(params.folderId), + ]); + const userId = user?.id as string; - return ( -
-
- -
-
-
- - {breadcrumb.map((folder, index) => ( -
-

/

- -
- ))} -
-
- {/* Display Child Folders */} - {childFolders.length > 0 && ( - <> -

Subfolders

-
- {childFolders.map((folder) => ( - - ))} -
- - )} - {/* Display Videos */} - -
- ); + return ( +
+
+ +
+
+
+ + {breadcrumb.map((folder, index) => ( +
+

/

+ +
+ ))} +
+
+ {/* Display Child Folders */} + {childFolders.length > 0 && ( + <> +

Subfolders

+
+ {childFolders.map((folder) => ( + + ))} +
+ + )} + {/* Display Videos */} + +
+ ); }; export default FolderPage; diff --git a/apps/web/app/api/settings/onboarding/route.ts b/apps/web/app/api/settings/onboarding/route.ts index 983f1bed76..5036599a3d 100644 --- a/apps/web/app/api/settings/onboarding/route.ts +++ b/apps/web/app/api/settings/onboarding/route.ts @@ -47,7 +47,7 @@ export async function POST(request: NextRequest) { ) .limit(1); - const isMemberOfOrganization = memberButNotOwner.length > 0;; + const isMemberOfOrganization = memberButNotOwner.length > 0; const [organization] = await db() .select() From 2268518b96a8ef9d4ba5383a33d41da1deebacad Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Thu, 25 Sep 2025 12:24:51 +0300 Subject: [PATCH 3/4] formatting --- .../app/(org)/dashboard/caps/components/CapCard/CapCard.tsx | 2 +- .../(org)/dashboard/caps/components/CapCard/CapCardButton.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 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 5cf04f73a0..b1d519666e 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -37,8 +37,8 @@ import { withRpc } from "@/lib/Rpcs"; import { PasswordDialog } from "../PasswordDialog"; import { SharingDialog } from "../SharingDialog"; import { CapCardAnalytics } from "./CapCardAnalytics"; -import { CapCardContent } from "./CapCardContent"; import { CapCardButton } from "./CapCardButton"; +import { CapCardContent } from "./CapCardContent"; export interface CapCardProps extends PropsWithChildren { cap: { 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 1b97d73e5b..6489430682 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx @@ -1,7 +1,7 @@ import { Button } from "@cap/ui"; -import { Tooltip } from "@/components/Tooltip"; -import { type ReactNode, type MouseEvent } from "react"; import clsx from "clsx"; +import type { MouseEvent, ReactNode } from "react"; +import { Tooltip } from "@/components/Tooltip"; interface CapCardButtonProps { tooltipContent: string; From c36ac22662c80ef577050ea3228b6303f444426f Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Thu, 25 Sep 2025 12:32:04 +0300 Subject: [PATCH 4/4] unused --- .../app/(org)/dashboard/caps/components/CapCard/CapCard.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 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 b1d519666e..bf979e2c32 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -106,8 +106,7 @@ export const CapCard = ({ ); const [copyPressed, setCopyPressed] = useState(false); const [isDragging, setIsDragging] = useState(false); - const { isSubscribed, setUpgradeModalOpen, activeSpace, user } = - useDashboardContext(); + const { isSubscribed, setUpgradeModalOpen } = useDashboardContext(); const [confirmOpen, setConfirmOpen] = useState(false);