Skip to content

Commit 760d78e

Browse files
luojiyin1987TechQueryCopilot
authored
[refactor] redesign Hackathon Team page (#67)
Signed-off-by: luojiyin <luojiyin@hotmail.com> Co-authored-by: TechQuery <shiy2008@gmail.com> Co-authored-by: Copilot <copilot@github.com>
1 parent d935d7a commit 760d78e

14 files changed

Lines changed: 1137 additions & 487 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
@import './theme.less';
2+
3+
.card {
4+
// prettier-ignore
5+
.panel-card();
6+
padding: 1.35rem;
7+
}
8+
9+
.avatar {
10+
flex-shrink: 0;
11+
box-shadow: 0 0 24px rgba(44, 232, 255, 0.14);
12+
border: 1px solid rgba(44, 232, 255, 0.26);
13+
width: 4rem;
14+
height: 4rem;
15+
}
16+
17+
.name {
18+
margin: 0;
19+
color: #fff;
20+
font-weight: 700;
21+
font-size: 1.05rem;
22+
}
23+
24+
.link {
25+
color: @cyan;
26+
text-decoration: none;
27+
28+
&:hover {
29+
color: #fff;
30+
}
31+
}
32+
33+
.summary {
34+
margin: 1rem 0 0;
35+
color: @muted;
36+
line-height: 1.75;
37+
}
38+
39+
.skill {
40+
// prettier-ignore
41+
.chip();
42+
padding: 0.45rem 0.72rem;
43+
color: rgba(255, 255, 255, 0.8);
44+
font-size: 0.82rem;
45+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Avatar } from 'idea-react';
2+
import { TableCellUser } from 'mobx-lark';
3+
import { FC } from 'react';
4+
import { Card } from 'react-bootstrap';
5+
6+
import { Member } from '../../../models/Hackathon';
7+
import styles from './TeamMember.module.less';
8+
9+
export const TeamMemberCard: FC<Member> = ({ person, githubAccount, summary, skills }) => {
10+
const member = person as TableCellUser;
11+
const githubName = githubAccount as string;
12+
const memberSummary = summary as string;
13+
const memberSkills = (skills as string[]).slice(0, 6);
14+
15+
return (
16+
<Card className={`${styles.card} h-100`} body>
17+
<div className="d-flex align-items-center gap-3">
18+
<Avatar className={styles.avatar} src={member?.avatar_url} />
19+
<div>
20+
<h3 className={styles.name}>{member?.name || '-'}</h3>
21+
22+
{githubName && (
23+
<a
24+
className={styles.link}
25+
href={`https://github.com/${githubName}`}
26+
target="_blank"
27+
rel="noreferrer"
28+
>
29+
@{githubName}
30+
</a>
31+
)}
32+
</div>
33+
</div>
34+
35+
{memberSummary && <p className={styles.summary}>{memberSummary}</p>}
36+
37+
{memberSkills[0] && (
38+
<ul className="d-flex flex-wrap gap-2 mt-3 mb-0 p-0 list-unstyled">
39+
{memberSkills.map(skill => (
40+
<li key={skill} className={styles.skill}>
41+
{skill}
42+
</li>
43+
))}
44+
</ul>
45+
)}
46+
</Card>
47+
);
48+
};

components/Activity/Hackathon/constant.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { TableCellUser } from 'mobx-lark';
2-
32
import { Activity, ActivityModel } from '../../../models/Activity';
43
import { Agenda, Organization, Person, Prize, Project, Template } from '../../../models/Hackathon';
54
import { i18n } from '../../../models/Translation';
@@ -336,7 +335,6 @@ export const buildProjectItems = (
336335
{ projects, activity }: { projects: Project[]; activity: Activity },
337336
) =>
338337
projects.map(({ id, name, score, summary, createdBy, members }) => {
339-
const creator = createdBy as TableCellUser | undefined;
340338
const scoreText = score === null || score === undefined || score === '' ? '—' : `${score}`;
341339

342340
return {
@@ -346,11 +344,10 @@ export const buildProjectItems = (
346344
score: scoreText,
347345
description: (summary as string) || '',
348346
meta: [
349-
creator
347+
createdBy
350348
? {
351349
label: t('created_by'),
352-
value: creator.name || '—',
353-
valueHref: creator.email ? `mailto:${creator.email}` : undefined,
350+
value: (createdBy as TableCellUser)?.name || '—',
354351
}
355352
: { label: t('created_by'), value: '—' },
356353
{
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { TableCellValue } from 'mobx-lark';
2+
import { useCallback, useEffect, useMemo, useState } from 'react';
3+
4+
import { CountdownWindow, firstTextOf, resolveCountdownState, timeOf } from './utility';
5+
6+
export const useLiveCountdownState = <T extends CountdownWindow>(
7+
items: T[],
8+
startTime?: TableCellValue,
9+
endTime?: TableCellValue,
10+
) => {
11+
const [referenceTime, setReferenceTime] = useState<number | null>(null);
12+
const refreshReferenceTime = useCallback(() => setReferenceTime(Date.now()), []);
13+
14+
useEffect(() => refreshReferenceTime(), [refreshReferenceTime]);
15+
16+
const countdownState = useMemo(
17+
() =>
18+
referenceTime === null
19+
? {
20+
nextItem: undefined as T | undefined,
21+
countdownTo: firstTextOf(startTime) || firstTextOf(endTime) || undefined,
22+
}
23+
: resolveCountdownState(items, referenceTime, startTime, endTime),
24+
[endTime, items, referenceTime, startTime],
25+
);
26+
27+
useEffect(() => {
28+
if (referenceTime === null) return;
29+
30+
const targetTime = timeOf(countdownState.countdownTo);
31+
32+
if (!Number.isFinite(targetTime)) return;
33+
34+
const delay = Math.min(2_147_483_647, Math.max(1000, targetTime - Date.now() + 1000));
35+
const timer = window.setTimeout(
36+
refreshReferenceTime,
37+
delay,
38+
);
39+
40+
return () => window.clearTimeout(timer);
41+
}, [countdownState.countdownTo, referenceTime, refreshReferenceTime]);
42+
43+
return countdownState;
44+
};

components/Activity/Hackathon/utility.ts

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TableCellValue, TableFormView } from 'mobx-lark';
2-
import { formatDate } from 'web-utility';
2+
import { Day, formatDate } from 'web-utility';
33

44
import type { HackathonScheduleTone } from './Schedule';
55
import { i18n, I18nKey } from '../../../models/Translation';
@@ -33,14 +33,124 @@ export const buildAgendaTypeLabelMap = ({
3333
export const isPublicForm = ({ shared_limit }: TableFormView) =>
3434
['anyone_editable'].includes(shared_limit as string);
3535

36+
type NamedLike = { name?: string | null };
37+
type TextLike = TableCellValue | NamedLike | null | undefined;
38+
type TextListLike = TextLike | TextLike[];
39+
40+
const textOf = (value: TextLike) => {
41+
if (value === null || value === undefined) return '';
42+
if (typeof value === 'boolean') return '';
43+
44+
if (typeof value === 'object' && !Array.isArray(value)) {
45+
const {
46+
name,
47+
text,
48+
value: primitiveValue,
49+
displayName,
50+
display_name,
51+
title,
52+
content,
53+
plainText,
54+
plain_text,
55+
user,
56+
} = value as NamedLike & {
57+
text?: string | null;
58+
value?: string | number | null;
59+
displayName?: string | null;
60+
display_name?: string | null;
61+
title?: string | null;
62+
content?: string | null;
63+
plainText?: string | null;
64+
plain_text?: string | null;
65+
user?: {
66+
name?: string | null;
67+
displayName?: string | null;
68+
display_name?: string | null;
69+
} | null;
70+
};
71+
const candidate = [
72+
name,
73+
text,
74+
primitiveValue,
75+
displayName,
76+
display_name,
77+
title,
78+
content,
79+
plainText,
80+
plain_text,
81+
user?.displayName,
82+
user?.display_name,
83+
user?.name,
84+
].find(item => item !== null && item !== undefined && `${item}`.trim());
85+
86+
return candidate === null || candidate === undefined ? '' : `${candidate}`.trim();
87+
}
88+
89+
const text = value.toString().trim();
90+
91+
return text === '[object Object]' ? '' : text;
92+
};
93+
94+
export const firstTextOf = (value: TextListLike) =>
95+
(Array.isArray(value) ? value.map(textOf).find(Boolean) : textOf(value)) || '';
96+
3697
export const formatMoment = (value?: TableCellValue) => (value ? formatDate(value as string) : '');
3798

3899
export const formatPeriod = (startedAt?: TableCellValue, endedAt?: TableCellValue) =>
39100
[formatMoment(startedAt), formatMoment(endedAt)].filter(Boolean).join(' - ');
40101

102+
export const timeOf = (value?: TableCellValue) => {
103+
if (value instanceof Date) return value.getTime();
104+
105+
if (typeof value === 'number') return Number.isFinite(value) ? value : NaN;
106+
107+
const text = firstTextOf(value as TextListLike);
108+
109+
if (!text) return NaN;
110+
111+
const time = Date.parse(text);
112+
113+
return Number.isFinite(time) ? time : NaN;
114+
};
115+
116+
export interface CountdownWindow {
117+
startedAt?: TableCellValue;
118+
endedAt?: TableCellValue;
119+
}
120+
121+
const countdownTextOf = (value?: TableCellValue) => {
122+
const time = timeOf(value);
123+
124+
return Number.isFinite(time) ? new Date(time).toISOString() : undefined;
125+
};
126+
127+
export const resolveCountdownState = <T extends CountdownWindow>(
128+
items: T[],
129+
referenceTime: number,
130+
startTime?: TableCellValue,
131+
endTime?: TableCellValue,
132+
) => {
133+
const nextItem = items.find(({ startedAt, endedAt }) => {
134+
const started = timeOf(startedAt);
135+
const ended = timeOf(endedAt);
136+
137+
return Number.isFinite(started) && Number.isFinite(ended) && referenceTime <= ended;
138+
});
139+
const nextStartedAt = timeOf(nextItem?.startedAt);
140+
const nextCountdownTarget =
141+
Number.isFinite(nextStartedAt) && nextStartedAt > referenceTime
142+
? nextItem?.startedAt
143+
: nextItem?.endedAt;
144+
const fallbackCountdownTarget = timeOf(startTime) > referenceTime ? startTime : endTime;
145+
const countdownTo =
146+
countdownTextOf(nextCountdownTarget) || countdownTextOf(fallbackCountdownTarget);
147+
148+
return { nextItem, countdownTo };
149+
};
150+
41151
export const previewText = (items: TableCellValue[], fallback: string) =>
42152
items
43-
.map(item => item?.toString())
153+
.map(item => textOf(item))
44154
.filter(Boolean)
45155
.slice(0, 2)
46156
.join(' · ') || fallback;
@@ -75,10 +185,10 @@ export const compactSummaryOf = (
75185
) => {
76186
const source = Array.isArray(text)
77187
? text
78-
.map(item => item?.toString())
188+
.map(item => textOf(item))
79189
.filter(Boolean)
80190
.join(' · ')
81-
: text?.toString() || '';
191+
: textOf(text);
82192
const normalized = source.replace(/\s+/g, ' ').trim();
83193

84194
if (!normalized) return fallback;
@@ -95,12 +205,12 @@ export const dateKeyOf = (value?: TableCellValue) => {
95205
export const compactDateKeyOf = (value?: TableCellValue) => dateKeyOf(value).replace('-', '.');
96206

97207
export const daysBetween = (startedAt?: TableCellValue, endedAt?: TableCellValue) => {
98-
const start = new Date((startedAt as string) || '').getTime();
99-
const end = new Date((endedAt as string) || '').getTime();
208+
const start = timeOf(startedAt);
209+
const end = timeOf(endedAt);
100210

101211
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return 0;
102212

103-
return Math.max(1, Math.ceil((end - start) / (24 * 60 * 60 * 1000)));
213+
return Math.max(1, Math.ceil((end - start) / Day));
104214
};
105215

106216
export const normalizeAgendaType = (value?: TableCellValue) =>

components/Activity/ProductCard.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ export const ProductCard: FC<ProductCardProps> = observer(
6868
</div>
6969
)}
7070

71-
<time className="text-dark opacity-75 small" dateTime={new Date(createdAt as number).toJSON()}>
71+
<time
72+
suppressHydrationWarning
73+
className="text-dark opacity-75 small"
74+
dateTime={new Date(createdAt as number).toJSON()}
75+
>
7276
📅 {formatDate(createdAt as number)}
7377
</time>
7478
</Card.Body>

0 commit comments

Comments
 (0)