|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import React, { memo } from "react"; |
4 | | -import Image from "next/image"; |
5 | | -import Link from "next/link"; |
6 | | -import DefaultAvatar from "@/components/DefaultAvatar"; |
7 | | -import { IconGithub, IconLink } from "@/public/assets/svgs"; |
8 | | -import { cn } from "@/lib/utils"; |
| 3 | +import React from "react"; |
| 4 | +import { getTestDate } from "@/app/applicant/waiting/_service/util"; |
9 | 5 | import { |
10 | | - Table as TableInstance, |
11 | | -} from "@tanstack/react-table"; |
| 6 | + Table, |
| 7 | + TableBody, |
| 8 | + TableCell, |
| 9 | + TableHead, |
| 10 | + TableHeader, |
| 11 | + TableRow, |
| 12 | +} from "@/components/ui/table"; |
| 13 | +import { cn } from "@/lib/utils"; |
| 14 | +import { flexRender, Table as ReactTable } from "@tanstack/react-table"; |
12 | 15 |
|
13 | 16 | import { NewApplicantType } from "../../_service/types"; |
14 | | -import ApplicantRowActionButton from "./applicantRowActionButton"; |
15 | | -import ApplicantTestTimeRemaining from "../applicantTestTimeRemaining"; |
16 | | -import ApplicantActionButton from "./applicantActionButton"; |
17 | 17 |
|
18 | | -function ApplicantMobileTableComponent<TData extends NewApplicantType>({ |
| 18 | +interface ApplicantMobileTableProps<TData extends NewApplicantType> { |
| 19 | + table: ReactTable<TData>; |
| 20 | +} |
| 21 | + |
| 22 | +function ApplicantMobileTable<TData extends NewApplicantType>({ |
19 | 23 | table, |
20 | | -}: { |
21 | | - table: TableInstance<TData>; |
22 | | -}) { |
| 24 | +}: ApplicantMobileTableProps<TData>) { |
| 25 | + const toBeFailed = ( |
| 26 | + testTaken: string | null | undefined, |
| 27 | + forkUrl: string | null | undefined, |
| 28 | + ): Boolean => { |
| 29 | + if (!testTaken) return false; |
| 30 | + if (forkUrl) return false; |
| 31 | + |
| 32 | + const currentDate = new Date(); |
| 33 | + const testTakenDate = getTestDate(new Date(testTaken || "") || new Date()); |
| 34 | + const difference = testTakenDate.getTime() - currentDate.getTime(); |
| 35 | + |
| 36 | + return difference <= 0; |
| 37 | + }; |
| 38 | + |
23 | 39 | return ( |
24 | | - <> |
25 | | - {/* Mobile card layout for smaller screens */} |
26 | | - <div className="block xl:hidden"> |
27 | | - {/* Sleek cards for each applicant */} |
28 | | - <div className="space-y-1"> |
29 | | - {table.getRowModel().rows?.length ? ( |
30 | | - table.getRowModel().rows.map((row) => { |
31 | | - const applicant = row.original; |
32 | | - return ( |
33 | | - <div |
34 | | - key={`${row.id}-${row.index}`} |
| 40 | + <Table className="table"> |
| 41 | + {/* Table header */} |
| 42 | + <TableHeader> |
| 43 | + {table.getHeaderGroups().map((headerGroup) => ( |
| 44 | + <TableRow key={headerGroup.id}> |
| 45 | + {headerGroup.headers |
| 46 | + .filter((_, index) => index <= 1) // 👈 show 1st and 2nd columns |
| 47 | + .map((header) => ( |
| 48 | + <TableHead |
| 49 | + key={header.id} |
35 | 50 | className={cn( |
36 | | - "rounded-md border bg-slate-100 p-2 shadow-sm transition-all hover:shadow-md dark:bg-slate-800 dark:border-slate-600", |
37 | | - applicant.application_status === "testing" && |
38 | | - applicant.applicant?.fork_url && |
39 | | - "bg-green-50 border-green-300 dark:bg-green-900/20 dark:border-green-700", |
40 | | - applicant.application_status === "onboarding" && |
41 | | - "bg-customBlue-50 border-customBlue-300 dark:bg-customBlue-900/20 dark:border-customBlue-700", |
42 | | - applicant.application_status === "denied" && |
43 | | - "bg-red-50 border-red-300 dark:bg-red-900/20 dark:border-red-700", |
| 51 | + "whitespace-nowrap", |
| 52 | + (header.column.columnDef.meta as any)?.className, |
44 | 53 | )} |
45 | 54 | > |
46 | | - {/* Header with checkbox, avatar, name, and action */} |
47 | | - <div className="mb-1.5 flex items-center justify-between"> |
48 | | - <div className="flex items-center gap-2"> |
49 | | - <div |
50 | | - onClick={() => row.toggleSelected(!row.getIsSelected())} |
51 | | - className={cn( |
52 | | - "h-5 w-5 rounded border-2 cursor-pointer transition-all flex items-center justify-center", |
53 | | - row.getIsSelected() |
54 | | - ? "bg-blue-500 border-blue-500" |
55 | | - : "bg-white border-gray-300 dark:bg-gray-700 dark:border-gray-500" |
56 | | - )} |
57 | | - > |
58 | | - {row.getIsSelected() && ( |
59 | | - <span className="text-white text-sm font-bold">✓</span> |
60 | | - )} |
61 | | - </div> |
62 | | - <div className="h-7 w-7 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700"> |
63 | | - {applicant.image_url ? ( |
64 | | - <Image |
65 | | - src={applicant.image_url} |
66 | | - alt={`${applicant.first_name} avatar`} |
67 | | - width={28} |
68 | | - height={28} |
69 | | - className="h-full w-full object-cover" |
70 | | - /> |
71 | | - ) : ( |
72 | | - <DefaultAvatar size={28} /> |
73 | | - )} |
74 | | - </div> |
75 | | - <div className="min-w-0 flex-1"> |
76 | | - <h3 className="truncate text-xs font-semibold text-gray-900 dark:text-gray-100"> |
77 | | - {applicant.first_name.charAt(0).toUpperCase() + applicant.first_name.slice(1)} {" "} |
78 | | - {applicant.last_name.charAt(0).toUpperCase() + applicant.last_name.slice(1)} |
79 | | - </h3> |
80 | | - <p className="truncate text-[11px] text-gray-600 dark:text-gray-400"> |
81 | | - {applicant.email_address} |
82 | | - </p> |
83 | | - </div> |
84 | | - </div> |
85 | | - <ApplicantActionButton applicant={applicant} /> |
86 | | - </div> |
87 | | - |
88 | | - {/* Compact info grid */} |
89 | | - <div className="grid grid-cols-2 gap-x-3 gap-y-1 text-[11px]"> |
90 | | - {/* Left column */} |
91 | | - <div className="space-y-1"> |
92 | | - {/* Position */} |
93 | | - <div className="flex items-center gap-1"> |
94 | | - <span className="text-[10px] opacity-60">💼</span> |
95 | | - <span className="font-medium text-gray-700 dark:text-gray-300"> |
96 | | - {applicant.display_position || "Not specified"} |
97 | | - </span> |
98 | | - </div> |
99 | | - {/* Experience */} |
100 | | - <div className="flex items-center gap-1"> |
101 | | - <span className="text-[10px] opacity-60">⏱</span> |
102 | | - <span className="text-gray-600 dark:text-gray-400"> |
103 | | - {applicant.years_of_experience !== undefined |
104 | | - ? `${applicant.years_of_experience} years` |
105 | | - : "No exp"} |
106 | | - </span> |
107 | | - </div> |
108 | | - </div> |
| 55 | + {header.isPlaceholder |
| 56 | + ? null |
| 57 | + : flexRender( |
| 58 | + header.column.columnDef.header, |
| 59 | + header.getContext(), |
| 60 | + )} |
| 61 | + </TableHead> |
| 62 | + ))} |
| 63 | + </TableRow> |
| 64 | + ))} |
| 65 | + </TableHeader> |
109 | 66 |
|
110 | | - {/* Right column */} |
111 | | - <div className="space-y-1 text-right"> |
112 | | - {/* Applied date */} |
113 | | - <div className="text-gray-600 dark:text-gray-400"> |
114 | | - <span className="text-[10px] opacity-60">📅 </span> |
115 | | - {applicant.date_applied |
116 | | - ? new Date(applicant.date_applied).toLocaleDateString('en-US', { |
117 | | - month: 'short', |
118 | | - day: 'numeric', |
119 | | - year: '2-digit' |
120 | | - }) |
121 | | - : "N/A"} |
122 | | - </div> |
123 | | - {/* Links and test status */} |
124 | | - <div className="flex items-center justify-end gap-1.5"> |
125 | | - {applicant.application_status !== "testing" && applicant.github && ( |
126 | | - <Link |
127 | | - href={applicant.github} |
128 | | - target="_blank" |
129 | | - className="text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200" |
130 | | - > |
131 | | - <IconGithub className="h-3 w-3" /> |
132 | | - </Link> |
133 | | - )} |
134 | | - {applicant.application_status !== "testing" && applicant.portfolio_website && ( |
135 | | - <Link |
136 | | - href={applicant.portfolio_website} |
137 | | - target="_blank" |
138 | | - className="text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200" |
139 | | - > |
140 | | - <IconLink className="h-3 w-3" /> |
141 | | - </Link> |
142 | | - )} |
143 | | - {applicant.application_status === "testing" && ( |
144 | | - <> |
145 | | - <ApplicantTestTimeRemaining applicant={applicant} /> |
146 | | - {applicant.applicant?.fork_url && ( |
147 | | - <Link |
148 | | - href={applicant.applicant.fork_url} |
149 | | - target="_blank" |
150 | | - rel="noopener noreferrer" |
151 | | - className="text-customBlue-600 hover:text-customBlue-800 dark:text-customBlue-400 dark:hover:text-customBlue-300" |
152 | | - > |
153 | | - <IconLink className="h-2.5 w-2.5" /> |
154 | | - </Link> |
155 | | - )} |
156 | | - </> |
157 | | - )} |
158 | | - </div> |
159 | | - </div> |
160 | | - </div> |
161 | | - |
162 | | - {/* Tech stacks - only show if not in testing status */} |
163 | | - {applicant.application_status !== "testing" && applicant.tech_stacks && applicant.tech_stacks.length > 0 && ( |
164 | | - <div className="mt-1 flex items-center gap-1 overflow-hidden"> |
165 | | - <span className="text-[10px] text-gray-500 dark:text-gray-400">💻</span> |
166 | | - <div className="flex gap-0.5 overflow-x-auto"> |
167 | | - {applicant.tech_stacks.slice(0, 4).map((stack) => ( |
168 | | - <span |
169 | | - key={stack} |
170 | | - className="rounded bg-gray-100 px-1 py-0.5 text-[10px] text-gray-600 dark:bg-gray-800 dark:text-gray-400" |
171 | | - > |
172 | | - {stack} |
173 | | - </span> |
174 | | - ))} |
175 | | - {applicant.tech_stacks.length > 4 && ( |
176 | | - <span className="text-[10px] text-gray-500 dark:text-gray-400"> |
177 | | - +{applicant.tech_stacks.length - 4} |
178 | | - </span> |
179 | | - )} |
180 | | - </div> |
181 | | - </div> |
182 | | - )} |
183 | | - </div> |
184 | | - ); |
185 | | - }) |
186 | | - ) : ( |
187 | | - <div className="rounded-md border bg-white p-3 text-center dark:bg-gray-900 dark:border-gray-700"> |
188 | | - <p className="text-xs text-gray-500 dark:text-gray-400">No results found.</p> |
189 | | - </div> |
190 | | - )} |
191 | | - </div> |
192 | | - </div> |
193 | | - </> |
| 67 | + <TableBody> |
| 68 | + {table.getRowModel().rows?.length ? ( |
| 69 | + table.getRowModel().rows.map((row) => ( |
| 70 | + <TableRow |
| 71 | + key={`${row.id}-${row.index}`} |
| 72 | + data-state={row.getIsSelected() && "selected"} |
| 73 | + className={cn( |
| 74 | + (row.original as NewApplicantType).application_status === |
| 75 | + "testing" && |
| 76 | + (row.original as NewApplicantType).applicant?.fork_url && |
| 77 | + "bg-green bg-opacity-5", |
| 78 | + (row.original as NewApplicantType).application_status === |
| 79 | + "testing" && |
| 80 | + toBeFailed( |
| 81 | + (row.original as NewApplicantType).applicant?.test_taken, |
| 82 | + (row.original as NewApplicantType).applicant?.fork_url, |
| 83 | + ) && |
| 84 | + "bg-customRed-100 bg-opacity-5", |
| 85 | + )} |
| 86 | + > |
| 87 | + {row |
| 88 | + .getVisibleCells() |
| 89 | + .filter((_, index) => index <= 1) // 👈 show 1st and 2nd columns |
| 90 | + .map((cell) => ( |
| 91 | + <TableCell |
| 92 | + key={cell.id} |
| 93 | + className={cn( |
| 94 | + (cell.column.columnDef.meta as any)?.className, |
| 95 | + )} |
| 96 | + > |
| 97 | + {flexRender(cell.column.columnDef.cell, cell.getContext())} |
| 98 | + </TableCell> |
| 99 | + ))} |
| 100 | + </TableRow> |
| 101 | + )) |
| 102 | + ) : ( |
| 103 | + <TableRow> |
| 104 | + <TableCell |
| 105 | + // now we render 2 columns, so colspan = 2 |
| 106 | + colSpan={2} |
| 107 | + className="h-24 text-center" |
| 108 | + > |
| 109 | + No results. |
| 110 | + </TableCell> |
| 111 | + </TableRow> |
| 112 | + )} |
| 113 | + </TableBody> |
| 114 | + </Table> |
194 | 115 | ); |
195 | 116 | } |
196 | 117 |
|
197 | | -export default memo(ApplicantMobileTableComponent) as typeof ApplicantMobileTableComponent; |
| 118 | +export default ApplicantMobileTable; |
0 commit comments