Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/app/api/similar-profiles/[username]/route.ts
Original file line number Diff line number Diff line change
@@ -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<NextResponse> {
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",
},
},
);
}
5 changes: 5 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
15 changes: 15 additions & 0 deletions src/components/profile/ProfileView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2647,6 +2659,9 @@ export function ProfileView({
{/* Multi-Platform Integrations */}
<ProviderIntegrations username={user.login} isOwner={isOwner} />

{/* Similar profiles — "You might also like..." */}
<SimilarProfiles username={user.login} currentUserScore={score} />

{/* Contribution heatmap with year navigation */}
<HeatmapWithYearNav
username={user.login}
Expand Down
Loading
Loading