Skip to content

Commit b16e8e3

Browse files
author
raineer
committed
fix: Simplify codev qualification to 100+ points only (remove status restrictions)
1 parent e047d1a commit b16e8e3

3 files changed

Lines changed: 140 additions & 82 deletions

File tree

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

Lines changed: 117 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { useMemo, useState } from "react";
66
import Image from "next/image";
77
import Link from "next/link";
88
import CodevBadge from "@/components/CodevBadge";
9-
import { getTotalCodevPoints } from "@/utils/codev-qualification";
109
import {
1110
AnimatePresence,
1211
motion,
@@ -32,32 +31,38 @@ interface Props {
3231
color: string;
3332
}
3433

35-
const STATUS_CONFIG = {
34+
const STATUS_CONFIG: Record<InternalStatus, { label: string; className: string }> = {
3635
TRAINING: {
3736
label: "Training",
38-
className: "bg-yellow-500/20 backdrop-blur-sm text-yellow-200 border border-yellow-500/30 dark:bg-yellow-500/10 dark:text-yellow-300",
37+
className:
38+
"bg-yellow-500/20 backdrop-blur-sm text-yellow-200 border border-yellow-500/30 dark:bg-yellow-500/10 dark:text-yellow-300",
3939
},
4040
GRADUATED: {
4141
label: "Graduated",
42-
className: "bg-green-500/20 backdrop-blur-sm text-green-200 border border-green-500/30 dark:bg-green-500/10 dark:text-green-300",
42+
className:
43+
"bg-green-500/20 backdrop-blur-sm text-green-200 border border-green-500/30 dark:bg-green-500/10 dark:text-green-300",
4344
},
4445
INACTIVE: {
4546
label: "Inactive",
46-
className: "bg-gray-500/20 backdrop-blur-sm text-gray-200 border border-gray-500/30 dark:bg-gray-500/10 dark:text-gray-300",
47+
className:
48+
"bg-gray-500/20 backdrop-blur-sm text-gray-200 border border-gray-500/30 dark:bg-gray-500/10 dark:text-gray-300",
4749
},
4850
MENTOR: {
4951
label: "Mentor",
50-
className: "bg-purple-500/20 backdrop-blur-sm text-purple-200 border border-purple-500/30 dark:bg-purple-500/10 dark:text-purple-300",
52+
className:
53+
"bg-purple-500/20 backdrop-blur-sm text-purple-200 border border-purple-500/30 dark:bg-purple-500/10 dark:text-purple-300",
5154
},
5255
ADMIN: {
5356
label: "Admin",
54-
className: "bg-blue-500/20 backdrop-blur-sm text-blue-200 border border-blue-500/30 dark:bg-blue-500/10 dark:text-blue-300",
57+
className:
58+
"bg-blue-500/20 backdrop-blur-sm text-blue-200 border border-blue-500/30 dark:bg-blue-500/10 dark:text-blue-300",
5559
},
5660
DEPLOYED: {
5761
label: "Deployed",
58-
className: "bg-indigo-500/20 backdrop-blur-sm text-indigo-200 border border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300",
62+
className:
63+
"bg-indigo-500/20 backdrop-blur-sm text-indigo-200 border border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300",
5964
},
60-
} as const;
65+
};
6166

6267
const CodevCard = ({ codev, color }: Props) => {
6368
const [hovered, setHovered] = useState(false);
@@ -78,10 +83,13 @@ const CodevCard = ({ codev, color }: Props) => {
7883
};
7984

8085
const internalStatus = codev.internal_status || "MENTOR";
81-
const statusConfig = STATUS_CONFIG[internalStatus as keyof typeof STATUS_CONFIG] || STATUS_CONFIG.MENTOR;
86+
const statusConfig =
87+
STATUS_CONFIG[internalStatus as InternalStatus] || STATUS_CONFIG.MENTOR;
8288

8389
const filteredLevel = useMemo(() => {
84-
return codev.level && codev.codev_points && Array.isArray(codev.codev_points)
90+
return codev.level &&
91+
codev.codev_points &&
92+
Array.isArray(codev.codev_points)
8593
? Object.fromEntries(
8694
Object.entries(codev.level)
8795
.filter(([skillCategoryId, levelValue]) => {
@@ -93,79 +101,143 @@ const CodevCard = ({ codev, color }: Props) => {
93101
);
94102
})
95103
.sort(([skillCategoryIdA], [skillCategoryIdB]) => {
96-
const pointsA = codev.codev_points!.find((point) => point?.skill_category_id === skillCategoryIdA)?.points || 0;
97-
const pointsB = codev.codev_points!.find((point) => point?.skill_category_id === skillCategoryIdB)?.points || 0;
104+
const pointsA =
105+
codev.codev_points!.find(
106+
(point) => point?.skill_category_id === skillCategoryIdA,
107+
)?.points || 0;
108+
const pointsB =
109+
codev.codev_points!.find(
110+
(point) => point?.skill_category_id === skillCategoryIdB,
111+
)?.points || 0;
98112
return pointsB - pointsA;
99113
}),
100114
)
101115
: {};
102116
}, [codev.level, codev.codev_points]);
103117

104-
const totalPoints = getTotalCodevPoints(codev.codev_points);
105-
106118
return (
107-
<div className="h-64" onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}>
108-
<div onClick={() => window.location.href = `https://www.codebility.tech/profiles/${codev.id}`} className="group relative flex h-full w-full flex-col items-center justify-between rounded-lg border border-white/20 bg-white/10 p-4 shadow-2xl backdrop-blur-md transition-all duration-300 cursor-pointer hover:bg-white/20 hover:scale-[1.02] dark:border-white/10 dark:bg-white/5 dark:hover:bg-white/10">
119+
<div
120+
className="h-64"
121+
onMouseEnter={() => setHovered(true)}
122+
onMouseLeave={() => setHovered(false)}
123+
>
124+
<div
125+
onClick={() => window.location.href = `https://www.codebility.tech/profiles/${codev.id}`}
126+
className="group relative flex h-full w-full flex-col items-center justify-between rounded-lg border border-white/20 bg-white/10 p-4 shadow-2xl backdrop-blur-md transition-all duration-300 cursor-pointer hover:bg-white/20 hover:scale-[1.02] dark:border-white/10 dark:bg-white/5 dark:hover:bg-white/10"
127+
>
128+
{/* Background decoration */}
109129
<div className="from-customBlue-50/30 dark:from-customBlue-950/10 absolute inset-0 bg-gradient-to-br to-purple-50/30 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:to-purple-950/10" />
110130
<div className="absolute -right-4 -top-4 h-32 w-32 rounded-full bg-gradient-to-br from-yellow-400/10 to-orange-400/10 opacity-0 blur-2xl transition-opacity duration-500 group-hover:opacity-100" />
131+
111132
<div className="grid w-full grid-cols-4 gap-2">
112133
<div className="col-span-1" />
113134
<div className="relative col-span-2 flex justify-center">
114-
<div className="absolute -top-2 -right-2 z-20">
115-
<TooltipProvider>
116-
<Tooltip delayDuration={100}>
117-
<TooltipTrigger asChild>
118-
<div className="flex items-center gap-1 rounded-full border border-yellow-500/30 bg-gradient-to-br from-yellow-500/20 to-orange-500/20 px-2.5 py-1 shadow-lg backdrop-blur-sm transition-all duration-300 hover:scale-110 hover:from-yellow-500/30 hover:to-orange-500/30">
119-
<span className="text-xs font-bold text-yellow-200">⭐</span>
120-
<span className="text-xs font-bold text-yellow-200">{totalPoints}</span>
121-
</div>
122-
</TooltipTrigger>
123-
<TooltipContent className="border border-white/20 bg-white/10 px-3 py-2 text-xs text-white backdrop-blur-md dark:border-white/10 dark:bg-white/5">
124-
<div className="flex flex-col gap-1">
125-
<p className="font-semibold">Total Skill Points</p>
126-
<p className="text-gray-200">Qualification: ≥100 points</p>
127-
</div>
128-
</TooltipContent>
129-
</Tooltip>
130-
</TooltipProvider>
131-
</div>
132-
<Image alt={`${codev.first_name} Avatar`} src={codev.image_url && (codev.image_url.startsWith("http") || codev.image_url.startsWith("/")) ? codev.image_url : "/assets/svgs/icon-codebility-black.svg"} width={70} height={70} className={`${color} h-[70px] w-[70px] rounded-full bg-cover object-cover p-0.5`} onMouseMove={handleMouseMove} onError={(e) => { console.error(`Failed to load image for ${codev.first_name}: ${codev.image_url}`); e.currentTarget.src = "/assets/svgs/icon-codebility-black.svg"; }} />
135+
<Image
136+
alt={`${codev.first_name} Avatar`}
137+
src={
138+
codev.image_url &&
139+
(codev.image_url.startsWith("http") ||
140+
codev.image_url.startsWith("/"))
141+
? codev.image_url
142+
: "/assets/svgs/icon-codebility-black.svg"
143+
}
144+
width={70}
145+
height={70}
146+
className={`${color} h-[70px] w-[70px] rounded-full bg-cover object-cover p-0.5`}
147+
onMouseMove={handleMouseMove}
148+
onError={(e) => {
149+
console.error(
150+
`Failed to load image for ${codev.first_name}: ${codev.image_url}`,
151+
);
152+
e.currentTarget.src = "/assets/svgs/icon-codebility-black.svg";
153+
}}
154+
/>
133155
<AnimatePresence>
134156
{hovered && internalStatus && (
135-
<motion.div initial={{ opacity: 0, y: 20, scale: 0.6 }} animate={{ opacity: 1, y: 0, scale: 1, transition: { type: "spring", stiffness: 260, damping: 10 } }} exit={{ opacity: 0, y: 20, scale: 0.6 }} style={{ translateX: translateX, rotate: rotate, whiteSpace: "nowrap" }} className={cn("absolute -top-8 right-1/2 z-50 flex -translate-x-1/2 transform flex-col items-center justify-center rounded-lg px-4 py-2 shadow-xl", statusConfig.className)}>
136-
<div className="relative z-30 text-base">{statusConfig.label}</div>
157+
<motion.div
158+
initial={{ opacity: 0, y: 20, scale: 0.6 }}
159+
animate={{
160+
opacity: 1,
161+
y: 0,
162+
scale: 1,
163+
transition: {
164+
type: "spring",
165+
stiffness: 260,
166+
damping: 10,
167+
},
168+
}}
169+
exit={{ opacity: 0, y: 20, scale: 0.6 }}
170+
style={{
171+
translateX: translateX,
172+
rotate: rotate,
173+
whiteSpace: "nowrap",
174+
}}
175+
className={cn(
176+
"absolute -top-8 right-1/2 z-50 flex -translate-x-1/2 transform flex-col items-center justify-center rounded-lg px-4 py-2 shadow-xl",
177+
statusConfig.className,
178+
)}
179+
>
180+
<div className="relative z-30 text-base">
181+
{statusConfig.label}
182+
</div>
137183
</motion.div>
138184
)}
139185
</AnimatePresence>
140186
<div className="absolute bottom-[1px] right-[1px]">
141-
<p className={cn("rounded-full border-2 p-2 text-[9px]", codev.availability_status ? "border-green-600 bg-green-500" : "border-red-600 bg-red-500")}></p>
187+
<p
188+
className={cn(
189+
"rounded-full border-2 p-2 text-[9px]",
190+
codev.availability_status
191+
? "border-green-600 bg-green-500"
192+
: "border-red-600 bg-red-500",
193+
)}
194+
></p>
142195
</div>
143196
</div>
144197
<div className="col-span-1 flex justify-end">
145198
<TooltipProvider>
146199
<Tooltip delayDuration={100}>
147200
<TooltipTrigger asChild>
148201
<Link href={`/profiles/${codev.id}`} target="_blank" onClick={(e) => e.stopPropagation()}>
149-
<Button variant="outline" size="icon" className="h-8 w-8 rounded-full border-white/30 bg-white/20 backdrop-blur-sm transition-all duration-300 hover:bg-white/30 dark:border-white/20 dark:bg-white/10 dark:hover:bg-white/20">
202+
<Button
203+
variant="outline"
204+
size="icon"
205+
className="h-8 w-8 rounded-full border-white/30 bg-white/20 backdrop-blur-sm transition-all duration-300 hover:bg-white/30 dark:border-white/20 dark:bg-white/10 dark:hover:bg-white/20"
206+
>
150207
<BookOpenIcon className="h-3.5 w-3.5" />
151208
</Button>
152209
</Link>
153210
</TooltipTrigger>
154-
<TooltipContent className="border border-white/20 bg-white/10 px-2 py-1 text-xs text-white backdrop-blur-md dark:border-white/10 dark:bg-white/5">Read Bio</TooltipContent>
211+
<TooltipContent className="border border-white/20 bg-white/10 px-2 py-1 text-xs text-white backdrop-blur-md dark:border-white/10 dark:bg-white/5">
212+
Read Bio
213+
</TooltipContent>
155214
</Tooltip>
156215
</TooltipProvider>
157216
</div>
158217
</div>
159218
<div className="flex flex-col items-center gap-0.5">
160219
<div className="flex flex-col gap-1 text-center">
161-
<p className="md:text-md relative z-10 text-sm capitalize text-white dark:text-white lg:text-base">{codev.first_name} {codev.last_name}</p>
162-
{codev.display_position ? (<p className="relative z-10 text-sm text-gray-200 dark:text-gray-300">{codev.display_position}</p>) : (<div className="relative z-10 text-sm lg:text-base">&nbsp;</div>)}
220+
<p className="md:text-md relative z-10 text-sm capitalize text-white dark:text-white lg:text-base">
221+
{codev.first_name} {codev.last_name}
222+
</p>
223+
{codev.display_position ? (
224+
<p className="relative z-10 text-sm text-gray-200 dark:text-gray-300">
225+
{codev.display_position}
226+
</p>
227+
) : (
228+
<div className="relative z-10 text-sm lg:text-base">&nbsp;</div>
229+
)}
163230
</div>
164231
</div>
165232
<div className="flex min-h-[20px] items-center justify-center">
166-
{filteredLevel && Object.keys(filteredLevel).length > 0 ? (<CodevBadge level={filteredLevel} className="transition-transform group-hover:scale-100" />) : null}
233+
{filteredLevel && Object.keys(filteredLevel).length > 0 ? (
234+
<CodevBadge
235+
level={filteredLevel}
236+
className="transition-transform group-hover:scale-100"
237+
/>
238+
) : null}
167239
</div>
168-
<CodevHireCodevButton codevId={codev.id}/>
240+
<CodevHireCodevButton codevId={codev.id} hovered={hovered} />
169241
</div>
170242
</div>
171243
);

‎apps/codebility/app/(marketing)/profiles/page.tsx‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Suspense } from "react";
22
import { UsersSkeleton } from "@/components/ui/skeleton/UsersSkeleton";
33
import { getCodevs } from "@/lib/server/codev.service";
44
import { Codev } from "@/types/home/codev";
5-
import { prioritizeCodevs } from "@/utils/codev-priority"; // ← CHANGED: Only import prioritizeCodevs
5+
import { prioritizeCodevs } from "@/utils/codev-priority";
66
import { getQualifiedCodevs } from "@/utils/codev-qualification";
77

