From 82b08abb46bddd24722c81e7c6dab938181f8719 Mon Sep 17 00:00:00 2001 From: Samson Okunola Date: Wed, 15 Jul 2026 13:47:13 +0100 Subject: [PATCH 1/2] feat(members): implement role selection in profile form and add member roles constant --- SKILLS.md | 10 +++ next.config.ts | 3 + src/app/(public)/page.tsx | 3 +- src/features/home/community.tsx | 2 +- src/features/home/hero.tsx | 2 +- src/features/members/member-card.tsx | 71 +++++++++++++-------- src/features/members/profile-form.tsx | 28 ++++++-- src/features/members/profile-schema.ts | 6 +- src/features/members/spotlight-carousel.tsx | 2 +- src/features/members/spotlight-section.tsx | 24 +++++-- src/features/members/types.ts | 17 +++++ 11 files changed, 120 insertions(+), 48 deletions(-) create mode 100644 SKILLS.md diff --git a/SKILLS.md b/SKILLS.md new file mode 100644 index 0000000..36165f8 --- /dev/null +++ b/SKILLS.md @@ -0,0 +1,10 @@ + + +--- + +## Session Learnings (2026-07-15 13:32) + + +### Code Conventions + +- remove the title input and it should be add by role diff --git a/next.config.ts b/next.config.ts index cf6dcfd..cd8fc24 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,6 +10,9 @@ const nextConfig: NextConfig = { { hostname: "*.unsplash.com", protocol: "https" }, { hostname: "avatars.githubusercontent.com", protocol: "https" }, { hostname: "cdn.jsdelivr.net", protocol: "https" }, + { hostname: "res.cloudinary.com", protocol: "https" }, + { hostname: "*.gravatar.com", protocol: "https" }, + { hostname: "gravatar.com", protocol: "https" }, ], }, }; diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 2fc1a8f..2871627 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -47,13 +47,12 @@ export default async function Page() { -
+
-
); } diff --git a/src/features/home/community.tsx b/src/features/home/community.tsx index 7faaba9..3ad01bf 100644 --- a/src/features/home/community.tsx +++ b/src/features/home/community.tsx @@ -86,7 +86,7 @@ export const Community = ({ contributors, wall }: CommunityProps) => { #{index + 1}
-

{contributor.name ?? contributor.login}

+

{contributor.name ?? contributor.login}

@{contributor.login}

diff --git a/src/features/home/hero.tsx b/src/features/home/hero.tsx index c43036c..030b4c8 100644 --- a/src/features/home/hero.tsx +++ b/src/features/home/hero.tsx @@ -82,7 +82,7 @@ export const Hero = ({ featured, stats, faces }: HeroProps) => { {featured && ( - +

{reducedMotion ? (
diff --git a/src/features/members/member-card.tsx b/src/features/members/member-card.tsx index b628b61..897a716 100644 --- a/src/features/members/member-card.tsx +++ b/src/features/members/member-card.tsx @@ -1,10 +1,9 @@ import { ExternalLink } from "lucide-react"; - -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import type { MemberProfile } from "./types"; +import Image from "next/image"; const MAX_VISIBLE_SKILLS = 4; @@ -30,36 +29,52 @@ export const MemberCard = ({ member }: { member: MemberProfile }) => { const overflowCount = member.skills.length - visibleSkills.length; return ( -
-
- - {member.avatarUrl && } - {initialsOf(member.displayName)} - -
-

{member.displayName}

-

{member.title}

+
+ {/* Avatar */} + {member.avatarUrl ? ( + {member.displayName} + ) : ( +
+ {initialsOf(member.displayName)}
-
+ )} -
- {member.experience} - {visibleSkills.map((skill) => ( - - {skill} - - ))} - {overflowCount > 0 && +{overflowCount}} + {/* Gradient overlay always visible at bottom */} +
+

{member.displayName}

- {member.portfolioUrl && ( - - )} + {/* Hover panel */} +
+

{member.displayName}

+

{member.title}

+ + {member.bio && ( +

{member.bio}

+ )} + + {member.skills.length > 0 && ( +
+ {visibleSkills.map((skill) => ( + {skill} + ))} + {overflowCount > 0 && ( + +{overflowCount} + )} +
+ )} + +
+ {member.experience} + {member.portfolioUrl && ( + + )} +
+
); }; diff --git a/src/features/members/profile-form.tsx b/src/features/members/profile-form.tsx index afcf320..877045c 100644 --- a/src/features/members/profile-form.tsx +++ b/src/features/members/profile-form.tsx @@ -9,7 +9,7 @@ import type { z } from "zod"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { EXPERIENCE_LEVELS, type MemberProfile } from "./types"; +import { EXPERIENCE_LEVELS, MEMBER_ROLES, type MemberProfile } from "./types"; import { useMyProfile, useUpdateProfile } from "./hooks"; import { Textarea } from "@/components/ui/textarea"; import { profileSchema } from "./profile-schema"; @@ -26,7 +26,8 @@ const MAX_AVATAR_BYTES = 2 * 1024 * 1024; const toFormInput = (profile: MemberProfile): ProfileFormInput => ({ displayName: profile.displayName, - title: profile.title, + // Legacy profiles may hold a free-text title; force a fresh role pick for those. + title: (MEMBER_ROLES as readonly string[]).includes(profile.title) ? profile.title : "", bio: profile.bio ?? "", portfolioUrl: profile.portfolioUrl ?? "", githubUrl: profile.githubUrl ?? "", @@ -202,10 +203,25 @@ export const ProfileForm = () => {
- - + Role + ( + + )} + /> {form.formState.errors.title && (

{form.formState.errors.title.message}

)} diff --git a/src/features/members/profile-schema.ts b/src/features/members/profile-schema.ts index ab3e86d..58577d5 100644 --- a/src/features/members/profile-schema.ts +++ b/src/features/members/profile-schema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { EXPERIENCE_LEVELS } from "./types"; +import { EXPERIENCE_LEVELS, MEMBER_ROLES } from "./types"; /** An optional URL field: accepts a valid URL, or an empty string normalized to `undefined`. */ const optionalUrl = z @@ -22,7 +22,9 @@ export const profileSchema = z.object({ .trim() .min(1, "Display name is required.") .max(100, "Display name must be 100 characters or fewer."), - title: z.string().trim().min(1, "Title is required.").max(100, "Title must be 100 characters or fewer."), + title: z.string().refine((value) => (MEMBER_ROLES as readonly string[]).includes(value), { + message: "Choose a role.", + }), bio: z .string() .trim() diff --git a/src/features/members/spotlight-carousel.tsx b/src/features/members/spotlight-carousel.tsx index 2b3b6cf..a8e9c85 100644 --- a/src/features/members/spotlight-carousel.tsx +++ b/src/features/members/spotlight-carousel.tsx @@ -32,7 +32,7 @@ export const SpotlightCarousel = () => { return (
{members.map((member) => ( -
+
))} diff --git a/src/features/members/spotlight-section.tsx b/src/features/members/spotlight-section.tsx index 4b71f1a..7a49d31 100644 --- a/src/features/members/spotlight-section.tsx +++ b/src/features/members/spotlight-section.tsx @@ -2,21 +2,31 @@ import { SpotlightCarousel } from "./spotlight-carousel"; +import { Reveal } from "@/features/home"; + /** - * Self-contained "Member Spotlight" section for the home page: heading plus the - * spotlight carousel. Mount directly inside a `
` wrapper. + * Self-contained "Member Spotlight" section for the home page: eyebrow label, + * editorial heading, and the spotlight carousel — matching the other home + * sections. Mount directly inside a `
` wrapper. * * @returns The spotlight section. * @example - *
- *
+ *
+ *
* *
*
*/ export const MembersSpotlightSection = () => ( -
-

Member Spotlight

- +
+ +

[ Member Spotlight ]

+

+ Meet the people you'll ship with. +

+
+ + +
); diff --git a/src/features/members/types.ts b/src/features/members/types.ts index f6117fc..8342c01 100644 --- a/src/features/members/types.ts +++ b/src/features/members/types.ts @@ -4,6 +4,23 @@ export const EXPERIENCE_LEVELS = ["Junior", "Mid", "Senior", "Lead"] as const; /** Union of valid `MemberProfile.experience` values. */ export type ExperienceLevel = (typeof EXPERIENCE_LEVELS)[number]; +/** Roles a member can pick for their profile; stored as `title` on the backend. */ +export const MEMBER_ROLES = [ + "Frontend Developer", + "Backend Developer", + "Fullstack Developer", + "Mobile Developer", + "DevOps Engineer", + "Data Engineer", + "Machine Learning Engineer", + "UI/UX Designer", + "QA Engineer", + "Product Manager", +] as const; + +/** Union of valid member role values. */ +export type MemberRole = (typeof MEMBER_ROLES)[number]; + /** A member's public profile, as returned by every `/members/*` endpoint. */ export type MemberProfile = { id: string; From 2febb7fb3cbbddd1d57f73b159f006c8519ddfe8 Mon Sep 17 00:00:00 2001 From: Samson Okunola Date: Wed, 15 Jul 2026 13:50:41 +0100 Subject: [PATCH 2/2] style(member-card): improve className formatting for consistency and readability --- src/features/members/member-card.tsx | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/features/members/member-card.tsx b/src/features/members/member-card.tsx index 897a716..e2bf6e3 100644 --- a/src/features/members/member-card.tsx +++ b/src/features/members/member-card.tsx @@ -29,18 +29,18 @@ export const MemberCard = ({ member }: { member: MemberProfile }) => { const overflowCount = member.skills.length - visibleSkills.length; return ( -
+
{/* Avatar */} {member.avatarUrl ? ( {member.displayName} ) : ( -
+
{initialsOf(member.displayName)}
)} {/* Gradient overlay always visible at bottom */} -
+

{member.displayName}

@@ -49,17 +49,19 @@ export const MemberCard = ({ member }: { member: MemberProfile }) => {

{member.displayName}

{member.title}

- {member.bio && ( -

{member.bio}

- )} + {member.bio &&

{member.bio}

} {member.skills.length > 0 && (
{visibleSkills.map((skill) => ( - {skill} + + {skill} + ))} {overflowCount > 0 && ( - +{overflowCount} + + +{overflowCount} + )}
)} @@ -67,7 +69,12 @@ export const MemberCard = ({ member }: { member: MemberProfile }) => {
{member.experience} {member.portfolioUrl && ( -