Skip to content

Commit 62306a0

Browse files
committed
feat: score card table UI
1 parent ddd7fe2 commit 62306a0

File tree

4 files changed

+283
-43
lines changed

4 files changed

+283
-43
lines changed

frontend/src/components/common/Typography/Typography.tsx

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,9 @@
11
import React, { PropsWithChildren } from 'react';
22

3-
import { TypographyProps, TypographyVariants } from '@app/components/common/Typography/Typography.interface';
3+
import { TypographyProps } from '@app/components/common/Typography/Typography.interface';
4+
import { variantsStyles } from '@app/components/common/Typography/variantsStyles';
45
import { generateClassNames } from '@app/utils';
56

6-
const variantsStyles: {
7-
[key in TypographyVariants]: string;
8-
} = {
9-
'hint/caps-medium': 'text-xs tracking-widest uppercase font-medium',
10-
'hint/regular': 'text-xs tracking-wider font-normal',
11-
'hint/medium': 'text-xs font-medium',
12-
'hint/semibold': 'text-xs font-semibold',
13-
'hint/bold': 'text-xs font-bold',
14-
'body-s/regular': 'text-sm tracking-wide font-normal',
15-
'body-s/medium': 'text-sm tracking-wider font-medium',
16-
'body-s/semibold': 'text-sm tracking-wider font-semibold',
17-
'body-s/bold': 'text-sm tracking-wide font-bold',
18-
'body-m/regular': 'text-base tracking-wider font-normal',
19-
'body-m/medium': 'text-base font-medium',
20-
'body-m/semibold': 'text-base font-semibold',
21-
'body-m/bold': 'text-base font-bold',
22-
'body-l/regular': 'text-lg font-normal',
23-
'body-l/medium': 'text-lg font-medium',
24-
'body-l/semibold': 'text-lg font-semibold',
25-
'body-l/bold': 'text-xl font-bold',
26-
'head-s/regular': 'text-xl font-normal',
27-
'head-s/medium': 'text-xl font-medium',
28-
'head-s/semibold': 'text-xl font-semibold',
29-
'head-s/bold': 'text-xl font-bold',
30-
'head-m/regular': 'text-2xl font-normal',
31-
'head-m/medium': 'text-2xl font-medium',
32-
'head-m/semibold': 'text-2xl font-semibold',
33-
'head-m/bold': 'text-2xl font-bold',
34-
'head-l/regular': 'text-3xl font-normal',
35-
'head-l/medium': 'text-3xl font-medium',
36-
'head-l/semibold': 'text-3xl font-semibold',
37-
'head-l/bold': 'text-3xl font-bold',
38-
'head-xl/regular': 'text-4xl font-normal',
39-
'head-xl/medium': 'text-4xl font-medium',
40-
'head-xl/semibold': 'text-4xl font-semibold',
41-
'head-xl/bold': 'text-4xl font-bold',
42-
'head-2xl/regular': 'text-5xl font-normal',
43-
'head-2xl/medium': 'text-5xl font-medium',
44-
'head-2xl/semibold': 'text-5xl font-semibold',
45-
'head-2xl/bold': 'text-5xl font-bold',
46-
};
47-
487
export const Typography = ({
498
variant = 'body-m/regular',
509
as,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { TypographyVariants } from '@app/components/common/Typography/Typography.interface';
2+
3+
export const variantsStyles: {
4+
[key in TypographyVariants]: string;
5+
} = {
6+
'hint/caps-medium': 'text-xs tracking-widest uppercase font-medium',
7+
'hint/regular': 'text-xs tracking-wider font-normal',
8+
'hint/medium': 'text-xs font-medium',
9+
'hint/semibold': 'text-xs font-semibold',
10+
'hint/bold': 'text-xs font-bold',
11+
'body-s/regular': 'text-sm tracking-wide font-normal',
12+
'body-s/medium': 'text-sm tracking-wider font-medium',
13+
'body-s/semibold': 'text-sm tracking-wider font-semibold',
14+
'body-s/bold': 'text-sm tracking-wide font-bold',
15+
'body-m/regular': 'text-base tracking-wider font-normal',
16+
'body-m/medium': 'text-base font-medium',
17+
'body-m/semibold': 'text-base font-semibold',
18+
'body-m/bold': 'text-base font-bold',
19+
'body-l/regular': 'text-lg font-normal',
20+
'body-l/medium': 'text-lg font-medium',
21+
'body-l/semibold': 'text-lg font-semibold',
22+
'body-l/bold': 'text-xl font-bold',
23+
'head-s/regular': 'text-xl font-normal',
24+
'head-s/medium': 'text-xl font-medium',
25+
'head-s/semibold': 'text-xl font-semibold',
26+
'head-s/bold': 'text-xl font-bold',
27+
'head-m/regular': 'text-2xl font-normal',
28+
'head-m/medium': 'text-2xl font-medium',
29+
'head-m/semibold': 'text-2xl font-semibold',
30+
'head-m/bold': 'text-2xl font-bold',
31+
'head-l/regular': 'text-3xl font-normal',
32+
'head-l/medium': 'text-3xl font-medium',
33+
'head-l/semibold': 'text-3xl font-semibold',
34+
'head-l/bold': 'text-3xl font-bold',
35+
'head-xl/regular': 'text-4xl font-normal',
36+
'head-xl/medium': 'text-4xl font-medium',
37+
'head-xl/semibold': 'text-4xl font-semibold',
38+
'head-xl/bold': 'text-4xl font-bold',
39+
'head-2xl/regular': 'text-5xl font-normal',
40+
'head-2xl/medium': 'text-5xl font-medium',
41+
'head-2xl/semibold': 'text-5xl font-semibold',
42+
'head-2xl/bold': 'text-5xl font-bold',
43+
};
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { FC, Fragment, PropsWithChildren } from 'react';
2+
3+
import { Button } from '@app/components/common/Button';
4+
import { variantsStyles } from '@app/components/common/Typography/variantsStyles';
5+
import { ClassNameProps } from '@app/types/common';
6+
import { generateClassNames } from '@app/utils';
7+
8+
const bands = [
9+
{
10+
band: 'Band 1',
11+
buckets: [
12+
{ name: 'Programming language', level: 2, bandScore: 4, threshold: 2, earned: 4 },
13+
{ name: 'UI/UX Development', level: 2, bandScore: 4, threshold: 2, earned: 4 },
14+
],
15+
},
16+
{
17+
band: 'Band 2',
18+
buckets: [
19+
{ name: 'Dynamic Data & Systems Integration', level: 0, bandScore: 0, threshold: 6, earned: 4 },
20+
{ name: 'VCS', level: 0, bandScore: 0, threshold: 6, earned: 4 },
21+
],
22+
},
23+
{
24+
band: 'Band 3',
25+
buckets: [{ name: 'Bucket', level: 0, bandScore: 0, threshold: 11, earned: 4 }],
26+
},
27+
{
28+
band: 'Band 4',
29+
buckets: [{ name: 'Bucket', level: 0, bandScore: 0, threshold: 11, earned: 4 }],
30+
},
31+
{
32+
band: 'Band 5',
33+
buckets: [{ name: 'Bucket', level: 0, bandScore: 0, threshold: 11, earned: 4 }],
34+
},
35+
{
36+
band: 'Band 6',
37+
buckets: [{ name: 'Bucket', level: 0, bandScore: 0, threshold: 11, earned: 4 }],
38+
},
39+
{
40+
band: 'Band 7',
41+
buckets: [{ name: 'Bucket', level: 0, bandScore: 0, threshold: 11, earned: 4 }],
42+
},
43+
{
44+
band: 'Band 8',
45+
buckets: [{ name: 'Bucket', level: 0, bandScore: 0, threshold: 11, earned: 4 }],
46+
},
47+
{
48+
band: 'Band 9',
49+
buckets: [{ name: 'Bucket', level: 0, bandScore: 0, threshold: 11, earned: 4 }],
50+
},
51+
{
52+
band: 'Band 10',
53+
buckets: [{ name: 'Bucket', level: 0, bandScore: 0, threshold: 11, earned: 4 }],
54+
},
55+
{
56+
band: 'Band 11',
57+
buckets: [{ name: 'Bucket', level: 0, bandScore: 0, threshold: 11, earned: 4 }],
58+
},
59+
{
60+
band: 'Band 12',
61+
buckets: [{ name: 'Bucket', level: 0, bandScore: 0, threshold: 11, earned: 4 }],
62+
},
63+
{
64+
band: 'Band 13',
65+
buckets: [{ name: 'Bucket', level: 0, bandScore: 0, threshold: 11, earned: 4 }],
66+
},
67+
];
68+
69+
// INFO: For now seniority levels are hardcoded. First 3 bands are junior, next
70+
// 3 are mid, and the rest are senior.
71+
enum Seniority {
72+
Junior = 'junior',
73+
Mid = 'mid',
74+
Senior = 'senior',
75+
}
76+
77+
const TableHeader: FC<
78+
PropsWithChildren<{
79+
last?: boolean;
80+
}> &
81+
ClassNameProps
82+
> = ({ children, className, last }) => {
83+
return (
84+
<th
85+
className={generateClassNames(
86+
'border-b border-navy-200 p-4 text-center uppercase text-navy-500',
87+
variantsStyles['hint/caps-medium'],
88+
!last && 'border-r',
89+
className,
90+
)}
91+
>
92+
{children}
93+
</th>
94+
);
95+
};
96+
97+
const TableCell: FC<
98+
PropsWithChildren<
99+
{
100+
lastRow?: boolean;
101+
lastCol?: boolean;
102+
rowSpan?: number;
103+
isHeader?: boolean;
104+
} & ClassNameProps
105+
>
106+
> = ({ children, lastRow, lastCol, rowSpan, className }) => {
107+
return (
108+
<td
109+
className={generateClassNames(
110+
'border-r border-navy-200 p-4 text-center text-navy-700',
111+
!lastRow && 'border-b',
112+
!lastCol && 'border-r',
113+
variantsStyles['body-s/semibold'],
114+
className,
115+
)}
116+
rowSpan={rowSpan}
117+
>
118+
{children}
119+
</td>
120+
);
121+
};
122+
123+
export const Dot: FC<ClassNameProps> = ({ className }) => {
124+
return <div className={generateClassNames('size-3 rounded-full', className)} />;
125+
};
126+
127+
export const ScoreCardTable = () => {
128+
const bandsWithSeniority = bands.reduce<Record<Seniority, typeof bands>>(
129+
(acc, { band, buckets }, bandIndex) => {
130+
if (bandIndex < 3) {
131+
acc.junior.push({ band, buckets });
132+
} else if (bandIndex < 6) {
133+
acc.mid.push({ band, buckets });
134+
} else {
135+
acc.senior.push({ band, buckets });
136+
}
137+
return acc;
138+
},
139+
{
140+
junior: [],
141+
mid: [],
142+
senior: [],
143+
},
144+
);
145+
146+
const bucketsCount = bands.reduce<number>((acc, { buckets }) => acc + buckets.length, 0);
147+
148+
const getBucketsInSeniorityCount = (seniority: Seniority) =>
149+
bandsWithSeniority[seniority].reduce<number>((acc, { buckets }) => acc + buckets.length, 0);
150+
151+
const renderLevelDots = (level: number) => {
152+
return (
153+
<div className="flex gap-2">
154+
{Array.from({ length: 3 }).map((_, index) => (
155+
<Dot key={index} className={level > index ? 'bg-green-600' : 'bg-navy-300'} />
156+
))}
157+
</div>
158+
);
159+
};
160+
161+
return (
162+
<div className="overflow-x-auto">
163+
<table className="min-w-full border-separate border-spacing-0 rounded-lg border border-navy-200">
164+
<thead>
165+
<tr className="bg-gray-100">
166+
<TableHeader>Seniority</TableHeader>
167+
<TableHeader>Band</TableHeader>
168+
<TableHeader>Bucket</TableHeader>
169+
<TableHeader>Advancement Level</TableHeader>
170+
<TableHeader>Band Score</TableHeader>
171+
<TableHeader>Threshold</TableHeader>
172+
<TableHeader>Score Earned</TableHeader>
173+
<TableHeader last>Action</TableHeader>
174+
</tr>
175+
</thead>
176+
<tbody>
177+
{Object.entries(bandsWithSeniority).map(([seniority, bands], seniorityIndex) => (
178+
<Fragment key={seniority}>
179+
{bands.map(({ band, buckets }, bandIndex) => (
180+
<Fragment key={band}>
181+
{buckets.map((bucket, bucketIndex) => {
182+
const lastRow = seniorityIndex + bandIndex + bucketIndex === bucketsCount + 1;
183+
const bucketsInSeniority = getBucketsInSeniorityCount(seniority as Seniority);
184+
console.log('bucketsInSeniority', bucketsInSeniority);
185+
return (
186+
<tr key={`${band}-${bucketIndex}`}>
187+
{bucketIndex + bandIndex === 0 && (
188+
<TableCell
189+
rowSpan={bucketsInSeniority}
190+
lastRow={seniority === 'senior'}
191+
className="uppercase"
192+
>
193+
{seniority}
194+
</TableCell>
195+
)}
196+
{bucketIndex === 0 && (
197+
<TableCell rowSpan={buckets.length} lastRow={lastRow}>
198+
{band}
199+
</TableCell>
200+
)}
201+
<TableCell lastRow={lastRow} className="font-normal">
202+
{bucket.name}
203+
</TableCell>
204+
<TableCell lastRow={lastRow}>{renderLevelDots(bucket.level)}</TableCell>
205+
{bucketIndex === 0 && (
206+
<>
207+
<TableCell rowSpan={buckets.length} lastRow={lastRow}>
208+
{bucket.bandScore}
209+
</TableCell>
210+
<TableCell rowSpan={buckets.length} lastRow={lastRow}>
211+
{bucket.threshold}
212+
</TableCell>
213+
<TableCell rowSpan={buckets.length} lastRow={lastRow}>
214+
{bucket.earned}
215+
</TableCell>
216+
<TableCell rowSpan={buckets.length} lastCol lastRow={lastRow} className="text-center">
217+
<Button variant="link" className="mx-auto">
218+
Evaluate
219+
</Button>
220+
</TableCell>
221+
</>
222+
)}
223+
</tr>
224+
);
225+
})}
226+
</Fragment>
227+
))}
228+
</Fragment>
229+
))}
230+
</tbody>
231+
</table>
232+
</div>
233+
);
234+
};

frontend/src/types/common.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ export interface CheckboxOption {
1717
}
1818

1919
export type ClassName = HTMLProps<HTMLElement>['className'];
20+
21+
export interface ClassNameProps {
22+
className?: ClassName;
23+
}

0 commit comments

Comments
 (0)