Skip to content

Commit 6c5d0fa

Browse files
committed
Address hackathon page review feedback
1 parent fec9d0a commit 6c5d0fa

5 files changed

Lines changed: 165 additions & 124 deletions

File tree

pages/hackathon/[id].tsx

Lines changed: 89 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BiTableSchema, TableCellLocation, TableCellUser } from 'mobx-lark';
1+
import { BiTableSchema, TableCellLocation, TableCellUser, TableFormView } from 'mobx-lark';
22
import { observer } from 'mobx-react';
33
import Link from 'next/link';
44
import { cache, compose, errorLogger } from 'next-ssr-middleware';
@@ -67,18 +67,15 @@ interface HackathonDetailProps {
6767
};
6868
}
6969

70-
type FormGroupKey = 'Person' | 'Project' | 'Product' | 'Evaluation';
70+
const FormButtonBar = ['Person', 'Project', 'Product', 'Evaluation'] as const;
7171

72-
interface FormLink {
73-
name: string;
74-
shared_limit?: string;
75-
shared_url: string;
76-
}
72+
type FormGroupKey = (typeof FormButtonBar)[number];
73+
type FormLink = TableFormView;
7774

7875
interface FormGroupMeta {
79-
description: string;
80-
eyebrow: string;
81-
title: string;
76+
description: I18nKey;
77+
eyebrow: I18nKey;
78+
title: I18nKey;
8279
}
8380

8481
interface FormGroup {
@@ -87,32 +84,31 @@ interface FormGroup {
8784
meta: FormGroupMeta;
8885
}
8986

90-
const FormButtonBar: FormGroupKey[] = ['Person', 'Project', 'Product', 'Evaluation'];
91-
9287
const FormSectionMeta: Record<FormGroupKey, FormGroupMeta> = {
9388
Person: {
94-
eyebrow: 'Participants',
95-
title: '参与者登记',
96-
description: '收集报名成员、建立参赛者池,并为后续组队和通知打底。',
89+
eyebrow: 'participants',
90+
title: 'hackathon_participant_registration',
91+
description: 'hackathon_participant_registration_description',
9792
},
9893
Project: {
99-
eyebrow: 'Team Lead',
100-
title: '项目注册',
101-
description: '由队长登记项目名称、成员、赛道和一句话介绍,完成队伍锁定。',
94+
eyebrow: 'hackathon_team_lead',
95+
title: 'hackathon_project_registration',
96+
description: 'hackathon_project_registration_description',
10297
},
10398
Product: {
104-
eyebrow: 'Submission',
105-
title: '作品提交',
106-
description: '比赛截止前统一提交最终作品、演示链接和补充说明。',
99+
eyebrow: 'hackathon_submission',
100+
title: 'product_submission',
101+
description: 'hackathon_product_submission_description',
107102
},
108103
Evaluation: {
109-
eyebrow: 'Review',
110-
title: '评审入口',
111-
description: '评委或导师在评审阶段使用,用于评分、复核与结果整理。',
104+
eyebrow: 'hackathon_review',
105+
title: 'hackathon_evaluation_entry',
106+
description: 'hackathon_evaluation_entry_description',
112107
},
113108
};
114109

115-
const isPublicForm = ({ shared_limit }: FormLink) => shared_limit === 'anyone_editable';
110+
const isPublicForm = ({ shared, shared_limit }: FormLink) =>
111+
shared || ['tenant_editable', 'anyone_editable'].includes(shared_limit as string);
116112

117113
const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackathon }) => {
118114
const { t } = useContext(I18nContext);
@@ -150,7 +146,7 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
150146
{ label: t('templates'), value: templates.length },
151147
{ label: t('organizations'), value: organizations.length },
152148
];
153-
const hostTags = ((host as string[] | undefined) || []).slice(0, 3);
149+
const hostTags = (host as string[] | undefined)?.slice(0, 3) || [];
154150
const agendaPreview = agenda.slice(0, 3);
155151

