From b069620da8cac9dec197c0400513c420d5711540 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 10 Oct 2025 15:26:02 +0800 Subject: [PATCH 1/2] use rpc and dataloader for analytics api --- apps/web/actions/video/upload.ts | 3 +- apps/web/actions/videos/get-analytics.ts | 2 +- apps/web/app/(org)/dashboard/caps/Caps.tsx | 58 ++--------- .../caps/components/UploadCapButton.tsx | 2 +- .../[id]/components/FolderVideosSection.tsx | 52 ++-------- .../dashboard/spaces/[spaceId]/SharedCaps.tsx | 53 ++--------- apps/web/app/api/desktop/[...route]/video.ts | 3 +- apps/web/components/VideoThumbnail.tsx | 2 +- apps/web/lib/EffectRuntime.ts | 4 +- apps/web/lib/Requests/AnalyticsRequest.ts | 95 +++++++++++++++++++ .../lib/{ => Requests}/ThumbnailRequest.ts | 7 +- packages/utils/package.json | 1 + packages/utils/src/index.ts | 1 + .../utils => packages/utils/src/lib}/dub.ts | 0 packages/web-backend/src/Videos/VideosRpcs.ts | 27 ++++++ packages/web-backend/src/Videos/index.ts | 25 +++++ packages/web-domain/src/Video.ts | 17 ++++ pnpm-lock.yaml | 9 +- 18 files changed, 205 insertions(+), 156 deletions(-) create mode 100644 apps/web/lib/Requests/AnalyticsRequest.ts rename apps/web/lib/{ => Requests}/ThumbnailRequest.ts (90%) rename {apps/web/utils => packages/utils/src/lib}/dub.ts (100%) diff --git a/apps/web/actions/video/upload.ts b/apps/web/actions/video/upload.ts index 5bb7dce560..92c7a13372 100644 --- a/apps/web/actions/video/upload.ts +++ b/apps/web/actions/video/upload.ts @@ -9,14 +9,13 @@ import { getCurrentUser } from "@cap/database/auth/session"; import { nanoId } from "@cap/database/helpers"; import { s3Buckets, videos, videoUploads } from "@cap/database/schema"; import { buildEnv, NODE_ENV, serverEnv } from "@cap/env"; -import { userIsPro } from "@cap/utils"; +import { dub, userIsPro } from "@cap/utils"; import { S3Buckets } from "@cap/web-backend"; import { type Folder, type Organisation, Video } from "@cap/web-domain"; import { eq } from "drizzle-orm"; import { Effect, Option } from "effect"; import { revalidatePath } from "next/cache"; import { runPromise } from "@/lib/server"; -import { dub } from "@/utils/dub"; async function getVideoUploadPresignedUrl({ fileKey, diff --git a/apps/web/actions/videos/get-analytics.ts b/apps/web/actions/videos/get-analytics.ts index f13d61811f..7792c16d46 100644 --- a/apps/web/actions/videos/get-analytics.ts +++ b/apps/web/actions/videos/get-analytics.ts @@ -1,6 +1,6 @@ "use server"; -import { dub } from "@/utils/dub"; +import { dub } from "@cap/utils"; export async function getVideoAnalytics(videoId: string) { if (!videoId) { diff --git a/apps/web/app/(org)/dashboard/caps/Caps.tsx b/apps/web/app/(org)/dashboard/caps/Caps.tsx index fd6240e075..13f7f70033 100644 --- a/apps/web/app/(org)/dashboard/caps/Caps.tsx +++ b/apps/web/app/(org)/dashboard/caps/Caps.tsx @@ -10,7 +10,7 @@ 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, useEffectQuery } from "@/lib/EffectRuntime"; import { Rpc, withRpc } from "@/lib/Rpcs"; import { useDashboardContext } from "../Contexts"; import { @@ -25,6 +25,10 @@ import { EmptyCapState } from "./components/EmptyCapState"; import type { FolderDataType } from "./components/Folder"; import Folder from "./components/Folder"; import { useUploadingStatus } from "./UploadingContext"; +import { + AnalyticsRequest, + useVideosAnalyticsQuery, +} from "@/lib/Requests/AnalyticsRequest"; export type VideoData = { id: Video.VideoId; @@ -73,54 +77,8 @@ export const Caps = ({ const anyCapSelected = selectedCaps.length > 0; - const videoIds = data.map((video) => video.id).sort(); - - const { data: analyticsData, isLoading: isLoadingAnalytics } = useQuery({ - queryKey: ["analytics", videoIds], - 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", - }, - }); - - 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 analytics = analyticsData || {}; + const analyticsQuery = useVideosAnalyticsQuery(data.map((video) => video.id)); + const analytics = analyticsQuery.data || {}; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -318,7 +276,7 @@ export const Caps = ({ } }} userId={user?.id} - isLoadingAnalytics={isLoadingAnalytics} + isLoadingAnalytics={analyticsQuery.isLoading} isSelected={selectedCaps.includes(video.id)} anyCapSelected={anyCapSelected} onSelectToggle={() => handleCapSelection(video.id)} diff --git a/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx b/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx index ba7c33a926..8449a5f731 100644 --- a/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx +++ b/apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx @@ -17,7 +17,7 @@ import { useUploadingContext, } from "@/app/(org)/dashboard/caps/UploadingContext"; import { UpgradeModal } from "@/components/UpgradeModal"; -import { ThumbnailRequest } from "@/lib/ThumbnailRequest"; +import { ThumbnailRequest } from "@/lib/Requests/ThumbnailRequest"; export const UploadCapButton = ({ size = "md", 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 8fcbd4afc7..7a25b8c442 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx @@ -14,6 +14,7 @@ import { CapCard } from "../../../caps/components/CapCard/CapCard"; import { SelectedCapsBar } from "../../../caps/components/SelectedCapsBar"; import { UploadPlaceholderCard } from "../../../caps/components/UploadPlaceholderCard"; import { useUploadingStatus } from "../../../caps/UploadingContext"; +import { useVideosAnalyticsQuery } from "@/lib/Requests/AnalyticsRequest"; interface FolderVideosSectionProps { initialVideos: VideoData; @@ -108,49 +109,10 @@ export default function FolderVideosSection({ }); }; - 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 analyticsQuery = useVideosAnalyticsQuery( + initialVideos.map((video) => video.id), + dubApiKeyEnabled, + ); const [isUploading, uploadingCapId] = useUploadingStatus(); const visibleVideos = useMemo( @@ -161,7 +123,7 @@ export default function FolderVideosSection({ [initialVideos, isUploading, uploadingCapId], ); - const analytics = analyticsData || {}; + const analytics = analyticsQuery.data || {}; return ( <> @@ -185,7 +147,7 @@ export default function FolderVideosSection({ cap={video} analytics={analytics[video.id] || 0} userId={user?.id} - isLoadingAnalytics={isLoadingAnalytics} + isLoadingAnalytics={analyticsQuery.isLoading} isSelected={selectedCaps.includes(video.id)} anyCapSelected={selectedCaps.length > 0} 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 b116ce8cd3..cfce55c550 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx @@ -22,6 +22,7 @@ import { } from "./components/OrganizationIndicator"; import { SharedCapCard } from "./components/SharedCapCard"; import type { SpaceMemberData } from "./page"; +import { useVideosAnalyticsQuery } from "@/lib/Requests/AnalyticsRequest"; type SharedVideoData = { id: Video.VideoId; @@ -94,52 +95,12 @@ export const SharedCaps = ({ 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 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 }; - } - }); - - 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; - }, - staleTime: 30000, // 30 seconds - refetchOnWindowFocus: false, - }); + const analyticsQuery = useVideosAnalyticsQuery( + data.map((video) => video.id), + dubApiKeyEnabled, + ); - const analytics = analyticsData || {}; + const analytics = analyticsQuery.data || {}; const handleVideosAdded = () => { router.refresh(); @@ -296,7 +257,7 @@ export const SharedCaps = ({ key={cap.id} cap={cap} hideSharedStatus - isLoadingAnalytics={isLoadingAnalytics} + isLoadingAnalytics={analyticsQuery.isLoading} analytics={analytics[cap.id] || 0} organizationName={activeOrganization?.organization.name || ""} spaceName={spaceData?.name || ""} diff --git a/apps/web/app/api/desktop/[...route]/video.ts b/apps/web/app/api/desktop/[...route]/video.ts index eea0b2f7e5..6a4a037b2d 100644 --- a/apps/web/app/api/desktop/[...route]/video.ts +++ b/apps/web/app/api/desktop/[...route]/video.ts @@ -11,7 +11,7 @@ import { videoUploads, } from "@cap/database/schema"; import { buildEnv, NODE_ENV, serverEnv } from "@cap/env"; -import { userIsPro } from "@cap/utils"; +import { dub, userIsPro } from "@cap/utils"; import { S3Buckets } from "@cap/web-backend"; import { Organisation, Video } from "@cap/web-domain"; import { zValidator } from "@hono/zod-validator"; @@ -20,7 +20,6 @@ import { Effect, Option } from "effect"; import { Hono } from "hono"; import { z } from "zod"; import { runPromise } from "@/lib/server"; -import { dub } from "@/utils/dub"; import { stringOrNumberOptional } from "@/utils/zod"; import { withAuth } from "../../utils"; diff --git a/apps/web/components/VideoThumbnail.tsx b/apps/web/components/VideoThumbnail.tsx index 01a18bad77..4ae8cbc5ed 100644 --- a/apps/web/components/VideoThumbnail.tsx +++ b/apps/web/components/VideoThumbnail.tsx @@ -6,7 +6,7 @@ import moment from "moment"; import Image from "next/image"; import { memo, useEffect, useRef } from "react"; import { useEffectQuery } from "@/lib/EffectRuntime"; -import { ThumbnailRequest } from "@/lib/ThumbnailRequest"; +import { ThumbnailRequest } from "@/lib/Requests/ThumbnailRequest"; export type ImageLoadingStatus = "loading" | "success" | "error"; diff --git a/apps/web/lib/EffectRuntime.ts b/apps/web/lib/EffectRuntime.ts index ba6957b011..aa00f3d3c0 100644 --- a/apps/web/lib/EffectRuntime.ts +++ b/apps/web/lib/EffectRuntime.ts @@ -4,11 +4,13 @@ import { makeUseEffectMutation, makeUseEffectQuery, } from "./effect-react-query"; +import { AnalyticsRequest } from "./Requests/AnalyticsRequest"; +import { ThumbnailRequest } from "./Requests/ThumbnailRequest"; import { Rpc } from "./Rpcs"; -import { ThumbnailRequest } from "./ThumbnailRequest"; export const RuntimeLayer = Layer.mergeAll( ThumbnailRequest.DataLoaderResolver.Default, + AnalyticsRequest.DataLoaderResolver.Default, Rpc.Default, FetchHttpClient.layer, ); diff --git a/apps/web/lib/Requests/AnalyticsRequest.ts b/apps/web/lib/Requests/AnalyticsRequest.ts new file mode 100644 index 0000000000..d122e520ab --- /dev/null +++ b/apps/web/lib/Requests/AnalyticsRequest.ts @@ -0,0 +1,95 @@ +import type { Video } from "@cap/web-domain"; +import { dataLoader } from "@effect/experimental/RequestResolver"; +import { Effect, Exit, Request, RequestResolver } from "effect"; +import type { NonEmptyArray } from "effect/Array"; +import { Rpc } from "@/lib/Rpcs"; +import { useEffectQuery } from "../EffectRuntime"; + +export namespace AnalyticsRequest { + export class AnalyticsRequest extends Request.Class< + { count: number }, + unknown, + { videoId: Video.VideoId } + > {} + + export class DataLoaderResolver extends Effect.Service()( + "AnalyticsRequest/DataLoaderResolver", + { + scoped: Effect.gen(function* () { + const rpc = yield* Rpc; + + const requestResolver = RequestResolver.makeBatched( + (requests: NonEmptyArray) => + rpc.VideosGetAnalytics(requests.map((r) => r.videoId)).pipe( + Effect.flatMap( + // biome-ignore lint/suspicious/useIterableCallbackReturn: effect + Effect.forEach((result, index) => + Exit.matchEffect(result, { + onSuccess: (v) => Request.succeed(requests[index]!, v), + onFailure: (e) => Request.fail(requests[index]!, e), + }), + ), + ), + Effect.catchAll((error) => + Effect.forEach( + requests, + (request) => Request.fail(request, error), + { + discard: true, + }, + ), + ), + ), + ); + + return yield* dataLoader(requestResolver, { + window: "10 millis", + }); + }), + dependencies: [Rpc.Default], + }, + ) {} +} + +export function useVideosAnalyticsQuery( + videoIds: Video.VideoId[], + dubApiKeyEnabled?: boolean, +) { + return useEffectQuery({ + queryKey: ["analytics", videoIds], + queryFn: Effect.fn(function* () { + if (!dubApiKeyEnabled) return {}; + + const dataloader = yield* AnalyticsRequest.DataLoaderResolver; + + const results = yield* Effect.all( + videoIds.map((videoId) => + Effect.request( + new AnalyticsRequest.AnalyticsRequest({ videoId }), + dataloader, + ).pipe( + Effect.catchAll((e) => { + console.warn( + `Failed to fetch analytics for video ${videoId}:`, + e, + ); + return Effect.succeed({ count: 0 }); + }), + Effect.map(({ count }) => ({ videoId, count })), + ), + ), + { concurrency: "unbounded" }, + ); + + return results.reduce( + (acc, current) => { + acc[current.videoId] = current.count; + return acc; + }, + {} as Record, + ); + }), + refetchOnWindowFocus: false, + refetchOnMount: true, + }); +} diff --git a/apps/web/lib/ThumbnailRequest.ts b/apps/web/lib/Requests/ThumbnailRequest.ts similarity index 90% rename from apps/web/lib/ThumbnailRequest.ts rename to apps/web/lib/Requests/ThumbnailRequest.ts index 4e8386f643..0d3047c079 100644 --- a/apps/web/lib/ThumbnailRequest.ts +++ b/apps/web/lib/Requests/ThumbnailRequest.ts @@ -20,8 +20,8 @@ export namespace ThumbnailRequest { const rpc = yield* Rpc; const requestResolver = RequestResolver.makeBatched( - (requests: NonEmptyArray) => { - return rpc.VideosGetThumbnails(requests.map((r) => r.videoId)).pipe( + (requests: NonEmptyArray) => + rpc.VideosGetThumbnails(requests.map((r) => r.videoId)).pipe( Effect.flatMap( // biome-ignore lint/suspicious/useIterableCallbackReturn: effect Effect.forEach((result, index) => @@ -40,8 +40,7 @@ export namespace ThumbnailRequest { }, ), ), - ); - }, + ), ); return yield* dataLoader(requestResolver, { diff --git a/packages/utils/package.json b/packages/utils/package.json index 0a055dc135..4f201dfe51 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -21,6 +21,7 @@ "@aws-sdk/client-s3": "^3.485.0", "@cap/env": "workspace:*", "clsx": "^2.0.0", + "dub": "^0.64.0", "stripe": "^14.24.0", "tailwind-merge": "^2.1.0", "zod": "^3" diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index c7ac6480ba..dd239b6816 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,5 @@ export * from "./constants/plans.ts"; export * from "./helpers.ts"; +export * from "./lib/dub.ts"; export * from "./lib/stripe/stripe.ts"; export * from "./types/database.ts"; diff --git a/apps/web/utils/dub.ts b/packages/utils/src/lib/dub.ts similarity index 100% rename from apps/web/utils/dub.ts rename to packages/utils/src/lib/dub.ts diff --git a/packages/web-backend/src/Videos/VideosRpcs.ts b/packages/web-backend/src/Videos/VideosRpcs.ts index f27239ea12..8f843fa20b 100644 --- a/packages/web-backend/src/Videos/VideosRpcs.ts +++ b/packages/web-backend/src/Videos/VideosRpcs.ts @@ -70,6 +70,33 @@ export const VideosRpcsLive = Video.VideoRpcs.toLayer( UnknownException: () => new InternalError({ type: "unknown" }), }), ), + + VideosGetAnalytics: (videoIds) => + Effect.all( + videoIds.map((id) => + videos.getAnalytics(id).pipe( + Effect.catchTags({ + DatabaseError: () => new InternalError({ type: "database" }), + UnknownException: () => new InternalError({ type: "unknown" }), + }), + Effect.matchEffect({ + onSuccess: (v) => Effect.succeed(Exit.succeed(v)), + onFailure: (e) => + Schema.is(InternalError)(e) + ? Effect.fail(e) + : Effect.succeed(Exit.fail(e)), + }), + Effect.map((v) => Unify.unify(v)), + ), + ), + { concurrency: 10 }, + ).pipe( + provideOptionalAuth, + Effect.catchTags({ + DatabaseError: () => new InternalError({ type: "database" }), + UnknownException: () => new InternalError({ type: "unknown" }), + }), + ), }; }), ); diff --git a/packages/web-backend/src/Videos/index.ts b/packages/web-backend/src/Videos/index.ts index 523fd64ff7..0a813613a6 100644 --- a/packages/web-backend/src/Videos/index.ts +++ b/packages/web-backend/src/Videos/index.ts @@ -1,4 +1,5 @@ import * as Db from "@cap/database/schema"; +import { dub } from "@cap/utils"; import { CurrentUser, Policy, Video } from "@cap/web-domain"; import * as Dz from "drizzle-orm"; import { Array, Effect, Option, pipe } from "effect"; @@ -192,6 +193,30 @@ export class Videos extends Effect.Service()("Videos", { Effect.map(Option.flatten), ); }), + + getAnalytics: Effect.fn("Videos.getAnalytics")(function* ( + videoId: Video.VideoId, + ) { + const [video] = yield* getById(videoId).pipe( + Effect.flatten, + Effect.catchTag( + "NoSuchElementException", + () => new Video.NotFoundError(), + ), + ); + + const response = yield* Effect.tryPromise(() => + dub().analytics.retrieve({ + domain: "cap.link", + key: video.id, + }), + ); + const { clicks } = response as { clicks: unknown }; + + if (typeof clicks !== "number" || clicks === null) return { count: 0 }; + + return { count: clicks }; + }), }; }), dependencies: [ diff --git a/packages/web-domain/src/Video.ts b/packages/web-domain/src/Video.ts index cd3fda5dfc..98d8c633bf 100644 --- a/packages/web-domain/src/Video.ts +++ b/packages/web-domain/src/Video.ts @@ -186,4 +186,21 @@ export class VideoRpcs extends RpcGroup.make( ), error: InternalError, }), + Rpc.make("VideosGetAnalytics", { + payload: Schema.Array(VideoId).pipe( + Schema.filter((a) => a.length <= 50 || "Maximum of 50 videos at a time"), + ), + success: Schema.Array( + Schema.Exit({ + success: Schema.Struct({ count: Schema.Int }), + failure: Schema.Union( + NotFoundError, + PolicyDeniedError, + VerifyVideoPasswordError, + ), + defect: Schema.Unknown, + }), + ), + error: InternalError, + }), ) {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2b39ec0a7..cd0cca7478 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -701,7 +701,7 @@ importers: version: 15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next-auth: specifier: ^4.24.5 - version: 4.24.11(next@15.5.4(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 4.24.11(next@15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next-contentlayer2: specifier: ^0.5.3 version: 0.5.8(acorn@8.15.0)(contentlayer2@0.5.8(acorn@8.15.0)(esbuild@0.25.5))(esbuild@0.25.5)(next@15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -1011,7 +1011,7 @@ importers: version: 15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next-auth: specifier: ^4.24.5 - version: 4.24.11(next@15.5.4(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 4.24.11(next@15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-email: specifier: ^4.0.16 version: 4.0.16(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -1263,6 +1263,9 @@ importers: clsx: specifier: ^2.0.0 version: 2.1.1 + dub: + specifier: ^0.64.0 + version: 0.64.0(@modelcontextprotocol/sdk@1.6.1)(zod@3.25.76) stripe: specifier: ^14.24.0 version: 14.25.0 @@ -26491,7 +26494,7 @@ snapshots: p-wait-for: 5.0.2 qs: 6.14.0 - next-auth@4.24.11(next@15.5.4(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + next-auth@4.24.11(next@15.5.4(@babel/core@7.27.1)(@opentelemetry/api@1.9.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(nodemailer@6.10.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: '@babel/runtime': 7.27.1 '@panva/hkdf': 1.2.1 From f196ff17d7344b26657a3589223987b75bcb97c2 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 10 Oct 2025 15:26:41 +0800 Subject: [PATCH 2/2] format --- apps/web/app/(org)/dashboard/caps/Caps.tsx | 8 ++++---- .../folder/[id]/components/FolderVideosSection.tsx | 2 +- .../app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/app/(org)/dashboard/caps/Caps.tsx b/apps/web/app/(org)/dashboard/caps/Caps.tsx index 13f7f70033..ef7a4f6a2c 100644 --- a/apps/web/app/(org)/dashboard/caps/Caps.tsx +++ b/apps/web/app/(org)/dashboard/caps/Caps.tsx @@ -11,6 +11,10 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { useEffectMutation, useEffectQuery } from "@/lib/EffectRuntime"; +import { + AnalyticsRequest, + useVideosAnalyticsQuery, +} from "@/lib/Requests/AnalyticsRequest"; import { Rpc, withRpc } from "@/lib/Rpcs"; import { useDashboardContext } from "../Contexts"; import { @@ -25,10 +29,6 @@ import { EmptyCapState } from "./components/EmptyCapState"; import type { FolderDataType } from "./components/Folder"; import Folder from "./components/Folder"; import { useUploadingStatus } from "./UploadingContext"; -import { - AnalyticsRequest, - useVideosAnalyticsQuery, -} from "@/lib/Requests/AnalyticsRequest"; export type VideoData = { id: Video.VideoId; 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 7a25b8c442..07801ee86a 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx @@ -8,13 +8,13 @@ import { useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { useDashboardContext } from "@/app/(org)/dashboard/Contexts"; import { useEffectMutation } from "@/lib/EffectRuntime"; +import { useVideosAnalyticsQuery } from "@/lib/Requests/AnalyticsRequest"; import { Rpc, withRpc } from "@/lib/Rpcs"; 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 { useUploadingStatus } from "../../../caps/UploadingContext"; -import { useVideosAnalyticsQuery } from "@/lib/Requests/AnalyticsRequest"; interface FolderVideosSectionProps { initialVideos: VideoData; diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx index cfce55c550..2068291bdc 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx @@ -8,6 +8,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQuery } from "@tanstack/react-query"; import { useRouter, useSearchParams } from "next/navigation"; import { useState } from "react"; +import { useVideosAnalyticsQuery } from "@/lib/Requests/AnalyticsRequest"; import { useDashboardContext } from "../../Contexts"; import { CapPagination } from "../../caps/components/CapPagination"; import Folder, { type FolderDataType } from "../../caps/components/Folder"; @@ -22,7 +23,6 @@ import { } from "./components/OrganizationIndicator"; import { SharedCapCard } from "./components/SharedCapCard"; import type { SpaceMemberData } from "./page"; -import { useVideosAnalyticsQuery } from "@/lib/Requests/AnalyticsRequest"; type SharedVideoData = { id: Video.VideoId;