From ef2697e7f23e26cd9f28b0106e95cd6b62e11e1b Mon Sep 17 00:00:00 2001 From: Aditya8369 Date: Thu, 6 Aug 2026 16:31:39 +0530 Subject: [PATCH] feat: Similar Profiles suggestions --- .../api/similar-profiles/[username]/route.ts | 61 +++ src/app/globals.css | 5 + src/components/profile/ProfileView.tsx | 15 + src/components/profile/SimilarProfiles.tsx | 436 ++++++++++++++++++ src/lib/db.ts | 38 ++ ...260806000000_add_find_similar_profiles.sql | 198 ++++++++ 6 files changed, 753 insertions(+) create mode 100644 src/app/api/similar-profiles/[username]/route.ts create mode 100644 src/components/profile/SimilarProfiles.tsx create mode 100644 supabase/migrations/20260806000000_add_find_similar_profiles.sql diff --git a/src/app/api/similar-profiles/[username]/route.ts b/src/app/api/similar-profiles/[username]/route.ts new file mode 100644 index 000000000..3739abd5e --- /dev/null +++ b/src/app/api/similar-profiles/[username]/route.ts @@ -0,0 +1,61 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { findSimilarProfiles } from "@/lib/db"; + +// Runtime managed by @opennextjs/cloudflare + +interface RouteParams { + params: Promise<{ username: string }>; +} + +/** + * GET /api/similar-profiles/[username] + * + * Returns up to 6 public OSSfolio profiles that are similar to [username], + * ranked by shared top languages (+2 pts) and shared GitHub orgs (+3 pts). + * + * Always returns `{ profiles: [] }` on any error so the profile page degrades + * gracefully — it never breaks because the RPC is absent or returns an error. + * + * Cached at the CDN edge for 5 minutes (s-maxage=300). Similarity is derived + * from profile data that is refreshed on a similar cadence, so 5 min is fine. + */ +export async function GET( + _req: NextRequest, + { params }: RouteParams, +): Promise { + const { username } = await params; + + if (!username || typeof username !== "string") { + return NextResponse.json({ profiles: [] }, { status: 400 }); + } + + const { data, error } = await findSimilarProfiles(username); + + if (error || !Array.isArray(data)) { + // Graceful degradation — log but never surface an error to the caller. + if (error) { + console.error( + `[similar-profiles] RPC error for "${username}":`, + error.message, + ); + } + return NextResponse.json( + { profiles: [] }, + { + status: 200, + headers: { + "Cache-Control": "public, s-maxage=300, stale-while-revalidate=60", + }, + }, + ); + } + + return NextResponse.json( + { profiles: data }, + { + headers: { + "Cache-Control": "public, s-maxage=300, stale-while-revalidate=60", + }, + }, + ); +} diff --git a/src/app/globals.css b/src/app/globals.css index 2cb722202..f00f09530 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -185,6 +185,11 @@ dialog::backdrop { outline-offset: 2px; box-shadow: 0 0 0 3px var(--color-primary); } +.similar-profile-card:hover { + border-color: var(--color-primary); + box-shadow: 0 6px 20px rgba(62, 207, 142, 0.08); + transform: translateY(-2px); +} .footer-social-link { transition: color 0.2s ease; } diff --git a/src/components/profile/ProfileView.tsx b/src/components/profile/ProfileView.tsx index d10b8f113..750b8ab53 100644 --- a/src/components/profile/ProfileView.tsx +++ b/src/components/profile/ProfileView.tsx @@ -86,6 +86,18 @@ const ImpactNetwork = dynamic( }, ); +const SimilarProfiles = dynamic( + () => + import("@/components/profile/SimilarProfiles").then( + (mod) => mod.SimilarProfiles, + ), + { + ssr: false, + // SimilarProfiles has its own skeleton state, so no external fallback needed. + loading: () => null, + }, +); + interface GitHubUser { login: string; name: string | null; @@ -2647,6 +2659,9 @@ export function ProfileView({ {/* Multi-Platform Integrations */} + {/* Similar profiles — "You might also like..." */} + + {/* Contribution heatmap with year navigation */}