Skip to content

Commit 7bdce3b

Browse files
committed
feat: Share video to space directly from share page
1 parent 3d2f578 commit 7bdce3b

File tree

4 files changed

+143
-4
lines changed

4 files changed

+143
-4
lines changed

apps/web/app/dashboard/caps/components/SharingDialog.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@ export const SharingDialog: React.FC<SharingDialogProps> = ({
3030
new Set(sharedSpaces.map((space) => space.id))
3131
);
3232
const [searchTerm, setSearchTerm] = useState("");
33+
const [initialSelectedSpaces, setInitialSelectedSpaces] = useState<
34+
Set<string>
35+
>(new Set(sharedSpaces.map((space) => space.id)));
3336

3437
useEffect(() => {
3538
if (isOpen) {
36-
setSelectedSpaces(new Set(sharedSpaces.map((space) => space.id)));
39+
const currentSpaceIds = new Set(sharedSpaces.map((space) => space.id));
40+
setSelectedSpaces(currentSpaceIds);
41+
setInitialSelectedSpaces(currentSpaceIds);
3742
setSearchTerm("");
3843
}
3944
}, [isOpen, sharedSpaces]);
@@ -62,8 +67,37 @@ export const SharingDialog: React.FC<SharingDialogProps> = ({
6267
throw new Error("Failed to update sharing settings");
6368
}
6469

65-
toast.success("Sharing settings updated successfully");
66-
onSharingUpdated(Array.from(selectedSpaces));
70+
const newSelectedSpaces = Array.from(selectedSpaces);
71+
const initialSpaces = Array.from(initialSelectedSpaces);
72+
73+
const addedSpaceIds = newSelectedSpaces.filter(
74+
(id) => !initialSpaces.includes(id)
75+
);
76+
const removedSpaceIds = initialSpaces.filter(
77+
(id) => !newSelectedSpaces.includes(id)
78+
);
79+
80+
if (addedSpaceIds.length === 1 && removedSpaceIds.length === 0) {
81+
const addedSpaceName = userSpaces?.find(
82+
(space) => space.id === addedSpaceIds[0]
83+
)?.name;
84+
toast.success(`Shared to ${addedSpaceName}`);
85+
} else if (removedSpaceIds.length === 1 && addedSpaceIds.length === 0) {
86+
const removedSpaceName = sharedSpaces.find(
87+
(space) => space.id === removedSpaceIds[0]
88+
)?.name;
89+
toast.success(`Unshared from ${removedSpaceName}`);
90+
} else if (addedSpaceIds.length > 0 && removedSpaceIds.length === 0) {
91+
toast.success(`Shared to ${addedSpaceIds.length} spaces`);
92+
} else if (removedSpaceIds.length > 0 && addedSpaceIds.length === 0) {
93+
toast.success(`Unshared from ${removedSpaceIds.length} spaces`);
94+
} else if (addedSpaceIds.length > 0 && removedSpaceIds.length > 0) {
95+
toast.success(`Sharing settings updated`);
96+
} else {
97+
toast.success("No changes to sharing settings");
98+
}
99+
100+
onSharingUpdated(newSelectedSpaces);
67101
onClose();
68102
} catch (error) {
69103
console.error("Error updating sharing settings:", error);

apps/web/app/s/[videoId]/Share.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface Analytics {
2424
type VideoWithSpaceInfo = typeof videos.$inferSelect & {
2525
spaceMembers?: string[];
2626
spaceId?: string;
27+
sharedSpaces?: { id: string; name: string }[];
2728
};
2829

2930
interface ShareProps {
@@ -37,6 +38,7 @@ interface ShareProps {
3738
};
3839
customDomain: string | null;
3940
domainVerified: boolean;
41+
userSpaces?: { id: string; name: string }[];
4042
}
4143

4244
export const Share: React.FC<ShareProps> = ({
@@ -46,6 +48,7 @@ export const Share: React.FC<ShareProps> = ({
4648
initialAnalytics,
4749
customDomain,
4850
domainVerified,
51+
userSpaces = [],
4952
}) => {
5053
const [analytics, setAnalytics] = useState(initialAnalytics);
5154
const effectiveDate = data.metadata?.customCreatedAt
@@ -93,6 +96,8 @@ export const Share: React.FC<ShareProps> = ({
9396
user={user}
9497
customDomain={customDomain}
9598
domainVerified={domainVerified}
99+
sharedSpaces={data.sharedSpaces || []}
100+
userSpaces={userSpaces}
96101
/>
97102

98103
<div className="mt-4">

apps/web/app/s/[videoId]/_components/ShareHeader.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,39 @@ import { clientEnv, NODE_ENV } from "@cap/env";
66
import { Button } from "@cap/ui";
77
import { isUserOnProPlan } from "@cap/utils";
88
import { Copy, Globe2 } from "lucide-react";
9+
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
10+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
911
import moment from "moment";
1012
import { useRouter } from "next/navigation";
1113
import { useState } from "react";
1214
import { toast } from "react-hot-toast";
15+
import { SharingDialog } from "@/app/dashboard/caps/components/SharingDialog";
16+
import clsx from "clsx";
1317

1418
export const ShareHeader = ({
1519
data,
1620
user,
1721
customDomain,
1822
domainVerified,
23+
sharedSpaces = [],
24+
userSpaces = [],
1925
}: {
2026
data: typeof videos.$inferSelect;
2127
user: typeof userSelectProps | null;
2228
customDomain: string | null;
2329
domainVerified: boolean;
30+
sharedSpaces?: { id: string; name: string }[];
31+
userSpaces?: { id: string; name: string }[];
2432
}) => {
2533
const { push, refresh } = useRouter();
2634
const [isEditing, setIsEditing] = useState(false);
2735
const [title, setTitle] = useState(data.name);
2836
const [isDownloading, setIsDownloading] = useState(false);
2937
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
38+
const [isSharingDialogOpen, setIsSharingDialogOpen] = useState(false);
39+
const [currentSharedSpaces, setCurrentSharedSpaces] = useState(sharedSpaces);
40+
41+
const isOwner = user !== null && user.id.toString() === data.ownerId;
3042

3143
const handleBlur = async () => {
3244
setIsEditing(false);
@@ -72,8 +84,55 @@ export const ShareHeader = ({
7284
})
7385
: false;
7486

87+
const handleSharingUpdated = (updatedSharedSpaces: string[]) => {
88+
setCurrentSharedSpaces(
89+
userSpaces?.filter((space) => updatedSharedSpaces.includes(space.id))
90+
);
91+
refresh();
92+
};
93+
94+
const renderSharedStatus = () => {
95+
const baseClassName =
96+
"text-sm text-gray-10 transition-colors duration-200 flex items-center";
97+
98+
if (isOwner) {
99+
if (currentSharedSpaces?.length === 0) {
100+
return (
101+
<p
102+
className={clsx(baseClassName, "hover:text-gray-12 cursor-pointer")}
103+
onClick={() => setIsSharingDialogOpen(true)}
104+
>
105+
Not shared{" "}
106+
<FontAwesomeIcon className="ml-2 size-2.5" icon={faChevronDown} />
107+
</p>
108+
);
109+
} else {
110+
return (
111+
<p
112+
className={clsx(baseClassName, "hover:text-gray-12 cursor-pointer")}
113+
onClick={() => setIsSharingDialogOpen(true)}
114+
>
115+
Shared{" "}
116+
<FontAwesomeIcon className="ml-1 size-2.5" icon={faChevronDown} />
117+
</p>
118+
);
119+
}
120+
} else {
121+
return <p className={baseClassName}>Shared with you</p>;
122+
}
123+
};
124+
75125
return (
76126
<>
127+
<SharingDialog
128+
isOpen={isSharingDialogOpen}
129+
onClose={() => setIsSharingDialogOpen(false)}
130+
capId={data.id}
131+
capName={data.name}
132+
sharedSpaces={currentSharedSpaces || []}
133+
userSpaces={userSpaces}
134+
onSharingUpdated={handleSharingUpdated}
135+
/>
77136
<div>
78137
<div className="space-x-0 md:flex md:items-center md:justify-between md:space-x-6">
79138
<div className="items-center md:flex md:justify-between md:space-x-6">
@@ -104,7 +163,8 @@ export const ShareHeader = ({
104163
</h1>
105164
)}
106165
</div>
107-
<p className="text-sm text-gray-10">
166+
{user && renderSharedStatus()}
167+
<p className="text-sm text-gray-10 mt-1">
108168
{moment(data.createdAt).fromNow()}
109169
</p>
110170
</div>

apps/web/app/s/[videoId]/page.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type VideoWithSpace = typeof videos.$inferSelect & {
3636
} | null;
3737
spaceMembers?: string[];
3838
spaceId?: string;
39+
sharedSpaces?: { id: string; name: string }[];
3940
};
4041

4142
type SpaceMember = {
@@ -280,6 +281,43 @@ export default async function ShareVideoPage(props: Props) {
280281
}
281282
}
282283

284+
const sharedSpacesData = await db
285+
.select({
286+
id: spaces.id,
287+
name: spaces.name,
288+
})
289+
.from(spaces)
290+
.innerJoin(sharedVideos, eq(spaces.id, sharedVideos.spaceId))
291+
.where(eq(sharedVideos.videoId, videoId));
292+
293+
let userSpaces: { id: string; name: string }[] = [];
294+
if (userId) {
295+
const ownedSpaces = await db
296+
.select({
297+
id: spaces.id,
298+
name: spaces.name,
299+
})
300+
.from(spaces)
301+
.where(eq(spaces.ownerId, userId));
302+
303+
const memberSpaces = await db
304+
.select({
305+
id: spaces.id,
306+
name: spaces.name,
307+
})
308+
.from(spaces)
309+
.innerJoin(spaceMembers, eq(spaces.id, spaceMembers.spaceId))
310+
.where(eq(spaceMembers.userId, userId));
311+
312+
const allSpaces = [...ownedSpaces, ...memberSpaces];
313+
const uniqueSpaceIds = new Set();
314+
userSpaces = allSpaces.filter((space) => {
315+
if (uniqueSpaceIds.has(space.id)) return false;
316+
uniqueSpaceIds.add(space.id);
317+
return true;
318+
});
319+
}
320+
283321
const membersList = video.sharedSpace?.spaceId
284322
? await db
285323
.select({
@@ -293,6 +331,7 @@ export default async function ShareVideoPage(props: Props) {
293331
...video,
294332
spaceMembers: membersList.map((member) => member.userId),
295333
spaceId: video.sharedSpace?.spaceId ?? undefined,
334+
sharedSpaces: sharedSpacesData,
296335
};
297336

298337
return (
@@ -303,6 +342,7 @@ export default async function ShareVideoPage(props: Props) {
303342
initialAnalytics={initialAnalytics}
304343
customDomain={customDomain}
305344
domainVerified={domainVerified}
345+
userSpaces={userSpaces}
306346
/>
307347
);
308348
}

0 commit comments

Comments
 (0)