From 53eedec9a732b05950470d05c1d515f3664349af Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 14 Oct 2025 23:36:23 +0800 Subject: [PATCH 1/3] put useAnalyticsQuery inside AnalyticsRequest --- apps/web/app/(org)/dashboard/caps/Caps.tsx | 10 +-- .../[id]/components/FolderVideosSection.tsx | 5 +- .../dashboard/spaces/[spaceId]/SharedCaps.tsx | 4 +- apps/web/lib/Requests/AnalyticsRequest.ts | 73 +++++++++---------- 4 files changed, 42 insertions(+), 50 deletions(-) diff --git a/apps/web/app/(org)/dashboard/caps/Caps.tsx b/apps/web/app/(org)/dashboard/caps/Caps.tsx index 6c0f3570e2..faeffcb374 100644 --- a/apps/web/app/(org)/dashboard/caps/Caps.tsx +++ b/apps/web/app/(org)/dashboard/caps/Caps.tsx @@ -5,16 +5,12 @@ import { Button } from "@cap/ui"; import type { Video } from "@cap/web-domain"; import { faFolderPlus, faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useQuery } from "@tanstack/react-query"; import { Effect, Exit } from "effect"; 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 { useEffectMutation } from "@/lib/EffectRuntime"; +import { AnalyticsRequest } from "@/lib/Requests/AnalyticsRequest"; import { Rpc, withRpc } from "@/lib/Rpcs"; import { useDashboardContext } from "../Contexts"; import { @@ -77,7 +73,7 @@ export const Caps = ({ const anyCapSelected = selectedCaps.length > 0; - const analyticsQuery = useVideosAnalyticsQuery( + const analyticsQuery = AnalyticsRequest.useQuery( data.map((video) => video.id), dubApiKeyEnabled, ); 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 07801ee86a..a14d608c1c 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx @@ -1,14 +1,13 @@ "use client"; import type { Video } from "@cap/web-domain"; -import { useQuery } from "@tanstack/react-query"; import { Effect, Exit } from "effect"; import { useRouter } from "next/navigation"; 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 { AnalyticsRequest } from "@/lib/Requests/AnalyticsRequest"; import { Rpc, withRpc } from "@/lib/Rpcs"; import type { VideoData } from "../../../caps/Caps"; import { CapCard } from "../../../caps/components/CapCard/CapCard"; @@ -109,7 +108,7 @@ export default function FolderVideosSection({ }); }; - const analyticsQuery = useVideosAnalyticsQuery( + const analyticsQuery = AnalyticsRequest.useQuery( initialVideos.map((video) => video.id), dubApiKeyEnabled, ); diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx index 2068291bdc..b32f21ae9a 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx @@ -8,7 +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 { AnalyticsRequest } from "@/lib/Requests/AnalyticsRequest"; import { useDashboardContext } from "../../Contexts"; import { CapPagination } from "../../caps/components/CapPagination"; import Folder, { type FolderDataType } from "../../caps/components/Folder"; @@ -95,7 +95,7 @@ export const SharedCaps = ({ const organizationMemberCount = organizationMembers?.length || 0; - const analyticsQuery = useVideosAnalyticsQuery( + const analyticsQuery = AnalyticsRequest.useQuery( data.map((video) => video.id), dubApiKeyEnabled, ); diff --git a/apps/web/lib/Requests/AnalyticsRequest.ts b/apps/web/lib/Requests/AnalyticsRequest.ts index d122e520ab..1aae129e60 100644 --- a/apps/web/lib/Requests/AnalyticsRequest.ts +++ b/apps/web/lib/Requests/AnalyticsRequest.ts @@ -49,47 +49,44 @@ export namespace AnalyticsRequest { dependencies: [Rpc.Default], }, ) {} -} -export function useVideosAnalyticsQuery( - videoIds: Video.VideoId[], - dubApiKeyEnabled?: boolean, -) { - return useEffectQuery({ - queryKey: ["analytics", videoIds], - queryFn: Effect.fn(function* () { - if (!dubApiKeyEnabled) return {}; + export function useQuery( + videoIds: Video.VideoId[], + dubApiKeyEnabled?: boolean, + ) { + return useEffectQuery({ + queryKey: ["analytics", videoIds], + queryFn: Effect.fn(function* () { + if (!dubApiKeyEnabled) return {}; - const dataloader = yield* AnalyticsRequest.DataLoaderResolver; + const dataloader = yield* 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 })), + const results = yield* Effect.all( + videoIds.map((videoId) => + Effect.request(new 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" }, - ); + { concurrency: "unbounded" }, + ); - return results.reduce( - (acc, current) => { - acc[current.videoId] = current.count; - return acc; - }, - {} as Record, - ); - }), - refetchOnWindowFocus: false, - refetchOnMount: true, - }); + return results.reduce( + (acc, current) => { + acc[current.videoId] = current.count; + return acc; + }, + {} as Record, + ); + }), + refetchOnWindowFocus: false, + refetchOnMount: true, + }); + } } From 0f2d7dd703b9ddf92b2d16e16d2c55f3f8ee9aed Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 14 Oct 2025 23:45:03 +0800 Subject: [PATCH 2/3] fix circular import --- apps/web/app/(org)/dashboard/caps/Caps.tsx | 3 +- .../[id]/components/FolderVideosSection.tsx | 3 +- .../dashboard/spaces/[spaceId]/SharedCaps.tsx | 3 +- apps/web/lib/Queries/Analytics.ts | 47 +++++++++++++++++++ apps/web/lib/Requests/AnalyticsRequest.ts | 41 ---------------- 5 files changed, 53 insertions(+), 44 deletions(-) create mode 100644 apps/web/lib/Queries/Analytics.ts diff --git a/apps/web/app/(org)/dashboard/caps/Caps.tsx b/apps/web/app/(org)/dashboard/caps/Caps.tsx index faeffcb374..09d018f7ef 100644 --- a/apps/web/app/(org)/dashboard/caps/Caps.tsx +++ b/apps/web/app/(org)/dashboard/caps/Caps.tsx @@ -25,6 +25,7 @@ import { EmptyCapState } from "./components/EmptyCapState"; import type { FolderDataType } from "./components/Folder"; import Folder from "./components/Folder"; import { useUploadingStatus } from "./UploadingContext"; +import { useVideosAnalyticsQuery } from "@/lib/Queries/Analytics"; export type VideoData = { id: Video.VideoId; @@ -73,7 +74,7 @@ export const Caps = ({ const anyCapSelected = selectedCaps.length > 0; - const analyticsQuery = AnalyticsRequest.useQuery( + const analyticsQuery = useVideosAnalyticsQuery( data.map((video) => video.id), dubApiKeyEnabled, ); 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 a14d608c1c..7633f79bbb 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/Queries/Analytics"; interface FolderVideosSectionProps { initialVideos: VideoData; @@ -108,7 +109,7 @@ export default function FolderVideosSection({ }); }; - const analyticsQuery = AnalyticsRequest.useQuery( + const analyticsQuery = useVideosAnalyticsQuery( initialVideos.map((video) => video.id), dubApiKeyEnabled, ); diff --git a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx index b32f21ae9a..296bbff5d5 100644 --- a/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx +++ b/apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx @@ -23,6 +23,7 @@ import { } from "./components/OrganizationIndicator"; import { SharedCapCard } from "./components/SharedCapCard"; import type { SpaceMemberData } from "./page"; +import { useVideosAnalyticsQuery } from "@/lib/Queries/Analytics"; type SharedVideoData = { id: Video.VideoId; @@ -95,7 +96,7 @@ export const SharedCaps = ({ const organizationMemberCount = organizationMembers?.length || 0; - const analyticsQuery = AnalyticsRequest.useQuery( + const analyticsQuery = useVideosAnalyticsQuery( data.map((video) => video.id), dubApiKeyEnabled, ); diff --git a/apps/web/lib/Queries/Analytics.ts b/apps/web/lib/Queries/Analytics.ts new file mode 100644 index 0000000000..eb00f33bf6 --- /dev/null +++ b/apps/web/lib/Queries/Analytics.ts @@ -0,0 +1,47 @@ +import type { Video } from "@cap/web-domain"; +import { Effect } from "effect"; +import { useEffectQuery } from "../EffectRuntime"; +import { AnalyticsRequest } from "../Requests/AnalyticsRequest"; + +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/Requests/AnalyticsRequest.ts b/apps/web/lib/Requests/AnalyticsRequest.ts index 1aae129e60..da475c5bb2 100644 --- a/apps/web/lib/Requests/AnalyticsRequest.ts +++ b/apps/web/lib/Requests/AnalyticsRequest.ts @@ -3,7 +3,6 @@ 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< @@ -49,44 +48,4 @@ export namespace AnalyticsRequest { dependencies: [Rpc.Default], }, ) {} - - export function useQuery( - videoIds: Video.VideoId[], - dubApiKeyEnabled?: boolean, - ) { - return useEffectQuery({ - queryKey: ["analytics", videoIds], - queryFn: Effect.fn(function* () { - if (!dubApiKeyEnabled) return {}; - - const dataloader = yield* DataLoaderResolver; - - const results = yield* Effect.all( - videoIds.map((videoId) => - Effect.request(new 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, - }); - } } From 17e56c6e0f7d775247c13c79936167e148c2c083 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 14 Oct 2025 23:49:37 +0800 Subject: [PATCH 3/3] formatting --- apps/web/app/(org)/dashboard/caps/Caps.tsx | 2 +- .../dashboard/folder/[id]/components/FolderVideosSection.tsx | 2 +- apps/web/app/(org)/dashboard/spaces/[spaceId]/SharedCaps.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/app/(org)/dashboard/caps/Caps.tsx b/apps/web/app/(org)/dashboard/caps/Caps.tsx index 09d018f7ef..bc0ff17e6b 100644 --- a/apps/web/app/(org)/dashboard/caps/Caps.tsx +++ b/apps/web/app/(org)/dashboard/caps/Caps.tsx @@ -10,6 +10,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { useEffectMutation } 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"; @@ -25,7 +26,6 @@ import { EmptyCapState } from "./components/EmptyCapState"; import type { FolderDataType } from "./components/Folder"; import Folder from "./components/Folder"; import { useUploadingStatus } from "./UploadingContext"; -import { useVideosAnalyticsQuery } from "@/lib/Queries/Analytics"; 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 7633f79bbb..443591ae44 100644 --- a/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx +++ b/apps/web/app/(org)/dashboard/folder/[id]/components/FolderVideosSection.tsx @@ -7,6 +7,7 @@ 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/Queries/Analytics"; import { AnalyticsRequest } from "@/lib/Requests/AnalyticsRequest"; import { Rpc, withRpc } from "@/lib/Rpcs"; import type { VideoData } from "../../../caps/Caps"; @@ -14,7 +15,6 @@ 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/Queries/Analytics"; 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 296bbff5d5..f496d9aa9b 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/Queries/Analytics"; import { AnalyticsRequest } from "@/lib/Requests/AnalyticsRequest"; import { useDashboardContext } from "../../Contexts"; import { CapPagination } from "../../caps/components/CapPagination"; @@ -23,7 +24,6 @@ import { } from "./components/OrganizationIndicator"; import { SharedCapCard } from "./components/SharedCapCard"; import type { SpaceMemberData } from "./page"; -import { useVideosAnalyticsQuery } from "@/lib/Queries/Analytics"; type SharedVideoData = { id: Video.VideoId;