From 3264f528ddba4771e15f131aadeea0b72bf85a21 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 07:11:44 +0000 Subject: [PATCH] Show cached cred.blue score badge on profile + explore pages Adds a small client-side badge that hits api.cred.blue's new cached-only /api/score/:handle endpoint. If a score exists, renders it as a compact chip linking to the cred.blue profile; if the account hasn't been scored yet, falls back to a subtle "Not yet scored on cred.blue" link. The endpoint never triggers a fresh compute, so we won't accidentally fan out heavy scoring jobs from page loads. --- src/components/CredBlueScore.tsx | 99 +++++++++++++++++++++++++ src/components/ProfilePreview.tsx | 7 ++ src/components/explore/RepoExplorer.tsx | 9 +++ src/utils/atproto/config.ts | 3 + src/utils/credBlueScore.ts | 36 +++++++++ 5 files changed, 154 insertions(+) create mode 100644 src/components/CredBlueScore.tsx create mode 100644 src/utils/credBlueScore.ts diff --git a/src/components/CredBlueScore.tsx b/src/components/CredBlueScore.tsx new file mode 100644 index 0000000..72daf26 --- /dev/null +++ b/src/components/CredBlueScore.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Gauge } from 'lucide-react'; +import { fetchCachedCredBlueScore, type CredBlueScore } from '@/utils/credBlueScore'; +import { CRED_BLUE_BASE } from '@/utils/atproto/config'; + +type State = + | { status: 'loading' } + | { status: 'ready'; score: CredBlueScore | null }; + +type Props = { + /** Handle or DID — passed straight to api.cred.blue. */ + identifier: string; + /** Optional handle used for the cred.blue profile link. Defaults to identifier. */ + linkHandle?: string; +}; + +export default function CredBlueScoreBadge({ identifier, linkHandle }: Props) { + const [state, setState] = useState({ status: 'loading' }); + + useEffect(() => { + let cancelled = false; + setState({ status: 'loading' }); + fetchCachedCredBlueScore(identifier).then((score) => { + if (!cancelled) setState({ status: 'ready', score }); + }); + return () => { + cancelled = true; + }; + }, [identifier]); + + if (state.status === 'loading') return null; + + const handleForLink = (linkHandle || identifier).replace(/^@/, ''); + const credUrl = `${CRED_BLUE_BASE}/${encodeURIComponent(handleForLink)}`; + + if (!state.score) { + return ( + { + e.currentTarget.style.color = 'var(--text-accent)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.color = 'var(--text-tertiary)'; + }} + > + + Not yet scored on cred.blue → + + ); + } + + const { combined } = state.score.scores; + + return ( + { + e.currentTarget.style.borderColor = 'var(--text-accent)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = 'var(--border-medium)'; + }} + > + + cred.blue + {combined.toLocaleString()} + + ); +} diff --git a/src/components/ProfilePreview.tsx b/src/components/ProfilePreview.tsx index 3f04cb8..8c3fde1 100644 --- a/src/components/ProfilePreview.tsx +++ b/src/components/ProfilePreview.tsx @@ -8,6 +8,7 @@ import { BskyProfile } from '@/utils/profileFetcher'; import { sanitizeUrl } from '@/utils/sanitize'; import { User, Calendar, Users, MessageSquare, Telescope } from 'lucide-react'; import { encodeRepo } from '@/utils/atproto/urls'; +import CredBlueScoreBadge from '@/components/CredBlueScore'; type ProfilePreviewProps = { profile: BskyProfile; @@ -230,6 +231,11 @@ export default function ProfilePreview({ profile }: ProfilePreviewProps) { marginTop: '1rem', borderTop: '1px solid var(--border-subtle)', fontSize: '0.8125rem', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + flexWrap: 'wrap', + gap: '0.75rem', }} > View raw repo in the explorer → + {/* Additional Info */} diff --git a/src/components/explore/RepoExplorer.tsx b/src/components/explore/RepoExplorer.tsx index c4a4dcb..e987deb 100644 --- a/src/components/explore/RepoExplorer.tsx +++ b/src/components/explore/RepoExplorer.tsx @@ -11,6 +11,7 @@ import Breadcrumb from './Breadcrumb'; import CopyButton from './CopyButton'; import ProfileHeader from './ProfileHeader'; import AccountStats from '@/components/account/AccountStats'; +import CredBlueScoreBadge from '@/components/CredBlueScore'; import CollectionsTab from './tabs/CollectionsTab'; import IdentityTab from './tabs/IdentityTab'; import AuditTab from './tabs/AuditTab'; @@ -86,6 +87,14 @@ export default function RepoExplorer({ repo }: { repo: string }) { + +
+ +
+
{/* High-level stats — same tile grid the account-settings page uses, dropped in here so anyone viewing a repo (not just its owner) sees how big it is, when it was created, and how much diff --git a/src/utils/atproto/config.ts b/src/utils/atproto/config.ts index cd90cc7..7c28177 100644 --- a/src/utils/atproto/config.ts +++ b/src/utils/atproto/config.ts @@ -12,3 +12,6 @@ export const JETSTREAM = 'wss://jetstream2.us-east.bsky.network/subscribe'; export const HANDLE_RESOLVER_FALLBACK = 'https://bsky.social'; export const ATURI_BASE = 'https://aturi.to'; + +export const CRED_BLUE_API = 'https://api.cred.blue'; +export const CRED_BLUE_BASE = 'https://cred.blue'; diff --git a/src/utils/credBlueScore.ts b/src/utils/credBlueScore.ts new file mode 100644 index 0000000..a3c89a4 --- /dev/null +++ b/src/utils/credBlueScore.ts @@ -0,0 +1,36 @@ +import { CRED_BLUE_API } from '@/utils/atproto/config'; + +export type CredBlueScore = { + handle: string; + did: string; + scores: { + combined: number; + bluesky: number; + atproto: number; + }; + cachedAt?: string; + version?: string; + source?: 'memory' | 'supabase'; + ageMs?: number; +}; + +/** + * Fetches a cached cred.blue score for the given handle or DID. + * Returns null when no cached score exists (HTTP 204) or on any error — + * the endpoint will not trigger a fresh compute on the backend. + */ +export async function fetchCachedCredBlueScore( + identifier: string, +): Promise { + if (!identifier) return null; + const cleaned = identifier.replace(/^@/, ''); + const url = `${CRED_BLUE_API}/api/score/${encodeURIComponent(cleaned)}`; + try { + const res = await fetch(url, { headers: { accept: 'application/json' } }); + if (res.status === 204 || res.status === 404) return null; + if (!res.ok) return null; + return (await res.json()) as CredBlueScore; + } catch { + return null; + } +} -- 2.51.2