Skip to content
Merged
20 changes: 11 additions & 9 deletions apps/web/actions/organization/upload-space-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import { spaces } from "@cap/database/schema";
import { serverEnv } from "@cap/env";
import { S3Buckets } from "@cap/web-backend";
import type { Space } from "@cap/web-domain";
import { ImageUpload, type Space } from "@cap/web-domain";
import { eq } from "drizzle-orm";
import { Effect, Option } from "effect";
import { revalidatePath } from "next/cache";
Expand Down Expand Up @@ -54,9 +53,11 @@ export async function uploadSpaceIcon(

// Prepare new file key
const fileExtension = file.name.split(".").pop();
const fileKey = `organizations/${
space.organizationId
}/spaces/${spaceId}/icon-${Date.now()}.${fileExtension}`;
const fileKey = ImageUpload.ImageKey.make(
`organizations/${
space.organizationId
}/spaces/${spaceId}/icon-${Date.now()}.${fileExtension}`,
);

const [bucket] = await S3Buckets.getBucketAccess(Option.none()).pipe(
runPromise,
Expand Down Expand Up @@ -89,12 +90,13 @@ export async function uploadSpaceIcon(
)
.pipe(runPromise);

const iconUrl = fileKey;

await db().update(spaces).set({ iconUrl }).where(eq(spaces.id, spaceId));
await db()
.update(spaces)
.set({ iconUrl: fileKey })
.where(eq(spaces.id, spaceId));

revalidatePath("/dashboard");
return { success: true, iconUrl };
return { success: true, iconUrl: fileKey };
} catch (error) {
console.error("Error uploading space icon:", error);
throw new Error(error instanceof Error ? error.message : "Upload failed");
Expand Down
10 changes: 4 additions & 6 deletions apps/web/app/(org)/dashboard/caps/Caps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ import { Effect, Exit } from "effect";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { useEffectMutation } from "@/lib/EffectRuntime";
import { useEffectMutation, useRpcClient } from "@/lib/EffectRuntime";
import { useVideosAnalyticsQuery } from "@/lib/Queries/Analytics";
import { AnalyticsRequest } from "@/lib/Requests/AnalyticsRequest";
import { Rpc, withRpc } from "@/lib/Rpcs";
import { useDashboardContext } from "../Contexts";
import {
NewFolderDialog,
Expand Down Expand Up @@ -147,12 +145,12 @@ export const Caps = ({
});
};

const rpc = useRpcClient();

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)),
Expand Down Expand Up @@ -203,7 +201,7 @@ export const Caps = ({
});