88
import Section from "../_shared/CodevsSection";
@@ -26,15 +26,13 @@ export default async function Profiles() {
2626
// Convert to array safely
2727
const codevsArray: Codev[] = Array.isArray(allCodevs) ? allCodevs : [];
2828

29-
// ✅ STEP 1: Filter by qualification criteria (100+ points, TRAINING/GRADUATED/NULL, available)
30-
// This handles ALL filtering logic - no need for additional filters
29+
// Filter by qualification criteria (100+ points, passed, available)
30+
// This handles ALL filtering logic - no internal_status restrictions
3131
const qualifiedCodevs = getQualifiedCodevs(codevsArray);
3232

33-
// ✅ STEP 2: Apply priority sorting only (no additional filtering)
34-
// We already filtered everything in getQualifiedCodevs(), so just prioritize by:
35-
// - Active projects > No projects
36-
// - Available > Unavailable
37-
const sortedCodevs = prioritizeCodevs(qualifiedCodevs); // ← CHANGED: Direct prioritization
33+
// Apply priority sorting only (no additional filtering)
34+
// Prioritize by: Active projects > No projects, Available > Unavailable
35+
const sortedCodevs = prioritizeCodevs(qualifiedCodevs);
3836

3937
// Debug logging in development
4038
if (process.env.NODE_ENV === "development") {

‎apps/codebility/utils/codev-qualification.ts‎

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Codev, CodevPoints } from "@/types/home/codev";
44
* Calculate total points across all skill categories for a codev
55
* @param codev_points - Array of CodevPoints from codev.codev_points
66
* @returns Total points sum (0 if undefined/empty)
7+
* @example getTotalCodevPoints([{points: 50}, {points: 75}]) // Returns 125
78
*/
89
export const getTotalCodevPoints = (codev_points?: CodevPoints[]): number => {
910
if (!codev_points || !Array.isArray(codev_points)) {
@@ -19,60 +20,39 @@ export const getTotalCodevPoints = (codev_points?: CodevPoints[]): number => {
1920
/**
2021
* Check if a codev meets the showcase qualification criteria
2122
*
22-
* Qualification Requirements:
23+
* SIMPLIFIED REQUIREMENTS (per team lead feedback):
2324
* 1. Total points >= 100 (graduation requirement)
24-
* 2. internal_status === "TRAINING" | "GRADUATED" | NULL (active developers)
25-
* 3. application_status === "passed" (accepted as codev)
26-
* 4. availability_status === true (currently available for hire)
25+
* 2. application_status === "passed" (accepted as codev)
26+
* 3. availability_status === true (currently available for hire)
2727
*
28-
* Note: TRAINING status is included because many active high-performing codevs
29-
* are still in training phase but meet the 100+ point threshold for showcase.
28+
* Note: internal_status is now IGNORED - we display anyone with 100+ points
29+
* regardless of TRAINING, GRADUATED, MENTOR, etc. status
3030
*
3131
* @param codev - Codev object to check
3232
* @returns true if codev meets ALL qualification criteria
3333
*/
3434
export const isQualifiedForShowcase = (codev: Codev): boolean => {
3535
const totalPoints = getTotalCodevPoints(codev.codev_points);
3636

37-
// Check all qualification criteria
37+
// Simplified qualification criteria (3 checks only)
3838
const meetsPointsThreshold = totalPoints >= 100;
39-
40-
// ✅ FIXED: Accept TRAINING, GRADUATED, or NULL status
41-
// TRAINING = Active codevs building their portfolio (common for 100+ point codevs)
42-
// GRADUATED = Completed training program
43-
// NULL = Legacy active codevs without explicit status
44-
const isActiveCodev =
45-
codev.internal_status === "TRAINING" ||
46-
codev.internal_status === "GRADUATED" ||
47-
codev.internal_status === null ||
48-
codev.internal_status === undefined;
49-
5039
const hasPassedApplication = codev.application_status === "passed";
5140
const isAvailable = codev.availability_status === true;
5241

53-
// Exclude specific statuses that should never show in public showcase
54-
const isExcludedStatus =
55-
codev.internal_status === "INACTIVE" ||
56-
codev.internal_status === "DEPLOYED" ||
57-
codev.internal_status === "ADMIN" ||
58-
codev.internal_status === "MENTOR";
59-
6042
// Debug logging (remove in production)
6143
if (process.env.NODE_ENV === "development") {
62-
if (!meetsPointsThreshold || !isActiveCodev || !hasPassedApplication || !isAvailable || isExcludedStatus) {
44+
if (!meetsPointsThreshold || !hasPassedApplication || !isAvailable) {
6345
console.log(`❌ ${codev.first_name} ${codev.last_name} disqualified:`, {
6446
totalPoints,
6547
meetsPointsThreshold,
6648
internal_status: codev.internal_status,
67-
isActiveCodev,
6849
hasPassedApplication,
6950
isAvailable,
70-
isExcludedStatus,
7151
});
7252
}
7353
}
7454

75-
return meetsPointsThreshold && isActiveCodev && hasPassedApplication && isAvailable && !isExcludedStatus;
55+
return meetsPointsThreshold && hasPassedApplication && isAvailable;
7656
};
7757

7858
/**
@@ -91,6 +71,14 @@ export const getQualifiedCodevs = (codevs: Codev[]): Codev[] => {
9171
// Debug summary
9272
if (process.env.NODE_ENV === "development") {
9373
console.log(`✅ Qualified codevs: ${qualified.length} out of ${codevs.length}`);
74+
75+
// Show total points distribution
76+
const pointsDistribution = qualified.map(c => ({
77+
name: `${c.first_name} ${c.last_name}`,
78+
points: getTotalCodevPoints(c.codev_points),
79+
status: c.internal_status
80+
}));
81+
console.table(pointsDistribution);
9482
}
9583

9684
return qualified;

0 commit comments

Comments
 (0)