Skip to content

Commit 3a1c4c4

Browse files
Merge pull request #515 from CodebilityDev/miguel/codev-bio
Miguel/codev bio
2 parents 1606c50 + d2d1c8b commit 3a1c4c4

10 files changed

Lines changed: 279 additions & 6 deletions

File tree

‎apps/codebility/app/(marketing)/profiles/[id]/_components/ProfileContent.tsx‎

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
11
"use client";
22

3-
import React from "react";
3+
import React, { useEffect, useState } from "react";
44
import Image from "next/image";
55
import Link from "next/link";
66
import { motion } from "framer-motion";
77
import { ClockIcon } from "lucide-react";
88
import { cn } from "@codevs/ui";
9-
import getRandomColor from "@/lib/getRandomColor";
109
import {
1110
IconAbout,
1211
IconGithub,
1312
IconLink,
1413
IconSkills,
14+
IconLinkedInWhiteSmall,
1515
} from "@/public/assets/svgs";
1616
import { Codev } from "@/types/home/codev";
17+
import CodevBadge from "@/components/CodevBadge";
18+
import { getMemberProjects, getMemberRatingScore, ProjectInfo } from "../_services/query";
19+
import StarRating from "./StarRating";
20+
import ProjectList from "./ProjectList";
21+
22+
23+
type LevelMap = Record<string, number>;
24+
25+
function getFilteredLevel(level?: LevelMap): LevelMap {
26+
if (!level) return {};
27+
28+
return Object.fromEntries(
29+
Object.entries(level).filter(([_, value]) => value > 0),
30+
);
31+
}
1732

