Skip to content

Commit d3dc692

Browse files
Zeff01claude
andcommitted
feat: improve applicant modal UI and add utility script
- Refactored ApplicantProfileModal for better layout and responsiveness - Reduced modal size and improved content organization - Compacted profile section with better spacing - Made modal more mobile-friendly - Fixed applicantDataTable to use applicant ID instead of array index for row selection - Added fix-applicants.ts utility script to revert incorrectly accepted applicants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d656cf0 commit d3dc692

3 files changed

Lines changed: 174 additions & 100 deletions

File tree

‎apps/codebility/app/home/applicants/_components/ApplicantProfileModal.tsx‎

Lines changed: 64 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,15 @@ const ApplicantProfileModal = () => {
6767
return (
6868
<Dialog open={isModalOpen} onOpenChange={(open) => !open && closeModal()}>
6969
<DialogContent
70-
className={`mobile:max-w-[95vw] flex max-h-[75vh] min-h-[75vh] flex-col
71-
overflow-y-auto sm:max-w-[90vw] lg:max-w-[80vw] xl:max-w-[70vw] 2xl:max-w-[60vw]
72-
`}
70+
className="flex max-h-[85vh] w-[95vw] max-w-4xl flex-col overflow-hidden"
7371
>
74-
<DialogHeader>
75-
<div className="mobile:flex-col flex gap-6 p-4 sm:flex-col sm:justify-between 2xl:flex-row 2xl:justify-start">
76-
<div className="mobile:flex-col mobile:justify-center mobile:items-center sm:flex-row flex gap-2 sm:items-start sm:justify-start">
77-
<div className="relative flex-shrink-0 ">
78-
{/* Profile Image - 128px size like interns modal */}
72+
<DialogHeader className="overflow-y-auto px-6 pb-0">
73+
<div className="flex flex-col gap-4">
74+
{/* Top Row: Profile Image, Name, Status */}
75+
<div className="flex items-start gap-4">
76+
<div className="relative flex-shrink-0">
7977
{selectedApplicant.image_url ? (
80-
<div className="relative h-32 w-32">
78+
<div className="relative h-20 w-20">
8179
<Image
8280
src={selectedApplicant.image_url}
8381
alt={`${selectedApplicant.first_name}'s profile`}
@@ -86,64 +84,67 @@ const ApplicantProfileModal = () => {
8684
/>
8785
</div>
8886
) : (
89-
<DefaultAvatar size={128} />
87+
<DefaultAvatar size={80} />
9088
)}
9189
</div>
9290

93-
{/* Basic Info */}
94-
<div className="flex flex-col gap-2">
95-
<DialogTitle className="text-2xl font-bold">
96-
{selectedApplicant.first_name} {selectedApplicant.last_name}
97-
</DialogTitle>
91+
<div className="flex min-w-0 flex-1 flex-col gap-2">
92+
<div className="flex items-start justify-between gap-2">
93+
<DialogTitle className="truncate text-xl font-bold">
94+
{selectedApplicant.first_name} {selectedApplicant.last_name}
95+
</DialogTitle>
96+
{selectedApplicant.availability_status ? (
97+
<div className="flex-shrink-0 rounded bg-green-500 px-2 py-1 text-xs text-white">
98+
Available
99+
</div>
100+
) : (
101+
<div className="flex-shrink-0 rounded bg-red-500 px-2 py-1 text-xs text-white">
102+
Unavailable
103+
</div>
104+
)}
105+
</div>
106+
107+
<p className="truncate text-sm text-gray-600 dark:text-gray-300">
108+
{selectedApplicant.display_position || "Position not specified"}
109+
</p>
98110

99-
<div className="flex flex-col gap-1">
100-
<p className="text-lg text-gray-600 dark:text-gray-300">
101-
{selectedApplicant.display_position || "Position not specified"}
102-
</p>
103-
104-
{/* Application Status Badge */}
105-
<div className={`inline-flex w-fit rounded px-2 py-1 text-xs text-white ${getStatusColor(selectedApplicant.application_status)}`}>
111+
<div className="flex flex-wrap items-center gap-2">
112+
<div className={`inline-flex rounded px-2 py-1 text-xs text-white ${getStatusColor(selectedApplicant.application_status)}`}>
106113
{selectedApplicant.application_status?.toUpperCase() || "UNKNOWN"}
107114
</div>
108-
109-
<p>{selectedApplicant.years_of_experience} years experience</p>
115+
<span className="text-sm text-gray-600 dark:text-gray-400">
116+
{selectedApplicant.years_of_experience} years experience
117+
</span>
110118
</div>
111119
</div>
112120
</div>
113121

114-
{/* Contact and Social Information */}
115-
<div className="mobile:flex-col mobile:items-center flex gap-4 sm:items-start sm:justify-between 2xl:justify-start">
116-
{/* Contact Information */}
117-
<div className="mobile:flex-row mobile:gap-4 flex sm:flex-col">
118-
<InfoItem
119-
icon={<Mail className="h-4 w-4" />}
120-
label="Email"
121-
value={selectedApplicant.email_address}
122-
/>
123-
<InfoItem
124-
icon={<Phone className="h-4 w-4" />}
125-
label="Phone"
126-
value={selectedApplicant.phone_number}
127-
/>
128-
<InfoItem
129-
icon={<MapPin className="h-4 w-4" />}
130-
label="Location"
131-
value={selectedApplicant.address}
132-
/>
133-
<InfoItem
134-
icon={<Briefcase className="h-4 w-4" />}
135-
label="Experience"
136-
value={`${selectedApplicant.years_of_experience} years`}
137-
/>
122+
{/* Contact Information - Compact Grid */}
123+
<div className="grid grid-cols-1 gap-2 text-sm sm:grid-cols-2">
124+
<div className="flex items-center gap-2 truncate">
125+
<Mail className="h-4 w-4 flex-shrink-0 text-gray-500" />
126+
<span className="truncate">{selectedApplicant.email_address}</span>
138127
</div>
128+
{selectedApplicant.phone_number && (
129+
<div className="flex items-center gap-2 truncate">
130+
<Phone className="h-4 w-4 flex-shrink-0 text-gray-500" />
131+
<span className="truncate">{selectedApplicant.phone_number}</span>
132+
</div>
133+
)}
134+
{selectedApplicant.address && (
135+
<div className="flex items-center gap-2 truncate">
136+
<MapPin className="h-4 w-4 flex-shrink-0 text-gray-500" />
137+
<span className="truncate">{selectedApplicant.address}</span>
138+
</div>
139+
)}
139140

140141
{/* Social Links */}
141-
<div className="mobile:flex-row flex gap-4">
142+
<div className="flex items-center gap-3">
142143
{selectedApplicant.github && (
143144
<Link
144145
href={selectedApplicant.github}
145146
target="_blank"
146-
className="text-gray-600 hover:text-gray-900"
147+
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
147148
>
148149
<Github className="h-5 w-5" />
149150
</Link>
@@ -152,7 +153,7 @@ const ApplicantProfileModal = () => {
152153
<Link
153154
href={selectedApplicant.linkedin}
154155
target="_blank"
155-
className="text-gray-600 hover:text-gray-900"
156+
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
156157
>
157158
<Linkedin className="h-5 w-5" />
158159
</Link>
@@ -161,7 +162,7 @@ const ApplicantProfileModal = () => {
161162
<Link
162163
href={selectedApplicant.facebook}
163164
target="_blank"
164-
className="text-gray-600 hover:text-gray-900"
165+
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
165166
>
166167
<Facebook className="h-5 w-5" />
167168
</Link>
@@ -170,42 +171,27 @@ const ApplicantProfileModal = () => {
170171
<Link
171172
href={selectedApplicant.discord}
172173
target="_blank"
173-
className="text-gray-600 hover:text-gray-900"
174+
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100"
174175
>
175176
<MessageSquare className="h-5 w-5" />
176177
</Link>
177178
)}
178179
</div>
179180
</div>
180-
181-
{/* Availability Status */}
182-
<div className="mobile:flex-col-reverse absolute right-4 top-12 flex items-center gap-4">
183-
<div>
184-
{selectedApplicant.availability_status ? (
185-
<div className="bg-green-500 rounded px-2 py-1 text-xs text-white">
186-
Available
187-
</div>
188-
) : (
189-
<div className="rounded bg-red-500 px-2 py-1 text-xs text-white">
190-
Unavailable
191-
</div>
192-
)}
193-
</div>
194-
</div>
195181
</div>
196182
</DialogHeader>
197183

198184
{/* Content Tabs */}
199-
<Tabs defaultValue="about" className="mobile:mt-2 flex flex-col gap-10">
200-
<TabsList className="mobile:text-sm mobile:gap-2 mobile:grid-cols-2 grid w-full grid-cols-4">
201-
<TabsTrigger value="about">About</TabsTrigger>
202-
<TabsTrigger value="experience">Experience</TabsTrigger>
203-
<TabsTrigger value="projects">Projects</TabsTrigger>
204-
<TabsTrigger value="skills">Skills</TabsTrigger>
185+
<Tabs defaultValue="about" className="flex min-h-0 flex-1 flex-col overflow-hidden px-6">
186+
<TabsList className="grid w-full grid-cols-4 gap-1">
187+
<TabsTrigger value="about" className="text-xs sm:text-sm">About</TabsTrigger>
188+
<TabsTrigger value="experience" className="text-xs sm:text-sm">Experience</TabsTrigger>
189+
<TabsTrigger value="projects" className="text-xs sm:text-sm">Projects</TabsTrigger>
190+
<TabsTrigger value="skills" className="text-xs sm:text-sm">Skills</TabsTrigger>
205191
</TabsList>
206192

207193
{/* About Tab */}
208-
<TabsContent value="about" className="space-y-6">
194+
<TabsContent value="about" className="mt-4 space-y-4 overflow-y-auto pr-2">
209195
{/* About Section */}
210196
{selectedApplicant.about && (
211197
<Section title="About">
@@ -329,7 +315,7 @@ const ApplicantProfileModal = () => {
329315
</TabsContent>
330316

331317
{/* Experience Tab */}
332-
<TabsContent value="experience" className="mt-4 space-y-6">
318+
<TabsContent value="experience" className="mt-4 space-y-4 overflow-y-auto pr-2">
333319
<Section title="Experience Level">
334320
<div className="rounded-lg border p-4">
335321
<h4 className="font-medium">Years of Experience</h4>
@@ -345,7 +331,7 @@ const ApplicantProfileModal = () => {
345331
</TabsContent>
346332

347333
{/* Projects Tab */}
348-
<TabsContent value="projects" className="mt-4 space-y-6">
334+
<TabsContent value="projects" className="mt-4 space-y-4 overflow-y-auto pr-2">
349335
<div className="text-center py-8">
350336
<div className="mb-4 text-4xl">📁</div>
351337
<h3 className="text-lg font-medium mb-2">No Projects Available</h3>
@@ -371,7 +357,7 @@ const ApplicantProfileModal = () => {
371357
</TabsContent>
372358

373359
{/* Skills Tab */}
374-
<TabsContent value="skills" className="mt-4 space-y-6">
360+
<TabsContent value="skills" className="mt-4 space-y-4 overflow-y-auto pr-2">
375361
{/* Tech Stacks */}
376362
{hasItems(selectedApplicant.tech_stacks) && (
377363
<Section title="Technical Skills">
@@ -423,26 +409,4 @@ const Section = ({
423409
</div>
424410
);
425411

426-
const InfoItem = ({
427-
icon,
428-
label,
429-
value,
430-
}: {
431-
icon: React.ReactNode;
432-
label: string;
433-
value?: string | null;
434-
}) => {
435-
if (!value) return null;
436-
437-
return (
438-
<div className="flex items-center gap-2">
439-
{icon}
440-
<div>
441-
<p className="mobile:hidden text-sm text-gray-500 sm:hidden">{label}</p>
442-
<p className="mobile:hidden text-sm">{value}</p>
443-
</div>
444-
</div>
445-
);
446-
};
447-
448412
export default ApplicantProfileModal;

‎apps/codebility/app/home/applicants/_components/_table/applicantDataTable.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function ApplicantDataTableComponent<TData extends NewApplicantType, TValue>({
7979
onRowSelectionChange: setRowSelection,
8080
onColumnVisibilityChange: setColumnVisibility,
8181
onPaginationChange: setPagination,
82+
getRowId: (row) => row.id, // Use applicant ID instead of array index
8283
state: {
8384
sorting,
8485
rowSelection,

‎apps/codebility/fix-applicants.ts‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* Utility script to find and revert incorrectly accepted applicants
3+
*
4+
* Usage: pnpm tsx apps/codebility/fix-applicants.ts
5+
*/
6+
7+
import { createClient } from '@supabase/supabase-js';
8+
import { config } from 'dotenv';
9+
import { resolve } from 'path';
10+
11+
// Load environment variables from .env.local
12+
config({ path: resolve(process.cwd(), '.env.local') });
13+
14+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
15+
const supabaseServiceKey = process.env.DB_SERVICE_ROLE!;
16+
17+
if (!supabaseUrl || !supabaseServiceKey) {
18+
console.error('❌ Missing required environment variables:');
19+
console.error(' NEXT_PUBLIC_SUPABASE_URL:', supabaseUrl ? '✓' : '✗');
20+
console.error(' DB_SERVICE_ROLE:', supabaseServiceKey ? '✓' : '✗');
21+
process.exit(1);
22+
}
23+
24+
const supabase = createClient(supabaseUrl, supabaseServiceKey);
25+
26+
async function findRecentlyAcceptedApplicants() {
27+
console.log('🔍 Finding recently accepted applicants...\n');
28+
29+
// Get applicants who were recently moved to "passed" status
30+
// Focusing on those accepted in the last 24 hours
31+
const oneDayAgo = new Date();
32+
oneDayAgo.setHours(oneDayAgo.getHours() - 24);
33+
34+
const { data: applicants, error } = await supabase
35+
.from('codev')
36+
.select('id, first_name, last_name, email_address, application_status, date_joined, updated_at')
37+
.eq('application_status', 'passed')
38+
.gte('date_joined', oneDayAgo.toISOString())
39+
.order('date_joined', { ascending: false });
40+
41+
if (error) {
42+
console.error('❌ Error fetching applicants:', error);
43+
return;
44+
}
45+
46+
if (!applicants || applicants.length === 0) {
47+
console.log('✅ No recently accepted applicants found.');
48+
return;
49+
}
50+
51+
console.log(`📋 Found ${applicants.length} recently accepted applicants:\n`);
52+
53+
applicants.forEach((applicant, index) => {
54+
console.log(`${index + 1}. ${applicant.first_name} ${applicant.last_name}`);
55+
console.log(` Email: ${applicant.email_address}`);
56+
console.log(` Accepted: ${new Date(applicant.date_joined).toLocaleString()}`);
57+
console.log(` ID: ${applicant.id}\n`);
58+
});
59+
60+
return applicants;
61+
}
62+
63+
async function revertApplicantsToWaitlist(applicantIds: string[]) {
64+
console.log(`\n🔄 Reverting ${applicantIds.length} applicants back to waitlist...\n`);
65+
66+
const { error } = await supabase
67+
.from('codev')
68+
.update({
69+
application_status: 'waitlist',
70+
role_id: 3, // Applicant role
71+
date_joined: null,
72+
updated_at: new Date().toISOString()
73+
})
74+
.in('id', applicantIds);
75+
76+
if (error) {
77+
console.error('❌ Error reverting applicants:', error);
78+
return false;
79+
}
80+
81+
console.log('✅ Successfully reverted applicants to waitlist status!');
82+
return true;
83+
}
84+
85+
// Main execution
86+
async function main() {
87+
console.log('🚀 Starting applicant recovery process...\n');
88+
89+
const applicants = await findRecentlyAcceptedApplicants();
90+
91+
if (!applicants || applicants.length === 0) {
92+
return;
93+
}
94+
95+
console.log('\n⚠️ REVERTING INCORRECTLY ACCEPTED APPLICANTS ⚠️\n');
96+
97+
// Revert only the 5 incorrectly accepted applicants (keeping Bon Jury, Gerald, and Ahliesa)
98+
const incorrectlyAcceptedIds = [
99+
'19d09872-ce66-4882-aa26-b2d5362069f9', // Ian Macalinao
100+
'2c4c071e-fc65-44c5-bca7-8ba8d312be95', // Romolo Jr Tacmoy
101+
'4e76b9d9-9b12-40f0-b955-60348f09d32a', // Jan Wayne Sepe
102+
'8e3d13be-380c-49ac-bd02-5147f46685c4', // Emmanuel Mingala
103+
'b1a5fe82-bf80-4a1e-aa92-48cd11219ed7', // Jude Janculan
104+
];
105+
106+
await revertApplicantsToWaitlist(incorrectlyAcceptedIds);
107+
}
108+
109+
main();

0 commit comments

Comments
 (0)