diff --git a/.env.example b/.env.example index 1f5691f..5683e7c 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,7 @@ GITHUB_PR_COUNT=80 GITHUB_ISSUE_COUNT=20 GITHUB_DISCUSSION_COUNT=10 GITHUB_USER_STALE_DAYS=14 +NEXT_PUBLIC_SITE_URL=https://your-domain.com # ── Database (PostgreSQL) ─────────────────────────────────────────────────── DATABASE_URL=postgresql://devimpact:devimpact@localhost:5432/devimpact?sslmode=disable diff --git a/src/features/developer/components/user-profile-client.tsx b/src/features/developer/components/user-profile-client.tsx index d351df4..d5bc04c 100644 --- a/src/features/developer/components/user-profile-client.tsx +++ b/src/features/developer/components/user-profile-client.tsx @@ -1,11 +1,12 @@ "use client"; - +import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; import type { Route } from "next"; import { useClipboardCopy } from "@/hooks"; import { ArrowLeft, + BadgeCheck, Check, Copy, ExternalLink, @@ -49,10 +50,52 @@ export function UserProfileClient({ user, location, countryParam }: Props) { const { t } = useTranslation(); const searchParams = useSearchParams(); const { copied, copy } = useClipboardCopy(); + const { copied: markdownCopied, copy: copyMarkdown } = useClipboardCopy(); + const { copied: htmlCopied, copy: copyHtml } = useClipboardCopy(); + const [badgeDialogOpen, setBadgeDialogOpen] = useState(false); + const [badgeStyle, setBadgeStyle] = useState<"flat" | "flat-square" | "gradient">("flat-square"); const displayName = user.name?.trim() || user.username; const githubUrl = `https://github.com/${user.username}`; const compareUrl = `/?user1=${encodeURIComponent(user.username)}`; + const scoreColor = (s: number) => + s >= 80 ? "brightgreen" : s >= 60 ? "green" : s >= 40 ? "yellow" : s >= 20 ? "orange" : "red"; + const esc = (s: string) => s.replace(/-/g, "--").replace(/_/g, "__").replace(/ /g, "_"); + const { badgeUrl, markdownSnippet, htmlSnippet } = useMemo(() => { + const score = user.normalizedFinalScore ?? 0; + const isGradient = badgeStyle === "gradient"; + const color = isGradient ? scoreColor(score) : "blue"; + const shieldStyle = isGradient ? "flat-square" : badgeStyle; + + const badgeUrl = + `https://img.shields.io/badge/${esc("DevImpact Score")}-${esc(score.toFixed(1))}-${color}` + + `?style=${shieldStyle}&logo=github`; + const siteUrl = process.env.NEXT_PUBLIC_SITE_URL; + let origin = ""; + if (siteUrl) { + origin = siteUrl.endsWith("/") ? siteUrl.slice(0, -1) : siteUrl; + } else if (typeof window !== "undefined") { + origin = window.location.origin; + } + + const profileUrl = `${origin}/user/${encodeURIComponent(user.username)}`; + + return { + badgeUrl, + markdownSnippet: `[![DevImpact Score](${badgeUrl})](${profileUrl})`, + htmlSnippet: `DevImpact Score`, + }; + }, [user.normalizedFinalScore, user.username, badgeStyle]); + + const handleCopyMarkdown = () => copyMarkdown(markdownSnippet); + const handleCopyHtml = () => copyHtml(htmlSnippet); + + useEffect(() => { + if (!badgeDialogOpen) return; + const onKey = (e: KeyboardEvent) => e.key === "Escape" && setBadgeDialogOpen(false); + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [badgeDialogOpen]); // Location & Country detection const detectedSlug = detectCountry(location ?? null); @@ -188,8 +231,8 @@ export function UserProfileClient({ user, location, countryParam }: Props) { {/* ── Header Profile Hero Section ────────────────────────────── */}
-
-
+
+
@@ -263,9 +308,11 @@ export function UserProfileClient({ user, location, countryParam }: Props) { {copied ? ( <> + {t("profile.copiedShort")} + {t("profile.copied")} @@ -273,11 +320,25 @@ export function UserProfileClient({ user, location, countryParam }: Props) { ) : ( <> + {t("profile.shareShort")} + {t("profile.copyLink")} )} + + {/* Get README Badge */} +
@@ -288,6 +349,7 @@ export function UserProfileClient({ user, location, countryParam }: Props) {

{t("profile.scoreOverview")}

+

{t("profile.title")}

@@ -301,18 +363,21 @@ export function UserProfileClient({ user, location, countryParam }: Props) { highlight={true} helperText={t("tooltip.final")} /> + + + {t("profile.scoreDistribution")} + {t("methodology.sections.weights.formula")} +
@@ -338,10 +405,12 @@ export function UserProfileClient({ user, location, countryParam }: Props) { {t("breakdown.repo")} (45%) + {t("profile.signalShare", { pct: repoWeightPct })}
+
@@ -351,10 +420,12 @@ export function UserProfileClient({ user, location, countryParam }: Props) { {t("breakdown.pr")} (45%) + {t("profile.signalShare", { pct: prWeightPct })} + @@ -364,10 +435,12 @@ export function UserProfileClient({ user, location, countryParam }: Props) { {t("breakdown.contribution")} (10%) + {t("profile.signalShare", { pct: contributionWeightPct })} +
@@ -379,6 +452,7 @@ export function UserProfileClient({ user, location, countryParam }: Props) {

{t("topwork.title")}

+

{t("profile.topWork")}

@@ -392,8 +466,10 @@ export function UserProfileClient({ user, location, countryParam }: Props) { {t("topwork.toprepos")} + {t("topwork.desc")} + {user.topRepos.length === 0 ? (

{t("empty.repos")}

@@ -419,8 +495,10 @@ export function UserProfileClient({ user, location, countryParam }: Props) { {t("topwork.topprs")} + {t("topwork.desc")} + {user.topPullRequests.length === 0 ? (

{t("empty.pullRequests")}

@@ -446,8 +524,10 @@ export function UserProfileClient({ user, location, countryParam }: Props) { {t("community.title")} + {t("community.comments")} + {user.topCommunityContributions && user.topCommunityContributions.length > 0 ? ( user.topCommunityContributions @@ -476,10 +556,12 @@ export function UserProfileClient({ user, location, countryParam }: Props) { {t("signals.title")} + ▼ +
{signalEntries.map((entry) => (

{entry.label}

+

{entry.value}

))} @@ -502,8 +585,10 @@ export function UserProfileClient({ user, location, countryParam }: Props) { {t("explanations.title")} +

{t("methodology.cta.description")}

+
+ + {badgeDialogOpen && ( +
setBadgeDialogOpen(false)} + > +
e.stopPropagation()} + > +
+

+ {t("profile.getBadge")} +

+

{t("profile.badgeDescription")}

+
+ + {/* Live preview */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {`DevImpact +
+ + {/* Style picker */} +
+

{t("profile.badgeStyle")}

+
+ {( + [ + ["flat", "profile.flat"], + ["flat-square", "profile.flatSquare"], + ["gradient", "profile.scoreColor"], + ] as const + ).map(([value, labelKey]) => ( + + ))} +
+
+ + {/* Markdown */} +
+

{t("profile.markdownLabel")}

+
+ e.currentTarget.select()} + className="min-w-0 flex-1 rounded-md border bg-muted px-3 py-2 text-xs" + /> + +
+
+ + {/* HTML */} +
+

{t("profile.htmlLabel")}

+
+ e.currentTarget.select()} + className="min-w-0 flex-1 rounded-md border bg-muted px-3 py-2 text-xs" + /> + +
+
+ +
+ +
+
+
+ )}
); } diff --git a/src/locales/ar.json b/src/locales/ar.json index f581900..60defac 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -281,6 +281,18 @@ "profile.signalShare": "{pct}% من الإشارة النهائية", "profile.title": "تأثير وإحصائيات المطور", "profile.topWork": "أبرز الأعمال والمساهمات", + "profile.badgeDescription": "احصل على شارة لملف README الخاص بك على GitHub.", + "profile.badgeStyle": "نمط الشارة", + "profile.markdownLabel": "ماركداون", + "profile.htmlLabel": "إتش تي إم إل", + "profile.copy": "نسخ", + "profile.getBadge": "الحصول على شارة README", + "profile.close": "إغلاق", + "profile.flat": "مسطح", + "profile.copyMarkdown": "نسخ ماركداون", + "profile.copyHtml": "نسخ إتش تي إم إل", + "profile.flatSquare": "مسطح مربع", + "profile.scoreColor": "لون النتيجة", "profile.transparencyTitle": "إشارات شفافية التقييم", "profile.viewFullStats": "عرض الملف الشخصي للمطور", "profile.viewOnGithub": "عرض الملف الشخصي على GitHub" diff --git a/src/locales/en.json b/src/locales/en.json index db82ac7..25ad91f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -281,6 +281,21 @@ "profile.signalShare": "{pct}% of final signal", "profile.title": "Developer Impact & Statistics", "profile.topWork": "Top Work & Highlights", + "profile.getBadge": "Get README Badge", + "profile.markdownLabel": "Markdown", + "profile.htmlLabel": "HTML", + "profile.badgeCopied": "Badge copied", + "profile.badgeDescription": "Get a badge for your GitHub Profile README.", + "profile.markdown": "Markdown", + "profile.html": "HTML", + "profile.copy": "Copy", + "profile.close": "Close", + "profile.copyMarkdown": "Copy Markdown", + "profile.copyHtml": "Copy HTML", + "profile.badgeStyle": "Badge Style", + "profile.flat": "Flat", + "profile.flatSquare": "Flat Square", + "profile.scoreColor": "Score Color", "profile.transparencyTitle": "Scoring Transparency Signals", "profile.viewFullStats": "View developer profile", "profile.viewOnGithub": "View GitHub Profile"