const { mutate: deleteCap, isPending: isDeletingCap } = useEffectMutation({
mutationFn: (id: Video.VideoId) => withRpc((r) => r.VideoDelete(id)),
mutationFn: (id: Video.VideoId) => rpc.VideoDelete(id),
onSuccess: () => {
toast.success("Cap deleted successfully");
router.refresh();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
type ImageLoadingStatus,
VideoThumbnail,
} from "@/components/VideoThumbnail";
import { useEffectMutation } from "@/lib/EffectRuntime";
import { useEffectMutation, useRpcClient } from "@/lib/EffectRuntime";
import { withRpc } from "@/lib/Rpcs";
import { usePublicEnv } from "@/utils/public-env";
import { PasswordDialog } from "../PasswordDialog";
Expand Down Expand Up @@ -133,11 +133,12 @@ export const CapCard = ({
const [confirmOpen, setConfirmOpen] = useState(false);

const router = useRouter();
const rpc = useRpcClient();

const downloadMutation = useEffectMutation({
mutationFn: () =>
Effect.gen(function* () {
const result = yield* withRpc((r) => r.VideoGetDownloadInfo(cap.id));
const result = yield* rpc.VideoGetDownloadInfo(cap.id);
const httpClient = yield* HttpClient.HttpClient;
if (Option.isSome(result)) {
const fetchResponse = yield* httpClient.get(result.value.downloadUrl);
Expand Down Expand Up @@ -175,7 +176,7 @@ export const CapCard = ({
});

const duplicateMutation = useEffectMutation({
mutationFn: () => withRpc((r) => r.VideoDuplicate(cap.id)),
mutationFn: () => rpc.VideoDuplicate(cap.id),
onSuccess: () => {
router.refresh();
},
Expand Down
10 changes: 5 additions & 5 deletions apps/web/app/(org)/dashboard/caps/components/Folder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { moveVideoToFolder } from "@/actions/folders/moveVideoToFolder";
import { useEffectMutation } from "@/lib/EffectRuntime";
import { withRpc } from "@/lib/Rpcs";
import { useEffectMutation, useRpcClient } from "@/lib/EffectRuntime";
import { ConfirmationDialog } from "../../_components/ConfirmationDialog";
import { useDashboardContext, useTheme } from "../../Contexts";
import { registerDropTarget } from "../../folder/[id]/components/ClientCapCard";
Expand Down Expand Up @@ -70,8 +69,10 @@ const FolderCard = ({
}),
});

const rpc = useRpcClient();

const deleteFolder = useEffectMutation({
mutationFn: (id: Folder.FolderId) => withRpc((r) => r.FolderDelete(id)),
mutationFn: (id: Folder.FolderId) => rpc.FolderDelete(id),
onSuccess: () => {
router.refresh();
toast.success("Folder deleted successfully");
Expand All @@ -83,8 +84,7 @@ const FolderCard = ({
});

const updateFolder = useEffectMutation({
mutationFn: (data: Folder.FolderUpdate) =>
withRpc((r) => r.FolderUpdate(data)),
mutationFn: (data: Folder.FolderUpdate) => rpc.FolderUpdate(data),
onSuccess: () => {
toast.success("Folder name updated successfully");
router.refresh();
Expand Down
19 changes: 9 additions & 10 deletions apps/web/app/(org)/dashboard/caps/components/NewFolderDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import { Option } from "effect";
import { useRouter } from "next/navigation";
import React, { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { useEffectMutation } from "@/lib/EffectRuntime";
import { withRpc } from "@/lib/Rpcs";
import { useEffectMutation, useRpcClient } from "@/lib/EffectRuntime";
import {
BlueFolder,
type FolderHandle,
Expand Down Expand Up @@ -101,16 +100,16 @@ export const NewFolderDialog: React.FC<Props> = ({
),
);

const rpc = useRpcClient();

const createFolder = useEffectMutation({
mutationFn: (data: { name: string; color: Folder.FolderColor }) =>
withRpc((r) =>
r.FolderCreate({
name: data.name,
color: data.color,
spaceId: Option.fromNullable(spaceId),
parentId: Option.none(),
}),
),
rpc.FolderCreate({
name: data.name,
color: data.color,
spaceId: Option.fromNullable(spaceId),
parentId: Option.none(),
}),
onSuccess: () => {
setFolderName("");
setSelectedColor(null);
Expand Down
88 changes: 61 additions & 27 deletions apps/web/app/(org)/dashboard/caps/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,46 +12,79 @@ import {
videoUploads,
} from "@cap/database/schema";
import { serverEnv } from "@cap/env";
import { Video } from "@cap/web-domain";
import { Database, ImageUploads } from "@cap/web-backend";
import { type ImageUpload, Video } from "@cap/web-domain";
import { and, count, desc, eq, inArray, isNull, sql } from "drizzle-orm";
import { Array, Effect } from "effect";
import type { Metadata } from "next";
Comment thread
Brendonovich marked this conversation as resolved.
Outdated
import { redirect } from "next/navigation";
import { runPromise } from "@/lib/server";
import { Caps } from "./Caps";

export const metadata: Metadata = {
title: "My Caps — Cap",
};

// Helper function to fetch shared spaces data for videos
async function getSharedSpacesForVideos(videoIds: Video.VideoId[]) {
const getSharedSpacesForVideos = Effect.fn(function* (
videoIds: Video.VideoId[],
) {
if (videoIds.length === 0) return {};

const db = yield* Database;
const imageUploads = yield* ImageUploads;

// Fetch space-level sharing
const spaceSharing = await db()
.select({
videoId: spaceVideos.videoId,
id: spaces.id,
name: spaces.name,
organizationId: spaces.organizationId,
iconUrl: organizations.iconUrl,
})
.from(spaceVideos)
.innerJoin(spaces, eq(spaceVideos.spaceId, spaces.id))
.innerJoin(organizations, eq(spaces.organizationId, organizations.id))
.where(inArray(spaceVideos.videoId, videoIds));
const spaceSharing = yield* db
.use((db) =>
db
.select({
videoId: spaceVideos.videoId,
id: spaces.id,
name: spaces.name,
organizationId: spaces.organizationId,
iconUrl: organizations.iconUrl,
})
.from(spaceVideos)
.innerJoin(spaces, eq(spaceVideos.spaceId, spaces.id))
.innerJoin(organizations, eq(spaces.organizationId, organizations.id))
.where(inArray(spaceVideos.videoId, videoIds)),
)
.pipe(
Effect.map((v) =>
v.map(
Effect.fn(function* (v) {
return {
...v,
iconUrl: v.iconUrl
? yield* imageUploads.resolveImageUrl(
v.iconUrl as ImageUpload.ImageUrlOrKey,
)
: null,
};
}),
),
),
Effect.flatMap(Effect.all),
);

// Fetch organization-level sharing
const orgSharing = await db()
.select({
videoId: sharedVideos.videoId,
id: organizations.id,
name: organizations.name,
organizationId: organizations.id,
iconUrl: organizations.iconUrl,
})
.from(sharedVideos)
.innerJoin(organizations, eq(sharedVideos.organizationId, organizations.id))
.where(inArray(sharedVideos.videoId, videoIds));
const orgSharing = yield* db.use((db) =>
db
.select({
videoId: sharedVideos.videoId,
id: organizations.id,
name: organizations.name,
organizationId: organizations.id,
iconUrl: organizations.iconUrl,
})
.from(sharedVideos)
.innerJoin(
organizations,
eq(sharedVideos.organizationId, organizations.id),
)
.where(inArray(sharedVideos.videoId, videoIds)),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Combine and group by videoId
const sharedSpacesMap: Record<
Expand Down Expand Up @@ -94,7 +127,7 @@ async function getSharedSpacesForVideos(videoIds: Video.VideoId[]) {
});

return sharedSpacesMap;
}
});

export default async function CapsPage(props: PageProps<"/dashboard/caps">) {
const searchParams = await props.searchParams;
Expand Down Expand Up @@ -211,7 +244,8 @@ export default async function CapsPage(props: PageProps<"/dashboard/caps">) {

// Fetch shared spaces data for all videos
const videoIds = videoData.map((video) => video.id);
const sharedSpacesMap = await getSharedSpacesForVideos(videoIds);
const sharedSpacesMap =
await getSharedSpacesForVideos(videoIds).pipe(runPromise);

const processedVideoData = videoData.map((video) => {
const { effectiveDate, ...videoWithoutEffectiveDate } = video;
Expand Down
Loading