From aae01beb68fa7dbb7497d2739479d38503d1beb2 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Thu, 16 Apr 2026 02:09:49 +0530 Subject: [PATCH 1/2] feat(chat): cut useArtistSegments over to GET /api/artists/{id}/segments Migrate the segments list UI off the local `/api/segments?artistId=...` route and onto the dedicated, nested `GET /api/artists/{id}/segments` endpoint (api PR #443). - `hooks/useArtistSegments.ts` now calls the dedicated endpoint with a Bearer token via `getClientApiBaseUrl()`, maps the paginated `{ status, segments, pagination }` envelope into the existing `Segment[]` consumer shape, and preserves the `s.size > 0` filter. - Extracted the `Segment` consumer type into `types/Segment.ts` so components can depend on a type-only module after the supabase-backed helpers go away. `components/Segments/Segments.tsx` and `components/Segments/SegmentButton.tsx` now import from there. No other consumer changes needed (SegmentsWrapper, FanGroupNavItem, MiniMenu already depended on the hook's return shape). - Updated the MCP `get_artist_segments` tool to call the nested dedicated URL as well, since api PR #443 removes the legacy flat `GET /api/artist/segments` in the same release. - Removed the local `app/api/segments/route.ts` and the supabase helpers that only backed it: `lib/supabase/getArtistSegments.ts`, `getArtistSegmentNames.ts`, `getSegmentCounts.ts`, and `fan_segments/selectFanSegments.ts`. Follows SEGMENTS_SURFACE_MIGRATION_PLAN.md step "List -> chat PR". Co-Authored-By: Claude Opus 4.6 --- app/api/segments/route.ts | 25 ------- components/Segments/SegmentButton.tsx | 2 +- components/Segments/Segments.tsx | 2 +- hooks/useArtistSegments.ts | 62 +++++++++++++--- .../fan_segments/selectFanSegments.ts | 36 ---------- lib/supabase/getArtistSegmentNames.ts | 35 ---------- lib/supabase/getArtistSegments.ts | 70 ------------------- lib/supabase/getSegmentCounts.ts | 32 --------- lib/tools/getArtistSegments.ts | 9 ++- types/Segment.ts | 19 +++++ 10 files changed, 81 insertions(+), 211 deletions(-) delete mode 100644 app/api/segments/route.ts delete mode 100644 lib/supabase/fan_segments/selectFanSegments.ts delete mode 100644 lib/supabase/getArtistSegmentNames.ts delete mode 100644 lib/supabase/getArtistSegments.ts delete mode 100644 lib/supabase/getSegmentCounts.ts create mode 100644 types/Segment.ts diff --git a/app/api/segments/route.ts b/app/api/segments/route.ts deleted file mode 100644 index deb3db5a6..000000000 --- a/app/api/segments/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getArtistSegments } from "@/lib/supabase/getArtistSegments"; -import { NextRequest } from "next/server"; - -export async function GET(req: NextRequest) { - const artistId = req.nextUrl.searchParams.get("artistId"); - - if (!artistId) { - return Response.json({ error: "artistId is required" }, { status: 400 }); - } - - try { - const segments = await getArtistSegments(artistId); - return Response.json(segments, { status: 200 }); - } catch (error) { - console.error("Error fetching segments:", error); - return Response.json( - { error: "Failed to fetch segments" }, - { status: 500 } - ); - } -} - -export const dynamic = "force-dynamic"; -export const fetchCache = "force-no-store"; -export const revalidate = 0; diff --git a/components/Segments/SegmentButton.tsx b/components/Segments/SegmentButton.tsx index 9ea4513f1..4c9b642f0 100644 --- a/components/Segments/SegmentButton.tsx +++ b/components/Segments/SegmentButton.tsx @@ -1,6 +1,6 @@ import { Card } from "@/components/ui/card"; import { Users, ArrowUpRight } from "lucide-react"; -import { Segment } from "@/lib/supabase/getArtistSegments"; +import type { Segment } from "@/types/Segment"; import SegmentFanCircles from "./SegmentFanCircles"; interface SegmentButtonProps { diff --git a/components/Segments/Segments.tsx b/components/Segments/Segments.tsx index 3a462d8f3..dc9194e52 100644 --- a/components/Segments/Segments.tsx +++ b/components/Segments/Segments.tsx @@ -1,5 +1,5 @@ import useGenerateSegmentReport from "@/hooks/useGenerateSegmentReport"; -import { type Segment } from "@/lib/supabase/getArtistSegments"; +import type { Segment } from "@/types/Segment"; import SegmentButton from "./SegmentButton"; interface SegmentsProps { diff --git a/hooks/useArtistSegments.ts b/hooks/useArtistSegments.ts index 879cb902f..08a96d1b8 100644 --- a/hooks/useArtistSegments.ts +++ b/hooks/useArtistSegments.ts @@ -1,20 +1,66 @@ -import { type Segment } from "@/lib/supabase/getArtistSegments"; import { useQuery } from "@tanstack/react-query"; +import { usePrivy } from "@privy-io/react-auth"; +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; +import type { Segment } from "@/types/Segment"; -async function fetchSegments(artistId: string): Promise { - const response = await fetch(`/api/segments?artistId=${artistId}`); +/** + * Shape returned by the dedicated `GET /api/artists/{id}/segments` endpoint. + * We map this into the existing consumer `Segment[]` shape so components + * (`SegmentsWrapper`, `FanGroupNavItem`, `MiniMenu`) do not change. + */ +interface MappedArtistSegment { + id: string; + name: string; + size: number; + icon?: string; +} + +interface ArtistSegmentsResponse { + status: string; + segments: MappedArtistSegment[]; + pagination: { + total_count: number; + page: number; + limit: number; + total_pages: number; + }; +} + +async function fetchSegments( + artistId: string, + accessToken: string, +): Promise { + const response = await fetch( + `${getClientApiBaseUrl()}/api/artists/${artistId}/segments`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); if (!response.ok) { - throw new Error("Failed to fetch segments"); + throw new Error(`Failed to fetch segments: ${response.status}`); } - const segments: Segment[] = await response.json(); - return segments.filter((s) => s.size > 0); + const data: ArtistSegmentsResponse = await response.json(); + return data.segments + .map((s) => ({ + id: s.id, + name: s.name, + size: s.size, + icon: s.icon, + })) + .filter((s) => s.size > 0); } export function useArtistSegments(artistId?: string) { + const { getAccessToken, authenticated } = usePrivy(); return useQuery({ queryKey: ["segments", artistId], - queryFn: () => fetchSegments(artistId!), - enabled: !!artistId, + queryFn: async () => { + const accessToken = await getAccessToken(); + return fetchSegments(artistId!, accessToken!); + }, + enabled: !!artistId && authenticated, staleTime: 1000 * 60 * 5, // 5 minutes refetchOnWindowFocus: false, }); diff --git a/lib/supabase/fan_segments/selectFanSegments.ts b/lib/supabase/fan_segments/selectFanSegments.ts deleted file mode 100644 index 78ffddc7f..000000000 --- a/lib/supabase/fan_segments/selectFanSegments.ts +++ /dev/null @@ -1,36 +0,0 @@ -import serverClient from "../serverClient"; -import { Tables } from "@/types/database.types"; - -type Social = Tables<"socials">; - -interface SelectFanSegmentsParams { - segment_id: string; -} - -export const selectFanSegments = async ( - params: SelectFanSegmentsParams -): Promise => { - try { - const { data, error } = await serverClient - .from("fan_segments") - .select(`socials!fan_segments_fan_social_id_fkey (*)`) - .eq("segment_id", params.segment_id); - - if (error) { - console.error("Error fetching fan segments with socials:", error); - throw error; - } - - // Extract and flatten the socials data from the joined result - const socialsData = - data - ?.flatMap((item: { socials: Social[] }) => item.socials || []) - .filter((social): social is Social => social !== null) - .sort((a, b) => (b.followerCount || 0) - (a.followerCount || 0)) || []; - - return socialsData; - } catch (error) { - console.error("Error in selectFanSegments:", error); - throw error; - } -}; diff --git a/lib/supabase/getArtistSegmentNames.ts b/lib/supabase/getArtistSegmentNames.ts deleted file mode 100644 index e8f0e7537..000000000 --- a/lib/supabase/getArtistSegmentNames.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ArtistSegment } from "./getArtistSegments"; -import supabase from "./serverClient"; - -/** - * Get all segments associated with an artist - */ -export async function getArtistSegmentNames( - artistId: string -): Promise { - try { - const { data: segments, error: segmentsError } = await supabase - .from("artist_segments") - .select( - ` - *, - segment:segments(*) - ` - ) - .eq("artist_account_id", artistId); - - if (segmentsError) { - console.error("Error fetching segments:", segmentsError); - return []; - } - - if (!segments?.length) { - return []; - } - - return segments; - } catch (error) { - console.error("Error in getArtistSegmentNames:", error); - return []; - } -} diff --git a/lib/supabase/getArtistSegments.ts b/lib/supabase/getArtistSegments.ts deleted file mode 100644 index 63d12a207..000000000 --- a/lib/supabase/getArtistSegments.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { getArtistSegmentNames } from "./getArtistSegmentNames"; -import { getSegmentCounts } from "./getSegmentCounts"; -import { selectFanSegments } from "./fan_segments/selectFanSegments"; -import { Tables } from "@/types/database.types"; - -type Social = Tables<"socials">; - -export interface Segment { - id: string; - name: string; - size: number; - icon?: string; - fans?: Social[]; -} - -export interface ArtistSegment { - id: string; - segment_id: string; - artist_account_id: string; - created_at: string; - segment: { - id: string; - name: string; - }; -} - -export interface SegmentCount { - segment_id: string; - count: number; -} - -export interface FanSegment { - id: string; - artist_segment_id: string; - fan_social_id: string; - created_at: string; -} - -export interface SegmentWithCount extends ArtistSegment { - fan_count: number; -} - -/** - * Get all segments with their fan counts for an artist - */ -export async function getArtistSegments(artistId: string): Promise { - const segments = await getArtistSegmentNames(artistId); - if (!segments.length) return []; - - const segmentIds = segments.map((s) => s.segment_id); - const counts = await getSegmentCounts(segmentIds); - - const countMap = new Map(counts.map((c) => [c.segment_id, c.count])); - - // Get fans for each segment (limit to 5 for social proof) - const segmentsWithFans = await Promise.all( - segments.map(async (segment) => { - const fans = await selectFanSegments({ segment_id: segment.segment_id }); - return { - id: segment.segment_id, - name: segment.segment.name, - size: countMap.get(segment.segment_id) || 0, - icon: undefined, - fans, - }; - }) - ); - - return segmentsWithFans; -} diff --git a/lib/supabase/getSegmentCounts.ts b/lib/supabase/getSegmentCounts.ts deleted file mode 100644 index 83075d661..000000000 --- a/lib/supabase/getSegmentCounts.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { SegmentCount } from "./getArtistSegments"; -import supabase from "./serverClient"; - -/** - * Get fan counts for a list of segment IDs - */ -export async function getSegmentCounts( - segmentIds: string[] -): Promise { - try { - const { data: fanSegments, error: countsError } = await supabase - .from("fan_segments") - .select("segment_id") - .in("segment_id", segmentIds); - - if (countsError) { - console.error("Error fetching fan counts:", countsError); - return []; - } - - const counts = segmentIds.map((segmentId) => ({ - segment_id: segmentId, - count: - fanSegments?.filter((fs) => fs.segment_id === segmentId).length || 0, - })); - - return counts; - } catch (error) { - console.error("Error in getSegmentCounts:", error); - return []; - } -} diff --git a/lib/tools/getArtistSegments.ts b/lib/tools/getArtistSegments.ts index 4b735db70..381f117f4 100644 --- a/lib/tools/getArtistSegments.ts +++ b/lib/tools/getArtistSegments.ts @@ -36,9 +36,12 @@ const getArtistSegments = tool({ inputSchema: schema, execute: async ({ artist_account_id, page, limit }) => { try { - // Construct URL with query parameters - const url = new URL(`${getClientApiBaseUrl()}/api/artist/segments`); - url.searchParams.append("artist_account_id", artist_account_id); + // Construct URL with query parameters. The artist account ID is now + // encoded in the path under the nested `/api/artists/{id}/segments` + // resource; only `page` and `limit` remain as query params. + const url = new URL( + `${getClientApiBaseUrl()}/api/artists/${artist_account_id}/segments`, + ); if (page) url.searchParams.append("page", page.toString()); if (limit) url.searchParams.append("limit", limit.toString()); diff --git a/types/Segment.ts b/types/Segment.ts new file mode 100644 index 000000000..86751df42 --- /dev/null +++ b/types/Segment.ts @@ -0,0 +1,19 @@ +import { Tables } from "@/types/database.types"; + +type Social = Tables<"socials">; + +/** + * Aggregated segment shape consumed by segment list UI + * (`SegmentsWrapper`, `FanGroupNavItem`, `MiniMenu`, `Segments`, `SegmentButton`). + * + * `fans` is optional because the dedicated `GET /api/artists/{id}/segments` + * endpoint does not return a fan roster; components that render avatars + * fall back to an empty array. + */ +export interface Segment { + id: string; + name: string; + size: number; + icon?: string; + fans?: Social[]; +} From 11911ef130dca5ba49cbf7aaedad81adf7eb097e Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Thu, 16 Apr 2026 02:44:19 +0530 Subject: [PATCH 2/2] refactor(chat): extract getArtistSegments lib helper from hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following project pattern (fetchArtists, deleteArtist) — network call lives in lib/artists/*, the hook just wires Privy auth + react-query. --- hooks/useArtistSegments.ts | 56 ++------------------------------ lib/artists/getArtistSegments.ts | 52 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 53 deletions(-) create mode 100644 lib/artists/getArtistSegments.ts diff --git a/hooks/useArtistSegments.ts b/hooks/useArtistSegments.ts index 08a96d1b8..3c4713e2a 100644 --- a/hooks/useArtistSegments.ts +++ b/hooks/useArtistSegments.ts @@ -1,56 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { usePrivy } from "@privy-io/react-auth"; -import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; -import type { Segment } from "@/types/Segment"; - -/** - * Shape returned by the dedicated `GET /api/artists/{id}/segments` endpoint. - * We map this into the existing consumer `Segment[]` shape so components - * (`SegmentsWrapper`, `FanGroupNavItem`, `MiniMenu`) do not change. - */ -interface MappedArtistSegment { - id: string; - name: string; - size: number; - icon?: string; -} - -interface ArtistSegmentsResponse { - status: string; - segments: MappedArtistSegment[]; - pagination: { - total_count: number; - page: number; - limit: number; - total_pages: number; - }; -} - -async function fetchSegments( - artistId: string, - accessToken: string, -): Promise { - const response = await fetch( - `${getClientApiBaseUrl()}/api/artists/${artistId}/segments`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - if (!response.ok) { - throw new Error(`Failed to fetch segments: ${response.status}`); - } - const data: ArtistSegmentsResponse = await response.json(); - return data.segments - .map((s) => ({ - id: s.id, - name: s.name, - size: s.size, - icon: s.icon, - })) - .filter((s) => s.size > 0); -} +import { getArtistSegments } from "@/lib/artists/getArtistSegments"; export function useArtistSegments(artistId?: string) { const { getAccessToken, authenticated } = usePrivy(); @@ -58,10 +8,10 @@ export function useArtistSegments(artistId?: string) { queryKey: ["segments", artistId], queryFn: async () => { const accessToken = await getAccessToken(); - return fetchSegments(artistId!, accessToken!); + return getArtistSegments(accessToken!, artistId!); }, enabled: !!artistId && authenticated, - staleTime: 1000 * 60 * 5, // 5 minutes + staleTime: 1000 * 60 * 5, refetchOnWindowFocus: false, }); } diff --git a/lib/artists/getArtistSegments.ts b/lib/artists/getArtistSegments.ts new file mode 100644 index 000000000..5c45fc721 --- /dev/null +++ b/lib/artists/getArtistSegments.ts @@ -0,0 +1,52 @@ +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; +import type { Segment } from "@/types/Segment"; + +interface MappedArtistSegment { + id: string; + name: string; + size: number; + icon?: string; +} + +interface GetArtistSegmentsResponse { + status: "success" | "error"; + segments?: MappedArtistSegment[]; + pagination?: { + total_count: number; + page: number; + limit: number; + total_pages: number; + }; + error?: string; +} + +/** + * Fetches aggregated segments for an artist from the dedicated API. + * + * @param accessToken - Privy access token for Bearer auth + * @param artistId - Artist account ID (path-encoded) + * @returns Consumer `Segment[]` shape, filtered to `size > 0` + */ +export async function getArtistSegments( + accessToken: string, + artistId: string, +): Promise { + const response = await fetch( + `${getClientApiBaseUrl()}/api/artists/${artistId}/segments`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + const data: GetArtistSegmentsResponse = await response.json(); + + if (!response.ok || data.status === "error") { + throw new Error(data.error || "Failed to fetch segments"); + } + + return (data.segments || []) + .map((s) => ({ id: s.id, name: s.name, size: s.size, icon: s.icon })) + .filter((s) => s.size > 0); +}