diff --git a/apps/api/src/app/(site)/[locale]/members/[username]/member-profile-content.tsx b/apps/api/src/app/(site)/[locale]/members/[username]/member-profile-content.tsx new file mode 100644 index 0000000..08fe35f --- /dev/null +++ b/apps/api/src/app/(site)/[locale]/members/[username]/member-profile-content.tsx @@ -0,0 +1,114 @@ +import type { Member } from "@gov-portal/api-client"; +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; +import { type Dictionary, type Locale, localePath } from "@/lib/i18n"; + +export function MemberProfileContent({ + profile, + dict, + locale, + isOwner, +}: { + profile: Member; + dict: Dictionary; + locale: Locale; + isOwner: boolean; +}) { + const bio = profile.bio?.trim() ?? ""; + const hasSkills = profile.skills.length > 0; + const hasAffiliation = profile.affiliation !== null; + const hasLinks = profile.links.length > 0; + const hasDetails = bio !== "" || hasSkills || hasAffiliation || hasLinks; + + return ( +
+
+

+ {dict.member.about} +

+
+ + {hasDetails ? ( +
+ {bio !== "" ? ( +

+ {bio} +

+ ) : null} + + {hasSkills || hasAffiliation || hasLinks ? ( +
+ {hasSkills ? ( +
+
{dict.member.skills}
+
+ {profile.skills.map((skill) => ( + + {dict.members.skillNames[skill]} + + ))} +
+
+ ) : null} + + {hasAffiliation ? ( +
+
+ {dict.member.affiliationLabel} +
+
{profile.affiliation}
+
+ ) : null} + + {hasLinks ? ( +
+
{dict.member.links}
+
+ {profile.links.map((link) => ( + + {link.replace(/^https?:\/\//, "")} + + ))} +
+
+ ) : null} +
+ ) : null} +
+ ) : ( +
+

{dict.member.emptyTitle}

+

+ {dict.member.emptyBody} +

+ {isOwner ? ( + + ) : null} +
+ )} +
+ ); +} diff --git a/apps/api/src/app/(site)/[locale]/members/[username]/page.tsx b/apps/api/src/app/(site)/[locale]/members/[username]/page.tsx index 342e7d0..717dc85 100644 --- a/apps/api/src/app/(site)/[locale]/members/[username]/page.tsx +++ b/apps/api/src/app/(site)/[locale]/members/[username]/page.tsx @@ -2,7 +2,6 @@ import { BriefcaseIcon, - BuildingsIcon, CheckCircleIcon, GithubLogoIcon, MapPinIcon, @@ -21,6 +20,8 @@ import { useActor, useLocale, useMember } from "@/hooks"; import { ApiError } from "@/lib/api-error"; import { localePath } from "@/lib/i18n"; +import { MemberProfileContent } from "./member-profile-content"; + export default function MemberDetailPage() { const { locale, dict } = useLocale(); const { username } = useParams<{ username: string }>(); @@ -75,17 +76,13 @@ export default function MemberDetailPage() { const isOwner = actor !== null && actor.member.githubId === profile.githubId; const isPending = actor !== null && isOwner && actor.member.status !== "approved"; - const hasBio = profile.bio !== null; - const hasSidebar = - profile.skills.length > 0 || profile.affiliation !== null || profile.links.length > 0; - return ( -
+
{/* Breadcrumb Bar */}
- Home + {dict.member.home} / @@ -170,7 +167,7 @@ export default function MemberDetailPage() { {copied ? ( - Copied link! + {dict.member.copiedLink} ) : null}
@@ -194,83 +191,14 @@ export default function MemberDetailPage() {
{/* Main Content Area */} -
+
{isPending ? ( {dict.profile.status[actor.member.status]} ) : null} -
- {hasBio ? ( -
-

- {profile.bio} -

-
- ) : null} - - {/* Right Sidebar Column */} - {hasSidebar ? ( -
- {/* Skills Card */} - {profile.skills.length > 0 ? ( -
-

Skills

-
- {profile.skills.map((skill) => ( - - {skill} - - ))} -
-
- ) : null} - - {/* Affiliation Card */} - {profile.affiliation !== null ? ( -
-

Affiliation

-
-
- - {profile.affiliation} -
-
-
- ) : null} - - {/* Links Card */} - {profile.links.length > 0 ? ( -
-

Links

-
- {profile.links.map((link) => ( - - {link.replace(/^https?:\/\//, "")} - - ))} -
-
- ) : null} -
- ) : null} -
+
); diff --git a/apps/api/src/lib/i18n.ts b/apps/api/src/lib/i18n.ts index 95762c8..8bcc08a 100644 --- a/apps/api/src/lib/i18n.ts +++ b/apps/api/src/lib/i18n.ts @@ -222,6 +222,13 @@ const en = { }, member: { kicker: "Member profile", + home: "Home", + about: "About", + emptyTitle: "No additional details yet", + emptyBody: "This member has not added a bio, skills, affiliation, or links.", + addDetails: "Add profile details", + shareProfile: "Share profile", + copiedLink: "Link copied", locationLabel: "Location", affiliationLabel: "Affiliation", skills: "Skills", @@ -560,6 +567,13 @@ const ne: Dictionary = { }, member: { kicker: "सदस्य प्रोफाइल", + home: "गृहपृष्ठ", + about: "परिचय", + emptyTitle: "थप विवरण अझै छैन", + emptyBody: "यो सदस्यले परिचय, सीप, संस्था वा लिंकहरू थपेका छैनन्।", + addDetails: "प्रोफाइल विवरण थप्नुहोस्", + shareProfile: "प्रोफाइल साझा गर्नुहोस्", + copiedLink: "लिंक कपी भयो", locationLabel: "स्थान", affiliationLabel: "संस्था", skills: "सीपहरू", diff --git a/apps/api/tests/unit/member-profile-content.test.tsx b/apps/api/tests/unit/member-profile-content.test.tsx new file mode 100644 index 0000000..d79ac47 --- /dev/null +++ b/apps/api/tests/unit/member-profile-content.test.tsx @@ -0,0 +1,81 @@ +import type { Member } from "@gov-portal/api-client"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; + +import { MemberProfileContent } from "@/app/(site)/[locale]/members/[username]/member-profile-content"; +import { getDictionary } from "@/lib/i18n"; + +const emptyProfile: Member = { + githubId: 123, + githubUsername: "member", + displayName: "Member", + headline: null, + affiliation: null, + location: null, + bio: null, + links: [], + skills: [], + avatarUrl: null, +}; + +describe("public member profile content", () => { + it("shows a meaningful empty card for visitors", () => { + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain('aria-labelledby="member-about-heading"'); + expect(html).toContain("No additional details yet"); + expect(html).not.toContain("Add profile details"); + }); + + it("offers the owner a route to complete an empty profile", () => { + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain("थप विवरण अझै छैन"); + expect(html).toContain('href="/ne/profile"'); + }); + + it("keeps sparse details in the same content card", () => { + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain("Public Office"); + expect(html).toContain("Engineering"); + expect(html).not.toContain("No additional details yet"); + expect(html.match(/
{ + const html = renderToStaticMarkup( + , + ); + + expect(html).toContain("Building public tools."); + expect(html).toContain('href="https://example.org"'); + expect(html).not.toContain("No additional details yet"); + }); +});