1833
const fadeInUp = {
1934
hidden: { opacity: 0, y: 20 },
@@ -59,9 +74,46 @@ export default function ProfileContent({
5974
codev,
6075
availableSchedule
6176
}: ProfileContentProps) {
77+
const [ratingScore, setRatingScore] = useState<number | null>(null);
78+
const [memberProjects, setMemberProjects] = useState<ProjectInfo[] | null>(null);
79+
80+
useEffect(() => {
81+
if (!codev.id) return;
82+
83+
const fetchScore = async () => {
84+
try {
85+
const score = await getMemberRatingScore(codev.id);
86+
setRatingScore(score ?? 0);
87+
} catch (err) {
88+
console.error("Failed to fetch member score:", err);
89+
}
90+
};
91+
92+
fetchScore();
93+
94+
const fetchProjects = async () => {
95+
try {
96+
const projects = await getMemberProjects(codev.id);
97+
setMemberProjects(projects);
98+
} catch (err) {
99+
console.error("Failed to fetch projects:", err);
100+
}
101+
};
102+
103+
fetchProjects();
104+
105+
}, [codev.id]);
106+
107+
62108
const allDays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
63109

64-
const getRandomBgColor = () => `bg-${getRandomColor()}`;
110+
// FIXED: Deterministic color based on codev.id to prevent hydration mismatch
111+
const getBgColor = (id: string) => {
112+
const colors = ['bg-bg-codeviolet', 'bg-bg-codemean', 'bg-bg-codegreen', 'bg-bg-codepink'];
113+
// Create a consistent hash from the ID
114+
const hash = id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
115+
return colors[hash % colors.length];
116+
};
65117

66118
const getStatusBadge = () =>
67119
codev.availability_status
@@ -84,6 +136,7 @@ export default function ProfileContent({
84136
const displayHour = hour % 12 || 12;
85137
return `${displayHour}:${minutes} ${ampm}`;
86138
};
139+
87140
const {
88141
first_name,
89142
last_name,
@@ -92,12 +145,19 @@ export default function ProfileContent({
92145
portfolio_website,
93146
about,
94147
github,
148+
linkedin,
95149
tech_stacks,
96150
education,
97151
work_experience,
98152
availability_status,
99153
} = codev;
100154

155+
const filteredLevel = React.useMemo(
156+
() => getFilteredLevel(codev.level),
157+
[codev.level],
158+
);
159+
160+
101161
return (
102162
<div className="mt-6 flex flex-col gap-6 md:gap-12 lg:mt-16 lg:flex-row">
103163
{/* Left Section */}
@@ -117,7 +177,7 @@ export default function ProfileContent({
117177
src={image_url || "/assets/svgs/icon-codebility-black.svg"}
118178
width={200}
119179
height={200}
120-
className={`${getRandomBgColor()} h-[150px] w-[150px] rounded-full bg-cover object-cover`}
180+
className={`${getBgColor(codev.id)} h-[150px] w-[150px] rounded-full object-cover`}
121181
/>
122182
<motion.div
123183
className="absolute bottom-[7px] right-[7px]"
@@ -149,10 +209,29 @@ export default function ProfileContent({
149209
</p>
150210
</motion.div>
151211
)}
212+
{codev.headline && (
213+
<p className="text-center text-sm capitalize lg:text-lg">
214+
{codev.headline}
215+
</p>
216+
)}
152217
<motion.div
153218
className="flex gap-4"
154219
variants={fadeInUp}
155220
>
221+
{linkedin && (
222+
<motion.div
223+
whileHover={{ scale: 1.1, rotate: 5 }}
224+
whileTap={{ scale: 0.95 }}
225+
>
226+
<Link
227+
href={sanitizeUrl(linkedin)}
228+
target="_blank"
229+
className="bg-darkgray hover:bg-black-100 rounded-lg p-2 transition duration-300 block"
230+
>
231+
<IconLinkedInWhiteSmall className="text-2xl" />
232+
</Link>
233+
</motion.div>
234+
)}
156235
{github && (
157236
<motion.div
158237
whileHover={{ scale: 1.1, rotate: 5 }}
@@ -182,6 +261,14 @@ export default function ProfileContent({
182261
</motion.div>
183262
)}
184263
</motion.div>
264+
265+
{codev.level && (
266+
<CodevBadge level={filteredLevel} className="transition-transform group-hover:scale-100"/>
267+
268+
)}
269+
270+
{ratingScore !== null && <StarRating rating={ratingScore} size={24} />}
271+
185272
{tech_stacks && tech_stacks.length > 0 && (
186273
<motion.div
187274
className="mt-4"
@@ -228,6 +315,12 @@ export default function ProfileContent({
228315
</motion.div>
229316
)}
230317

318+
{memberProjects && (
319+
<div className="mt-4">
320+
<ProjectList projects={memberProjects} />
321+
</div>
322+
)}
323+
231324
{/* Work Schedule Section */}
232325
{availableSchedule && (
233326
<motion.div
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from "react";
2+
3+
export interface ProjectInfo {
4+
project_id: string;
5+
name: string;
6+
main_image: string | null;
7+
}
8+
9+
interface ProjectListProps {
10+
projects: ProjectInfo[];
11+
}
12+
13+
const ProjectList: React.FC<ProjectListProps> = ({ projects }) => {
14+
if (!projects || projects.length === 0) {
15+
return <p>No projects found.</p>;
16+
}
17+
18+
return (
19+
<div>
20+
{/* Title on top */}
21+
<h3 className="text-md mb-2 font-semibold lg:text-2xl text-center">
22+
Projects
23+
</h3>
24+
25+
{/* Projects grid */}
26+
<div className="flex flex-wrap justify-center gap-4">
27+
{projects.map((project) => (
28+
<div
29+
key={project.project_id}
30+
className="w-40 rounded-lg p-4 text-center bg-black-100"
31+
>
32+
{project.main_image ? (
33+
<img
34+
src={project.main_image}
35+
alt={project.name}
36+
className="w-full h-32 object-cover rounded"
37+
/>
38+
) : (
39+
<div className="w-full h-32 bg-gray-200 flex items-center justify-center rounded">
40+
No Image
41+
</div>
42+
)}
43+
<h3 className="mt-2 text-lg">{project.name}</h3>
44+
</div>
45+
))}
46+
</div>
47+
</div>
48+
);
49+
};
50+
51+
export default ProjectList;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client";
2+
3+
import { Star, StarHalf } from "lucide-react";
4+
import React from "react";
5+
6+
interface StarRatingProps {
7+
rating: number;
8+
maxStars?: number;
9+
size?: number;
10+
}
11+
12+
export default function StarRating({ rating, maxStars = 5, size = 20 }: StarRatingProps) {
13+
const fullStars = Math.floor(rating);
14+
const hasHalfStar = rating - fullStars >= 0.5;
15+
16+
return (
17+
<div className="star-rating relative flex">
18+
{/* Empty Stars */}
19+
<div className="stars flex gap-1">
20+
{Array.from({ length: maxStars }).map((_, i) => (
21+
<Star key={i} fill="none" stroke="yellow" strokeWidth={2} size={size} />
22+
))}
23+
</div>
24+
25+
{/* Filled Stars */}
26+
<div className="stars rating absolute top-0 left-0 flex gap-1 overflow-hidden"
27+
style={{ width: `${(fullStars + (hasHalfStar ? 0.5 : 0)) / maxStars * 100}%` }}
28+
>
29+
{Array.from({ length: fullStars }).map((_, i) => (
30+
<Star key={`full-${i}`} fill="yellow" strokeWidth={0} size={size} />
31+
))}
32+
{hasHalfStar && <StarHalf key="half" fill="yellow" strokeWidth={0} size={size} />}
33+
</div>
34+
</div>
35+
);
36+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"use server";
2+
3+
import { createClientServerComponent } from "@/utils/supabase/server";
4+
5+
export const getMemberRatingScore = async (memberId: string): Promise<number> => {
6+
try {
7+
const supabase = await createClientServerComponent();
8+
9+
const { data, error } = await supabase
10+
.rpc("calculate_member_rating_score", { member_uuid: memberId })
11+
.single();
12+
13+
if (error) throw error;
14+
15+
// unwrap the actual numeric value from the returned object
16+
const score =
17+
typeof data === "number" ? data : (data as any)?.calculate_member_rating_score;
18+
19+
return score ?? 0; // default to 0 if null or undefined
20+
} catch (error) {
21+
console.error("Error fetching member score:", error);
22+
return 0; // fail gracefully
23+
}
24+
};
25+
26+
27+
export interface ProjectInfo {
28+
project_id: string;
29+
name: string;
30+
main_image: string | null;
31+
}
32+
33+
export const getMemberProjects = async (memberId: string): Promise<ProjectInfo[]> => {
34+
try {
35+
const supabase = await createClientServerComponent();
36+
37+
// Step 1: Get all project_ids for the member
38+
const { data: memberProjects, error: memberError } = await supabase
39+
.from("project_members")
40+
.select("project_id")
41+
.eq("codev_id", memberId);
42+
43+
if (memberError) throw memberError;
44+
if (!memberProjects || memberProjects.length === 0) return [];
45+
46+
const projectIds = memberProjects.map((row) => row.project_id);
47+
48+
49+
50+
// Step 2: Fetch project info for those IDs
51+
const { data: projects, error: projectError } = await supabase
52+
.from("projects")
53+
.select("id, name, main_image") // Use actual column names
54+
.in("id", projectIds);
55+
56+
if (projectError) throw projectError;
57+
if (!projects) return [];
58+
59+
// Map to the ProjectInfo shape
60+
const result: ProjectInfo[] = projects.map((p) => ({
61+
project_id: p.id,
62+
name: p.name,
63+
main_image: p.main_image,
64+
}));
65+
66+
return result;
67+
} catch (error) {
68+
console.error("Error fetching member projects:", error);
69+
return [];
70+
}
71+
};

‎apps/codebility/app/(marketing)/profiles/_components/CodevCard.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ const CodevCard = ({ codev, color }: Props) => {
237237
/>
238238
) : null}
239239
</div>
240-
<CodevHireCodevButton codevId={codev.id} hovered={hovered} />
240+
<CodevHireCodevButton codevId={codev.id} />
241241
</div>
242242
</div>
243243
);

‎apps/codebility/app/home/settings/profile/_components/PersonalInfo.tsx‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type FormValues = {
3232
address: string | undefined;
3333
display_position: string | undefined;
3434
years_of_experience: number;
35+
headline: string | undefined;
3536
};
3637

3738
const PersonalInfo = ({ data }: PersonalInfoProps) => {
@@ -53,6 +54,7 @@ const PersonalInfo = ({ data }: PersonalInfoProps) => {
5354
address: data.address || undefined,
5455
display_position: data.display_position || undefined,
5556
years_of_experience: data.years_of_experience || 0,
57+
headline: data.headline || undefined,
5658
},
5759
});
5860

@@ -199,6 +201,17 @@ const PersonalInfo = ({ data }: PersonalInfoProps) => {
199201
placeholder="Years of experience"
200202
/>
201203

204+
<Input
205+
id="headline"
206+
{...register("headline")}
207+
label="Headline"
208+
disabled={!isEditMode}
209+
parentClassName="flex w-full flex-col justify-between gap-2"
210+
variant={isEditMode ? "lightgray" : "darkgray"}
211+
className="rounded"
212+
placeholder="e.g. Senior Frontend Developer specializing in React"
213+
/>
214+
202215
{isEditMode && (
203216
<div className="mt-5 flex justify-end gap-2">
204217
<Button

‎apps/codebility/lib/server/codev.service.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ export const getCodevs = async ({
113113
id,
114114
skill_category_id,
115115
points
116-
)
116+
),
117+
headline
117118
`);
118119

119120
// Apply filters dynamically

0 commit comments

Comments
 (0)