Skip to content

Commit 2e17b2d

Browse files
committed
feat: add usecorpse hook and skeleton rendering
1 parent e2d622d commit 2e17b2d

File tree

4 files changed

+290
-139
lines changed

4 files changed

+290
-139
lines changed

src/WebClient/app/components/Graveyard.tsx

Lines changed: 10 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import { useCallback, useEffect, useState, JSX } from 'react';
2-
import {
3-
Button,
4-
Card,
5-
CardHeader,
6-
Image,
7-
Text,
8-
makeStyles,
9-
tokens,
10-
} from '@fluentui/react-components';
11-
import { News16Regular } from '@fluentui/react-icons';
2+
import { makeStyles, tokens } from '@fluentui/react-components';
123
import corpsesDocument from '@microsoftgraveyard/data/corpses.json';
134
import GraveyardHeader from '@microsoftgraveyard/components/GraveyardHeader';
145
import GraveyardFooter from '@microsoftgraveyard/components/GraveyardFooter';
6+
import {
7+
Corpse,
8+
CorpseRecord,
9+
CorpsesDocument,
10+
} from '@microsoftgraveyard/types/corpse';
11+
import Headstone from '@microsoftgraveyard/components/Headstone';
1512

1613
const useStyles = makeStyles({
1714
main: {
@@ -52,94 +49,6 @@ const useStyles = makeStyles({
5249
},
5350
});
5451

55-
interface Corpse {
56-
name: string;
57-
qualifier: string | null;
58-
birthDate: Date | null;
59-
deathDate: Date;
60-
description: string;
61-
link: string;
62-
}
63-
64-
interface CorpseRecord {
65-
name: string;
66-
qualifier: string | null;
67-
birthDate: string | null;
68-
deathDate: string;
69-
description: string;
70-
link: string;
71-
}
72-
73-
interface CorpsesDocument {
74-
$schema: string;
75-
corpses: CorpseRecord[];
76-
}
77-
78-
const getAge = (start: Date, end: Date): { age: number; period: string } => {
79-
let years = end.getFullYear() - start.getFullYear();
80-
if (
81-
end.getMonth() < start.getMonth() ||
82-
(end.getMonth() === start.getMonth() && end.getDate() < start.getDate())
83-
) {
84-
years--;
85-
}
86-
87-
if (years >= 1) {
88-
return { age: years, period: years === 1 ? 'year' : 'years' };
89-
}
90-
91-
const months =
92-
end.getMonth() -
93-
start.getMonth() +
94-
12 * (end.getFullYear() - start.getFullYear());
95-
if (months >= 1) {
96-
return { age: months, period: months === 1 ? 'month' : 'months' };
97-
}
98-
99-
const days = end.getDate() - start.getDate();
100-
return { age: days, period: days === 1 ? 'day' : 'days' };
101-
};
102-
103-
const getExpectedDeathDate = (corpse: Corpse): string =>
104-
corpse.deathDate.toLocaleDateString(undefined, {
105-
month: 'long',
106-
year: 'numeric',
107-
});
108-
109-
const getFullName = (corpse: Corpse): string =>
110-
corpse.qualifier ? `${corpse.name} (${corpse.qualifier})` : corpse.name;
111-
112-
const getLifeDates = (corpse: Corpse): string =>
113-
corpse.birthDate
114-
? `${corpse.birthDate.getFullYear()} - ${corpse.deathDate.getFullYear()}`
115-
: `${corpse.deathDate.getFullYear()}`;
116-
117-
const getObituary = (corpse: Corpse, today: Date): string => {
118-
let obituary = '';
119-
120-
const dead = isDead(corpse, today);
121-
if (dead) {
122-
const { age, period } = getAge(corpse.deathDate, today);
123-
const message = age === 0 ? 'today' : `${age} ${period} ago`;
124-
obituary += `Killed by Microsoft ${message}, `;
125-
} else {
126-
const { age, period } = getAge(today, corpse.deathDate);
127-
obituary += `To be killed by Microsoft in ${age} ${period}, `;
128-
}
129-
130-
obituary += `${corpse.name} ${dead ? 'was' : 'is'} ${corpse.description}.`;
131-
132-
if (dead && corpse.birthDate) {
133-
const { age, period } = getAge(corpse.birthDate, corpse.deathDate);
134-
obituary += ` It was ${age} ${period} old.`;
135-
}
136-
137-
return obituary;
138-
};
139-
140-
const isDead = (corpse: Corpse, today: Date): boolean =>
141-
corpse.deathDate <= today;
142-
14352
const Graveyard = () => {
14453
const [corpses, setCorpses] = useState<Corpse[]>([]);
14554
const today: Date = new Date();
@@ -168,48 +77,10 @@ const Graveyard = () => {
16877
);
16978
}, []);
17079

171-
const renderGraves = (): JSX.Element[] => {
80+
const renderHeadstones = (): JSX.Element[] => {
17281
return corpses.map((corpse, index) => (
17382
<li className={styles.container} key={index}>
174-
<Card appearance="filled-alternative" key={index}>
175-
<CardHeader
176-
image={
177-
<Image
178-
src={
179-
isDead(corpse, today)
180-
? '/images/headstone.svg'
181-
: '/images/coffin.svg'
182-
}
183-
alt="a headstone for that which is dead"
184-
height={72}
185-
width={72}
186-
/>
187-
}
188-
header={
189-
<Text as="h2" weight="bold" block className={styles.title}>
190-
{getFullName(corpse)}
191-
</Text>
192-
}
193-
description={
194-
<Text as="p" className={styles.lifeDates}>
195-
{isDead(corpse, today)
196-
? getLifeDates(corpse)
197-
: getExpectedDeathDate(corpse)}
198-
</Text>
199-
}
200-
action={
201-
<Button
202-
as="a"
203-
icon={<News16Regular />}
204-
appearance="subtle"
205-
href={corpse.link}
206-
target="_blank"
207-
rel="noreferrer noopener"
208-
/>
209-
}
210-
/>
211-
<Text as="p">{getObituary(corpse, today)}</Text>
212-
</Card>
83+
<Headstone corpse={corpse} today={today} />
21384
</li>
21485
));
21586
};
@@ -222,7 +93,7 @@ const Graveyard = () => {
22293
<main className={styles.main}>
22394
<GraveyardHeader />
22495
<section id="graveyard">
225-
<ul className={styles.list}>{renderGraves()}</ul>
96+
<ul className={styles.list}>{renderHeadstones()}</ul>
22697
</section>
22798
<GraveyardFooter />
22899
</main>
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import {
2+
Card,
3+
CardHeader,
4+
Button,
5+
Image,
6+
tokens,
7+
makeStyles,
8+
Skeleton,
9+
SkeletonItem,
10+
Body1,
11+
Subtitle1,
12+
} from '@fluentui/react-components';
13+
import { News16Regular } from '@fluentui/react-icons';
14+
import useCorpse from '@microsoftgraveyard/hooks/useCorpse';
15+
import { Corpse } from '@microsoftgraveyard/types/corpse';
16+
import { FC } from 'react';
17+
18+
const useStyles = makeStyles({
19+
container: {
20+
width: '100%',
21+
},
22+
skeletonImage: {
23+
height: '72px',
24+
width: '72px',
25+
},
26+
skeletonAction: {
27+
height: '16px',
28+
width: '16px',
29+
},
30+
skeletonFirstRow: {
31+
alignItems: 'center',
32+
display: 'grid',
33+
paddingTop: '10px',
34+
paddingBottom: '10px',
35+
position: 'relative',
36+
gap: '10px',
37+
gridTemplateColumns: '20% 15% 30% 25% min-content',
38+
},
39+
skeletonSecondRow: {
40+
alignItems: 'center',
41+
display: 'grid',
42+
paddingBottom: '10px',
43+
position: 'relative',
44+
gap: '10px',
45+
gridTemplateColumns: '15% 30% 20% min-content 20%',
46+
},
47+
skeletonThirdRow: {
48+
alignItems: 'center',
49+
display: 'grid',
50+
paddingBottom: '10px',
51+
position: 'relative',
52+
gap: '10px',
53+
gridTemplateColumns: '10% 15% 20% 35% min-content',
54+
},
55+
title: {
56+
margin: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalNone}`,
57+
},
58+
lifeDates: {
59+
color: tokens.colorBrandForeground2,
60+
lineHeight: tokens.lineHeightBase200,
61+
margin: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalNone}`,
62+
},
63+
});
64+
65+
interface HeadstoneProps {
66+
corpse: Corpse;
67+
today: Date;
68+
}
69+
70+
const Headstone: FC<HeadstoneProps> = ({ corpse, today }) => {
71+
const styles = useStyles();
72+
const { name, lifeDates, obituary, isDead, loading } = useCorpse(
73+
corpse,
74+
today
75+
);
76+
77+
return (
78+
<Card appearance="filled-alternative" className={styles.container}>
79+
{loading ? (
80+
<Skeleton aria-label="Loading headstone">
81+
<CardHeader
82+
image={
83+
<SkeletonItem shape="circle" className={styles.skeletonImage} />
84+
}
85+
header={<SkeletonItem size={20} className={styles.title} />}
86+
description={
87+
<SkeletonItem size={16} className={styles.lifeDates} />
88+
}
89+
action={
90+
<SkeletonItem
91+
shape="square"
92+
size={24}
93+
className={styles.skeletonAction}
94+
/>
95+
}
96+
/>
97+
<div className={styles.skeletonFirstRow}>
98+
<SkeletonItem size={12} />
99+
<SkeletonItem size={12} />
100+
<SkeletonItem size={12} />
101+
<SkeletonItem size={12} />
102+
<SkeletonItem size={12} />
103+
</div>
104+
<div className={styles.skeletonSecondRow}>
105+
<SkeletonItem size={12} />
106+
<SkeletonItem size={12} />
107+
<SkeletonItem size={12} />
108+
<SkeletonItem size={12} />
109+
<SkeletonItem size={12} />
110+
</div>
111+
<div className={styles.skeletonThirdRow}>
112+
<SkeletonItem size={12} />
113+
<SkeletonItem size={12} />
114+
<SkeletonItem size={12} />
115+
<SkeletonItem size={12} />
116+
<SkeletonItem size={12} />
117+
</div>
118+
</Skeleton>
119+
) : (
120+
<>
121+
<CardHeader
122+
image={
123+
<Image
124+
src={isDead ? '/images/headstone.svg' : '/images/coffin.svg'}
125+
alt="a headstone for that which is dead"
126+
height={72}
127+
width={72}
128+
/>
129+
}
130+
header={
131+
<Subtitle1 as="h2" block className={styles.title}>
132+
{name}
133+
</Subtitle1>
134+
}
135+
description={
136+
<Body1 as="p" className={styles.lifeDates}>
137+
{lifeDates}
138+
</Body1>
139+
}
140+
action={
141+
<Button
142+
as="a"
143+
icon={<News16Regular />}
144+
appearance="subtle"
145+
href={corpse.link}
146+
target="_blank"
147+
rel="noreferrer noopener"
148+
/>
149+
}
150+
/>
151+
<Body1 as="p">{obituary}</Body1>
152+
</>
153+
)}
154+
</Card>
155+
);
156+
};
157+
158+
export default Headstone;

0 commit comments

Comments
 (0)