-
Notifications
You must be signed in to change notification settings - Fork 15
feat: show social PFP and username on connected artist connectors #1644
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: test
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,11 @@ | ||
| "use client"; | ||
|
|
||
| import { useMemo } from "react"; | ||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { useConnectors } from "@/hooks/useConnectors"; | ||
| import { ALLOWED_ARTIST_CONNECTORS } from "@/lib/composio/allowedArtistConnectors"; | ||
| import { getArtistSocials } from "@/lib/api/artist/getArtistSocials"; | ||
| import { matchSocialToConnector } from "@/lib/composio/matchSocialToConnector"; | ||
| import { ConnectorCard } from "@/components/ConnectorsPage/ConnectorCard"; | ||
| import { Loader2, Plug } from "lucide-react"; | ||
|
|
||
|
|
@@ -14,15 +17,29 @@ interface ArtistConnectorsTabProps { | |
| * Connectors tab content for the Artist Settings modal. | ||
| * Reuses ConnectorCard from the user-level ConnectorsPage (DRY). | ||
| */ | ||
| export function ArtistConnectorsTab({ artistAccountId }: ArtistConnectorsTabProps) { | ||
| const config = useMemo(() => ({ | ||
| accountId: artistAccountId, | ||
| allowedSlugs: [...ALLOWED_ARTIST_CONNECTORS], | ||
| callbackUrl: `${window.location.origin}${window.location.pathname}?artist_connected=true&artist_id=${artistAccountId}`, | ||
| }), [artistAccountId]); | ||
| export function ArtistConnectorsTab({ | ||
| artistAccountId, | ||
| }: ArtistConnectorsTabProps) { | ||
| const config = useMemo( | ||
| () => ({ | ||
| accountId: artistAccountId, | ||
| allowedSlugs: [...ALLOWED_ARTIST_CONNECTORS], | ||
| callbackUrl: `${window.location.origin}${window.location.pathname}?artist_connected=true&artist_id=${artistAccountId}`, | ||
| }), | ||
| [artistAccountId], | ||
| ); | ||
| const { connectors, isLoading, error, authorize, disconnect } = | ||
| useConnectors(config); | ||
|
|
||
| const { data: socialsData } = useQuery({ | ||
| queryKey: ["artistSocials", artistAccountId], | ||
| queryFn: () => getArtistSocials(artistAccountId), | ||
| enabled: !!artistAccountId, | ||
| staleTime: 1000 * 60 * 5, | ||
| }); | ||
|
Comment on lines
+34
to
+39
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This solution won't work currently. The getArtistSocials does not include data from artist connections. We should find a way to update the API to return the data you're expecting.
|
||
|
|
||
| const socials = socialsData?.socials ?? []; | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <div className="flex items-center justify-center py-12"> | ||
|
|
@@ -51,17 +68,23 @@ export function ArtistConnectorsTab({ artistAccountId }: ArtistConnectorsTabProp | |
| return ( | ||
| <div className="space-y-3 py-2"> | ||
| <p className="text-sm text-muted-foreground"> | ||
| Connect third-party services so the AI agent can take actions on behalf of this artist. | ||
| Connect third-party services so the AI agent can take actions on behalf | ||
| of this artist. | ||
| </p> | ||
| <div className="space-y-2"> | ||
| {connectors.map((connector) => ( | ||
| <ConnectorCard | ||
| key={connector.slug} | ||
| connector={connector} | ||
| onConnect={authorize} | ||
| onDisconnect={disconnect} | ||
| /> | ||
| ))} | ||
| {connectors.map((connector) => { | ||
| const social = matchSocialToConnector(connector.slug, socials); | ||
| return ( | ||
| <ConnectorCard | ||
| key={connector.slug} | ||
| connector={connector} | ||
| onConnect={authorize} | ||
| onDisconnect={disconnect} | ||
| socialAvatar={social?.avatar} | ||
| socialUsername={social?.username} | ||
| /> | ||
| ); | ||
| })} | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,8 @@ interface ConnectorCardProps { | |
| connector: ConnectorInfo; | ||
| onConnect: (slug: string) => Promise<string | null>; | ||
| onDisconnect: (connectedAccountId: string) => Promise<boolean>; | ||
| socialAvatar?: string | null; | ||
| socialUsername?: string | null; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -21,6 +23,8 @@ export function ConnectorCard({ | |
| connector, | ||
| onConnect, | ||
| onDisconnect, | ||
| socialAvatar, | ||
| socialUsername, | ||
| }: ConnectorCardProps) { | ||
| const { isConnecting, isDisconnecting, handleConnect, handleDisconnect } = | ||
| useConnectorHandlers({ | ||
|
|
@@ -30,19 +34,41 @@ export function ConnectorCard({ | |
| onDisconnect, | ||
| }); | ||
| const meta = getConnectorMeta(connector.slug); | ||
| const showSocialProfile = | ||
| connector.isConnected && (socialAvatar || socialUsername); | ||
|
|
||
| return ( | ||
| <div className="group flex items-center gap-4 p-4 rounded-xl border border-border bg-card transition-all duration-200 hover:border-muted-foreground/30 hover:shadow-sm"> | ||
| <div className="shrink-0 p-2.5 rounded-xl transition-colors bg-muted/50 group-hover:bg-muted"> | ||
| {getConnectorIcon(connector.slug, 22)} | ||
| <div className="shrink-0 relative"> | ||
| {showSocialProfile && socialAvatar ? ( | ||
| <div className="relative"> | ||
| {/* eslint-disable-next-line @next/next/no-img-element */} | ||
| <img | ||
| src={socialAvatar} | ||
| alt={socialUsername || "Profile"} | ||
| width={40} | ||
| height={40} | ||
| className="rounded-xl object-cover" | ||
| /> | ||
| <div className="absolute -bottom-1 -right-1 p-0.5 rounded-md bg-card"> | ||
| {getConnectorIcon(connector.slug, 14)} | ||
| </div> | ||
| </div> | ||
| ) : ( | ||
| <div className="p-2.5 rounded-xl transition-colors bg-muted/50 group-hover:bg-muted"> | ||
| {getConnectorIcon(connector.slug, 22)} | ||
| </div> | ||
| )} | ||
|
Comment on lines
+42
to
+61
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Open closed principle
|
||
| </div> | ||
|
|
||
| <div className="flex-1 min-w-0"> | ||
| <h3 className="font-medium text-foreground truncate"> | ||
| {formatConnectorName(connector.name, connector.slug)} | ||
| </h3> | ||
| <p className="text-sm text-muted-foreground truncate"> | ||
| {meta.description} | ||
| {showSocialProfile && socialUsername | ||
| ? `@${socialUsername}` | ||
| : meta.description} | ||
| </p> | ||
| </div> | ||
|
|
||
|
|
@@ -53,7 +79,7 @@ export function ConnectorCard({ | |
| onReconnect={handleConnect} | ||
| onDisconnect={handleDisconnect} | ||
| /> | ||
| ) : ( | ||
| ) : ( | ||
| <ConnectorEnableButton | ||
| isConnecting={isConnecting} | ||
| onClick={handleConnect} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import type { Social } from "@/lib/api/artist/getArtistSocials"; | ||
|
|
||
| const CONNECTOR_SOCIAL_DOMAINS: Record<string, string> = { | ||
| instagram: "instagram.com", | ||
| tiktok: "tiktok.com", | ||
| }; | ||
|
Comment on lines
+3
to
+6
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. KISS - why is this mapping needed? doesn't the Social type already include a platform tag? |
||
|
|
||
| /** | ||
| * Find the matching social profile for a connector slug. | ||
| */ | ||
| export function matchSocialToConnector( | ||
| slug: string, | ||
| socials: Social[], | ||
| ): Social | undefined { | ||
| const domain = CONNECTOR_SOCIAL_DOMAINS[slug]; | ||
| if (!domain) return undefined; | ||
| return socials.find((s) => s.profile_url?.toLowerCase().includes(domain)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Match against the URL hostname instead of string Prompt for AI agents
Comment on lines
+15
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Match on the hostname, not a substring.
🔧 Safer match export function matchSocialToConnector(
slug: string,
socials: Social[],
): Social | undefined {
const domain = CONNECTOR_SOCIAL_DOMAINS[slug];
if (!domain) return undefined;
- return socials.find((s) => s.profile_url?.toLowerCase().includes(domain));
+ return socials.find((social) => {
+ try {
+ const hostname = new URL(social.profile_url).hostname.toLowerCase();
+ return hostname === domain || hostname.endsWith(`.${domain}`);
+ } catch {
+ return false;
+ }
+ });
}🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In Next.js App Router, can a"use client"component safely readwindowduring render, or must browser APIs be accessed after mount (for example inuseEffector event handlers)?💡 Result:
In the App Router, a
"use client"component still participates in the initial server render to produce HTML (“Client Components and the RSC Payload are used to prerender HTML”) and is only made interactive after hydration. So it is not safe to readwindow(or other browser-only APIs) during render, because that render can run in a non-browser environment. Use browser APIs after mount (e.g., inuseEffect/useLayoutEffect) or in event handlers (which only run in the browser). [1][2]If you truly need code to run only in the browser even for the initial render, you must opt out of SSR for that component (e.g.,
next/dynamicwithssr: false). [1]Sources
[1] Next.js Docs — Server and Client Components (Last updated Mar 31, 2026) (nextjs.org)
[2] React Docs —
useEffect(“Effects only run on the client. They don’t run during server rendering.”) (react.dev)Citations:
Move callback URL construction out of the render path
useMemoruns during the render phase, and Next.js client components are still prerendered on the server before hydration. Readingwindow.locationhere will throw in the server environment and block rendering. Build the callback URL inuseEffector derive it from Next.js router state instead.🤖 Prompt for AI Agents