diff --git a/apps/web/app/(org)/dashboard/caps/Caps.tsx b/apps/web/app/(org)/dashboard/caps/Caps.tsx index 10869c80a0..7c98c8b434 100644 --- a/apps/web/app/(org)/dashboard/caps/Caps.tsx +++ b/apps/web/app/(org)/dashboard/caps/Caps.tsx @@ -266,7 +266,7 @@ export const Caps = ({ [data, isUploading, uploadingCapId], ); - if (count === 0) return ; + if (count === 0 && folders.length === 0) return ; return (
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 6fc8db838e..a907272801 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCard.tsx @@ -18,6 +18,7 @@ import { faGear, faLink, faLock, + faShare, faTrash, faUnlock, faVideo, @@ -42,6 +43,7 @@ import { } from "@/components/VideoThumbnail"; import { useEffectMutation } from "@/lib/EffectRuntime"; import { withRpc } from "@/lib/Rpcs"; +import { usePublicEnv } from "@/utils/public-env"; import { PasswordDialog } from "../PasswordDialog"; import { SettingsDialog } from "../SettingsDialog"; import { SharingDialog } from "../SharingDialog"; @@ -121,6 +123,8 @@ export const CapCard = ({ const [passwordProtected, setPasswordProtected] = useState( cap.hasPassword || false, ); + const { webUrl } = usePublicEnv(); + const [copyPressed, setCopyPressed] = useState(false); const [isDragging, setIsDragging] = useState(false); const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false); @@ -345,72 +349,74 @@ export const CapCard = ({ "top-2 right-2 flex-col gap-2 z-[51]", )} > - {isOwner ? ( - { - e.stopPropagation(); - setIsSettingsDialogOpen(true); - }} - className="delay-0" - icon={() => { - return ; - }} - /> - ) : ( - { - e.stopPropagation(); - handleDownload(); - }} - className="delay-0" - icon={() => ( - - )} - /> - )} { e.stopPropagation(); - handleCopy( - buildEnv.NEXT_PUBLIC_IS_CAP && - NODE_ENV === "production" && - customDomain && - domainVerified - ? `https://${customDomain}/s/${cap.id}` - : buildEnv.NEXT_PUBLIC_IS_CAP && NODE_ENV === "production" - ? `https://cap.link/${cap.id}` - : `${location.origin}/s/${cap.id}`, - ); + setIsSharingDialogOpen(true); }} className="delay-0" - icon={() => { - return !copyPressed ? ( - - ) : ( - - - - ); + icon={} + /> + + { + e.stopPropagation(); + handleDownload(); }} + className="delay-0" + icon={} /> + {!isOwner && ( + { + e.stopPropagation(); + handleCopy( + NODE_ENV === "development" + ? `${webUrl}/s/${cap.id}` + : buildEnv.NEXT_PUBLIC_IS_CAP && + customDomain && + domainVerified + ? `https://${customDomain}/s/${cap.id}` + : buildEnv.NEXT_PUBLIC_IS_CAP && + !customDomain && + !domainVerified + ? `https://cap.link/${cap.id}` + : `${webUrl}/s/${cap.id}`, + ); + }} + className="delay-0" + icon={ + <> + {!copyPressed ? ( + + ) : ( + + + + )} + + } + /> + )} + {isOwner && ( @@ -418,9 +424,7 @@ export const CapCard = ({ ( - - )} + icon={} />
@@ -432,12 +436,33 @@ export const CapCard = ({ { e.stopPropagation(); - handleDownload(); + setIsSettingsDialogOpen(true); + }} + className="flex gap-2 items-center rounded-lg" + > + +

Settings

+
+ { + e.stopPropagation(); + handleCopy( + buildEnv.NEXT_PUBLIC_IS_CAP && + NODE_ENV === "production" && + customDomain && + domainVerified + ? `https://${customDomain}/s/${cap.id}` + : buildEnv.NEXT_PUBLIC_IS_CAP && + NODE_ENV === "production" + ? `https://cap.link/${cap.id}` + : `${location.origin}/s/${cap.id}`, + ); + toast.success("Link copied to clipboard"); }} className="flex gap-2 items-center rounded-lg" > - -

Download

+ +

Copy link

{ 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 61868a1f9a..cf0724ed93 100644 --- a/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/CapCard/CapCardButton.tsx @@ -2,7 +2,8 @@ import { Button } from "@cap/ui"; import clsx from "clsx"; -import type { MouseEvent, ReactNode } from "react"; +import type { MouseEvent } from "react"; +import React from "react"; import { Tooltip } from "@/components/Tooltip"; interface CapCardButtonProps { @@ -10,7 +11,7 @@ interface CapCardButtonProps { onClick?: (e: MouseEvent) => void; disabled?: boolean; className: string; - icon: () => ReactNode; + icon: React.JSX.Element; asChild?: boolean; } @@ -36,7 +37,9 @@ export const CapCardButton = ({ size="sm" aria-label={tooltipContent} > - {icon()} + {React.cloneElement(icon, { + className: clsx(icon.props.className, "size-3.5"), + })} ); diff --git a/apps/web/app/(org)/dashboard/caps/page.tsx b/apps/web/app/(org)/dashboard/caps/page.tsx index b71d6ce162..754d9665da 100644 --- a/apps/web/app/(org)/dashboard/caps/page.tsx +++ b/apps/web/app/(org)/dashboard/caps/page.tsx @@ -227,6 +227,7 @@ export default async function CapsPage(props: PageProps<"/dashboard/caps">) { .where( and( eq(folders.organizationId, user.activeOrganizationId), + eq(folders.createdById, user.id), isNull(folders.parentId), isNull(folders.spaceId), ), diff --git a/apps/web/app/s/[videoId]/_components/ShareHeader.tsx b/apps/web/app/s/[videoId]/_components/ShareHeader.tsx index 1401f11b8b..50989fb70b 100644 --- a/apps/web/app/s/[videoId]/_components/ShareHeader.tsx +++ b/apps/web/app/s/[videoId]/_components/ShareHeader.tsx @@ -3,13 +3,13 @@ import type { userSelectProps } from "@cap/database/auth/session"; import type { videos } from "@cap/database/schema"; import { buildEnv, NODE_ENV } from "@cap/env"; -import { Button } from "@cap/ui"; +import { Avatar, Button } from "@cap/ui"; import { userIsPro } from "@cap/utils"; import { faChevronDown, faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import clsx from "clsx"; import { Check, Copy, Globe2 } from "lucide-react"; import moment from "moment"; +import Image from "next/image"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "sonner"; @@ -29,7 +29,10 @@ export const ShareHeader = ({ sharedSpaces = [], spacesData = null, }: { - data: typeof videos.$inferSelect; + data: typeof videos.$inferSelect & { + ownerName?: string | null; + ownerImage?: string | null; + }; user: typeof userSelectProps | null; customDomain?: string | null; domainVerified?: boolean; @@ -70,7 +73,8 @@ export const ShareHeader = ({ const handleBlur = async () => { setIsEditing(false); - + const next = title.trim(); + if (next === "" || next === data.name) return; try { await editTitle(data.id, title); toast.success("Video title updated"); @@ -135,9 +139,6 @@ export const ShareHeader = ({ }; const renderSharedStatus = () => { - const baseClassName = - "text-sm text-gray-10 transition-colors duration-200 flex items-center"; - if (isOwner) { const hasSpaceSharing = sharedOrganizations?.length > 0 || effectiveSharedSpaces?.length > 0; @@ -145,27 +146,39 @@ export const ShareHeader = ({ if (!hasSpaceSharing && !isPublic) { return ( -

setIsSharingDialogOpen(true)} > Not shared{" "} -

+ ); } else { return ( -

setIsSharingDialogOpen(true)} > Shared{" "} -

+ ); } } else { - return

Shared with you

; + return ( + + ); } }; @@ -199,8 +212,8 @@ export const ShareHeader = ({
-
-
+
+
{isEditing ? ( ) : (

)}

- {user && renderSharedStatus()} -

- {moment(data.createdAt).fromNow()} -

+
+
+ {data.ownerImage ? ( + {data.ownerName + ) : ( + + )} +
+

{data.ownerName}

+

+ {moment(data.createdAt).fromNow()} +

+
+
+ {user && renderSharedStatus()} +
{user !== null && ( diff --git a/apps/web/app/s/[videoId]/page.tsx b/apps/web/app/s/[videoId]/page.tsx index 94a44f0974..3a6a50987d 100644 --- a/apps/web/app/s/[videoId]/page.tsx +++ b/apps/web/app/s/[videoId]/page.tsx @@ -270,6 +270,8 @@ export default async function ShareVideoPage(props: PageProps<"/s/[videoId]">) { id: videos.id, name: videos.name, ownerId: videos.ownerId, + ownerName: users.name, + ownerImage: users.image, orgId: videos.orgId, createdAt: videos.createdAt, updatedAt: videos.updatedAt, @@ -363,6 +365,8 @@ async function AuthorizedContent({ sharedOrganization: { organizationId: Organisation.OrganisationId } | null; hasPassword: boolean; ownerIsPro?: boolean; + ownerName?: string | null; + ownerImage?: string | null; orgSettings?: OrganizationSettings | null; videoSettings?: OrganizationSettings | null; }; @@ -466,6 +470,8 @@ async function AuthorizedContent({ id: videos.id, name: videos.name, ownerId: videos.ownerId, + ownerName: users.name, + ownerImage: users.image, ownerIsPro: sql`${users.stripeSubscriptionStatus} IN ('active','trialing','complete','paid') OR ${users.thirdPartyStripeSubscriptionId} IS NOT NULL`.mapWith( Boolean,