Skip to content

Commit 0983d34

Browse files
authored
[feat] added case listing card component, introduce styled components (#15)
* added components and changed cases page * added some functionality * refactored styled components to separate files * refactored colors and text components, added scrollbar to card column * add SSR support to styled components * added proof of concept case detail display
1 parent 0e673eb commit 0983d34

File tree

11 files changed

+2792
-189
lines changed

11 files changed

+2792
-189
lines changed

next.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
/** @type {import('next').NextConfig} */
2-
const nextConfig = {}
2+
const nextConfig = {
3+
compiler: {
4+
styledComponents: true
5+
}
6+
}
37

48
module.exports = nextConfig

package-lock.json

Lines changed: 2583 additions & 135 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"eslint-config-next": "13.5.2",
2222
"next": "13.5.2",
2323
"react": "18.2.0",
24-
"react-dom": "18.2.0"
24+
"react-dom": "18.2.0",
25+
"styled-components": "^6.0.8"
2526
},
2627
"devDependencies": {
2728
"@calblueprint/eslint-config-react": "^0.0.3",

src/api/supabase/queries/cases.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@ import supabase from '../createClient';
33

44
export async function getAllCases() {
55
const { data, error } = await supabase.from('cases').select();
6+
if (error) throw error;
7+
return data;
8+
}
69

7-
if (error) {
8-
throw new Error(`An error occurred trying to read cases: ${error}`);
9-
}
10-
10+
export async function getNCases(n: number) {
11+
const { data, error } = await supabase.from('cases').select().limit(n);
12+
if (error) throw error;
1113
return data;
1214
}
1315

1416
export async function getCaseById(id: UUID) {
1517
const { data, error } = await supabase.from('cases').select().eq('id', id);
16-
17-
if (error) {
18-
throw new Error(`An error occurred trying to read cases: ${error}`);
19-
}
20-
18+
if (error) throw error;
2119
return data;
2220
}

src/app/cases/page.tsx

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,52 @@
11
'use client';
22

33
import { useEffect, useState } from 'react';
4-
import { getAllCases } from '../../api/supabase/queries/cases';
4+
import { UUID } from 'crypto';
55
import { CaseListing } from '../../types/schemaTypes';
6+
import { getNCases } from '../../api/supabase/queries/cases';
7+
import ListingCard from '../../components/ListingCard/ListingCard';
8+
import {
9+
CardColumn,
10+
CaseDetailDisplay,
11+
CaseDetails,
12+
MainDisplay,
13+
PageContainer,
14+
} from './styles';
15+
import { H1, H2 } from '../../styles/text';
616

717
export default function Page() {
8-
const [data, setData] = useState<CaseListing[]>([]);
18+
const [caseData, setCaseData] = useState<CaseListing[]>([]);
19+
const [selectedCard, setSelectedCard] = useState<UUID>();
920

21+
// react hooks
1022
useEffect(() => {
11-
getAllCases().then(casesData => {
12-
setData(casesData);
23+
getNCases(20).then(casesData => {
24+
setCaseData(casesData);
1325
});
1426
}, []);
1527

28+
// page structure
1629
return (
17-
<div>
18-
<table>
19-
<thead>
20-
<tr>
21-
<th>id</th>
22-
<th>summary</th>
23-
<th>languages</th>
24-
<th>country</th>
25-
<th>legalServerId</th>
26-
<th>clientInitials</th>
27-
<th>timeToComplete</th>
28-
<th>isRemote</th>
29-
<th>clientLocation</th>
30-
<th>program</th>
31-
<th>upcomingHearingDate</th>
32-
<th>needsInterpreter</th>
33-
<th>interestIds</th>
34-
</tr>
35-
</thead>
36-
<tbody>
37-
{data.map(d => (
38-
<tr key={d.id}>
39-
<td>{d.id}</td>
40-
<td>{d.summary}</td>
41-
<td>{JSON.stringify(d.languages)}</td>
42-
<td>{d.country}</td>
43-
<td>{d.legal_server_id}</td>
44-
<td>{d.client_initials}</td>
45-
<td>{d.time_to_complete}</td>
46-
<td>{d.is_remote}</td>
47-
<td>{d.client_location}</td>
48-
<td>{d.program}</td>
49-
<td>{d.upcoming_hearing_date}</td>
50-
<td>{JSON.stringify(d.needs_interpreter)}</td>
51-
<td>{JSON.stringify(d.interest_ids)}</td>
52-
</tr>
30+
<PageContainer>
31+
<H1>Browse Available Cases</H1>
32+
<MainDisplay>
33+
<CardColumn>
34+
{caseData.map(c => (
35+
<ListingCard
36+
key={c.id}
37+
caseData={c}
38+
isSelected={c.id === selectedCard}
39+
onClick={() => setSelectedCard(c.id)}
40+
/>
5341
))}
54-
</tbody>
55-
</table>
56-
</div>
42+
</CardColumn>
43+
<CaseDetailDisplay>
44+
{/* proof of concept -- to turn into component later */}
45+
<CaseDetails>
46+
<H2>Case details.</H2>
47+
</CaseDetails>
48+
</CaseDetailDisplay>
49+
</MainDisplay>
50+
</PageContainer>
5751
);
5852
}

src/app/cases/styles.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import styled from 'styled-components';
2+
3+
// styled components
4+
export const PageContainer = styled.div`
5+
display: grid;
6+
place-items: center;
7+
padding: 2rem;
8+
`;
9+
10+
export const MainDisplay = styled.main`
11+
display: grid;
12+
grid-template-columns: 5fr 10fr;
13+
margin-top: 2rem;
14+
width: 100%;
15+
`;
16+
17+
// cards
18+
export const CardColumn = styled.div`
19+
display: flex;
20+
flex-direction: column;
21+
gap: 2rem;
22+
border-right: 1px solid black;
23+
padding-top: 0.5rem;
24+
padding-right: 2rem;
25+
`;
26+
27+
// case detail
28+
export const CaseDetailDisplay = styled.aside`
29+
position: relative;
30+
width: 100%;
31+
`;
32+
33+
export const CaseDetails = styled.div`
34+
position: sticky;
35+
top: 4rem;
36+
background: white;
37+
width: 90%;
38+
height: 80vh;
39+
border-radius: 20px;
40+
margin: 0 auto;
41+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
42+
padding: 2rem;
43+
border: 1px solid lightgray;
44+
`;

src/app/globals.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
* {
2+
margin: 0;
3+
padding: 0;
4+
box-sizing: border-box;
5+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'react';
2+
import { UUID } from 'crypto';
3+
import { CaseListing } from '../../types/schemaTypes';
4+
import timestampStringToDate from '../../utils/helpers';
5+
import { CardBody, TagRow, CardTag } from './styles';
6+
import { H2 } from '../../styles/text';
7+
import COLORS from '../../styles/colors';
8+
9+
export default function ListingCard({
10+
caseData,
11+
isSelected = false,
12+
onClick,
13+
}: {
14+
caseData: CaseListing;
15+
isSelected?: boolean;
16+
onClick?: (id: UUID) => void;
17+
}) {
18+
// setup
19+
const rolesNeeded = ['Attorney'].concat(
20+
caseData.needs_interpreter ? ['Interpreter'] : [],
21+
);
22+
23+
// helper functions
24+
const parseDate = (d: Date): string =>
25+
`${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
26+
27+
return (
28+
<CardBody
29+
$selected={isSelected}
30+
onClick={onClick ? () => onClick(caseData.id) : undefined}
31+
>
32+
{/* hard-coded for now */}
33+
<H2>Case title.</H2>
34+
<p>
35+
<strong>Languages: </strong>
36+
{caseData.languages.join(', ')}
37+
</p>
38+
<p>
39+
<strong>Case Deadline: </strong>
40+
{parseDate(timestampStringToDate(caseData.time_to_complete))}
41+
</p>
42+
<TagRow>
43+
{rolesNeeded.map(r => (
44+
<CardTag
45+
key={r}
46+
color={
47+
r === 'Interpreter'
48+
? COLORS.interpreterColor
49+
: COLORS.attorneyColor
50+
}
51+
>
52+
{r}
53+
</CardTag>
54+
))}
55+
</TagRow>
56+
</CardBody>
57+
);
58+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import styled from 'styled-components';
2+
3+
// the card itself
4+
export const CardBody = styled.div<{ $selected?: boolean }>`
5+
display: flex;
6+
flex-direction: column;
7+
border: 1px solid lightgray;
8+
padding: 1rem;
9+
border-radius: 10px;
10+
transition: 150ms;
11+
cursor: pointer;
12+
gap: 1rem;
13+
14+
${({ $selected }) => $selected && `border-color: #097A62`};
15+
16+
&:hover {
17+
box-shadow: 0 4px 4px 1px rgba(0, 0, 0, 0.1);
18+
transform: translateY(-2px);
19+
}
20+
`;
21+
22+
export const TagRow = styled.div`
23+
display: flex;
24+
gap: 1rem;
25+
`;
26+
27+
export const CardTag = styled.span<{ color: string }>`
28+
border-radius: 100px;
29+
font-size: 0.8rem;
30+
padding: 0.2rem 0.5rem;
31+
background: ${({ color }) => color};
32+
`;

src/styles/colors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const COLORS = {
2+
attorneyColor: '#85b4d2',
3+
interpreterColor: '#8dd89f',
4+
};
5+
6+
export default COLORS;

0 commit comments

Comments
 (0)