156152
return (
@@ -161,30 +157,30 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
161157
<Container>
162158
<Row className="align-items-center g-4">
163159
<Col lg={7}>
164-
<div className={styles.heroEyebrow}>
165-
<span className={styles.heroTag}>{(activityType as string) || t('hackathon')}</span>
160+
<ul className="list-unstyled d-flex flex-wrap gap-3 mb-3">
161+
<li className={styles.heroTag}>{(activityType as string) || t('hackathon')}</li>
166162
{hostTags.map(tag => (
167-
<span key={tag} className={styles.heroTag}>
163+
<li key={tag} className={styles.heroTag}>
168164
{tag}
169-
</span>
165+
</li>
170166
))}
171-
</div>
167+
</ul>
172168

173169
<h1 className={styles.title}>{name as string}</h1>
174170
<p className={styles.description}>{summary as string}</p>
175171

176-
<div className={styles.heroStats}>
172+
<ul className="list-unstyled d-flex flex-wrap gap-3 mt-4 mb-0">
177173
{heroStats.map(({ label, value }) => (
178-
<span key={label} className={styles.statChip}>
174+
<li key={label} className={styles.statChip}>
179175
{value} {label}
180-
</span>
176+
</li>
181177
))}
182-
</div>
178+
</ul>
183179

184-
<div className={styles.heroActions}>
180+
<nav className="d-flex flex-wrap gap-3 mt-4">
185181
{primaryForm && (
186182
<Button href={primaryForm.list[0].shared_url} target="_blank" rel="noreferrer">
187-
{primaryForm.meta.title}
183+
{t(primaryForm.meta.title)}
188184
</Button>
189185
)}
190186
{secondaryForm && (
@@ -194,35 +190,32 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
194190
rel="noreferrer"
195191
variant="light"
196192
>
197-
{secondaryForm.meta.title}
193+
{t(secondaryForm.meta.title)}
198194
</Button>
199195
)}
200196
{formGroups[0] && (
201197
<Button href="#entry-hub" variant="outline-light">
202-
查看全部入口
198+
{t('hackathon_view_all_entries')}
203199
</Button>
204200
)}
205-
</div>
206-
207-
<Row className="mt-4 g-3">
208-
<Col md={6}>
209-
<Card className={styles.infoCard}>
210-
<Card.Body>
211-
<h5 className="text-white mb-2">📍 {t('event_location')}</h5>
212-
<p className="text-white-50 mb-0">
213-
{(location as TableCellLocation)?.full_address}
214-
</p>
215-
</Card.Body>
201+
</nav>
202+
203+
<Row className="mt-4 g-3" md={2}>
204+
<Col>
205+
<Card className={styles.infoCard} body>
206+
<h5 className="text-white mb-2">📍 {t('event_location')}</h5>
207+
<address className="text-white-50 mb-0 fst-normal">
208+
{(location as TableCellLocation)?.full_address}
209+
</address>
216210
</Card>
217211
</Col>
218-
<Col md={6}>
219-
<Card className={styles.infoCard}>
220-
<Card.Body>
221-
<h5 className="text-white mb-2">{t('event_duration')}</h5>
222-
<p className="text-white-50 mb-0">
223-
{formatDate(startTime as string)} - {formatDate(endTime as string)}
224-
</p>
225-
</Card.Body>
212+
<Col>
213+
<Card className={styles.infoCard} body>
214+
<h5 className="text-white mb-2">{t('event_duration')}</h5>
215+
<p className="text-white-50 mb-0">
216+
<time dateTime={startTime as string}>{formatDate(startTime as string)}</time>{' '}
217+
- <time dateTime={endTime as string}>{formatDate(endTime as string)}</time>
218+
</p>
226219
</Card>
227220
</Col>
228221
</Row>
@@ -236,24 +229,32 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
236229
</div>
237230
)}
238231
<Card.Body>
239-
<div className={styles.heroVisualHead}>
240-
<span className={styles.heroVisualKicker}>Agenda Preview</span>
241-
<strong>{agendaPreview[0]?.name as string}</strong>
242-
</div>
243-
244-
<div className={styles.heroVisualList}>
245-
{agendaPreview.map(({ name, startedAt, endedAt }) => (
246-
<div
247-
key={`${name as string}-${startedAt as string}`}
248-
className={styles.heroVisualItem}
249-
>
250-
<strong>{name as string}</strong>
251-
<span>
252-
{formatDate(startedAt as string)} - {formatDate(endedAt as string)}
253-
</span>
232+
{agendaPreview[0] && (
233+
<dl className="d-grid gap-3 mb-0">
234+
<div className={`${styles.heroVisualHead} ${styles.heroVisualItem}`}>
235+
<dt className={styles.heroVisualKicker}>{t('hackathon_agenda_preview')}</dt>
236+
<dd className="fw-semibold">{agendaPreview[0].name as string}</dd>
254237
</div>
255-
))}
256-
</div>
238+
239+
{agendaPreview.map(({ name, startedAt, endedAt }) => (
240+
<div
241+
key={`${name as string}-${startedAt as string}`}
242+
className={styles.heroVisualItem}
243+
>
244+
<dt className="fw-semibold">{name as string}</dt>
245+
<dd>
246+
<time dateTime={startedAt as string}>
247+
{formatDate(startedAt as string)}
248+
</time>{' '}
249+
-{' '}
250+
<time dateTime={endedAt as string}>
251+
{formatDate(endedAt as string)}
252+
</time>
253+
</dd>
254+
</div>
255+
))}
256+
</dl>
257+
)}
257258
</Card.Body>
258259
</Card>
259260
</Col>
@@ -264,22 +265,23 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
264265
<Container className="my-5">
265266
{formGroups[0] && (
266267
<section id="entry-hub" className={styles.section}>
267-
<p className={styles.sectionEyebrow}>Action Hub · Forms</p>
268-
<h2 className={styles.sectionTitle}>报名与提交流程</h2>
269-
<p className={styles.sectionLead}>
270-
入口根据活动当前配置自动生成。不同活动可以复用同一套页面结构,而不是为每次活动单开专页。
271-
</p>
268+
<hgroup className="mb-0">
269+
<p className={styles.sectionEyebrow}>{t('hackathon_action_hub')}</p>
270+
<h2 className={styles.sectionTitle}>{t('hackathon_entry_flow')}</h2>
271+
<p className={styles.sectionLead}>{t('hackathon_entry_flow_description')}</p>
272+
</hgroup>
272273

273-
<Row className="mt-4 g-3" md={2} xl={4}>
274+
<Row as="ol" className="list-unstyled mt-4 g-3" md={2} xl={4}>
274275
{formGroups.map(({ key, list, meta }, index) => (
275-
<Col key={key}>
276+
<Col as="li" key={key}>
276277
<Card className={styles.entryCard} body>
277278
<span className={styles.entryStep}>
278-
Step {String(index + 1).padStart(2, '0')} · {meta.eyebrow}
279+
{t('hackathon_step')} {String(index + 1).padStart(2, '0')} ·{' '}
280+
{t(meta.eyebrow)}
279281
</span>
280-
<h3 className="h5 text-white mt-2">{meta.title}</h3>
281-
<p className="text-white-50 mb-3">{meta.description}</p>
282-
<div className="d-grid gap-2">
282+
<h3 className="h5 text-white mt-2">{t(meta.title)}</h3>
283+
<p className="text-white-50 mb-3">{t(meta.description)}</p>
284+
<nav className="d-grid gap-2">
283285
{list.map(({ name, shared_url }) => (
284286
<Button
285287
key={name}
@@ -291,7 +293,7 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
291293
{name}
292294
</Button>
293295
))}
294-
</div>
296+
</nav>
295297
</Card>
296298
</Col>
297299
))}

styles/Hackathon.module.less

Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,6 @@
4444
font-size: 1.2rem;
4545
}
4646

47-
.heroEyebrow {
48-
display: flex;
49-
flex-wrap: wrap;
50-
gap: 0.75rem;
51-
margin-bottom: 1rem;
52-
}
53-
5447
.heroTag {
5548
display: inline-flex;
5649
align-items: center;
@@ -64,13 +57,6 @@
6457
font-size: 0.9rem;
6558
}
6659

67-
.heroStats {
68-
display: flex;
69-
flex-wrap: wrap;
70-
gap: 0.75rem;
71-
margin-top: 1.5rem;
72-
}
73-
7460
.statChip {
7561
display: inline-flex;
7662
align-items: center;
@@ -82,13 +68,6 @@
8268
font-size: 0.9rem;
8369
}
8470

85-
.heroActions {
86-
display: flex;
87-
flex-wrap: wrap;
88-
gap: 0.75rem;
89-
margin-top: 1.75rem;
90-
}
91-
9271
.heroVisualCard {
9372
overflow: hidden;
9473
box-shadow: 0 18px 50px rgba(10, 12, 30, 0.35);
@@ -109,27 +88,13 @@
10988
object-fit: cover;
11089
}
11190

112-
.heroVisualHead {
113-
display: flex;
114-
flex-wrap: wrap;
115-
justify-content: space-between;
116-
gap: 0.75rem;
117-
align-items: center;
118-
}
119-
12091
.heroVisualKicker {
12192
letter-spacing: 0.08em;
12293
text-transform: uppercase;
12394
opacity: 0.72;
12495
font-size: 0.8rem;
12596
}
12697

127-
.heroVisualList {
128-
display: grid;
129-
gap: 0.75rem;
130-
margin-top: 1rem;
131-
}
132-
13398
.heroVisualItem {
13499
display: flex;
135100
flex-direction: column;
@@ -139,20 +104,31 @@
139104
background: rgba(255, 255, 255, 0.06);
140105
padding: 0.85rem 1rem;
141106

142-
span {
107+
dt {
108+
margin: 0;
109+
}
110+
111+
dd {
143112
opacity: 0.74;
113+
margin: 0;
144114
font-size: 0.88rem;
145115
}
146116
}
147117

118+
.heroVisualHead {
119+
dd {
120+
opacity: 1;
121+
font-size: 1rem;
122+
}
123+
}
124+
148125
.infoCard {
149126
backdrop-filter: blur(10px);
150127
transition: all 0.3s ease;
151128
margin-bottom: 1rem;
152129
border: 2px solid rgba(255, 255, 255, 0.3);
153130
border-radius: 16px;
154131
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
155-
padding: 1.5rem;
156132

157133
&:hover {
158134
transform: translateY(-5px);

0 commit comments

Comments
 (0)