Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
af78882
chore: make env sync explicit
luojiyin1987 Apr 21, 2026
ebf6972
docs: document explicit env sync
luojiyin1987 Apr 21, 2026
7d82b01
fix: stabilize hackathon detail hydration
luojiyin1987 Apr 21, 2026
14d64e9
fix: suppress product card hydration mismatch
luojiyin1987 Apr 21, 2026
f194794
i18n: add hackathon team showcase label in en-US
luojiyin1987 Apr 21, 2026
f30369f
i18n: add hackathon team showcase label in zh-CN
luojiyin1987 Apr 21, 2026
d00122b
i18n: add hackathon team showcase label in zh-TW
luojiyin1987 Apr 21, 2026
cab62c1
feat: add hackathon team page styles
luojiyin1987 Apr 21, 2026
6e4f39e
feat: redesign hackathon team detail page
luojiyin1987 Apr 21, 2026
8c12e16
refactor: extract hackathon countdown resolver
luojiyin1987 Apr 21, 2026
8ed3e06
refactor: use shared hackathon user helper
luojiyin1987 Apr 21, 2026
1c7d4d8
refactor: simplify hackathon detail countdown
luojiyin1987 Apr 21, 2026
e00e2eb
refactor: simplify hackathon team countdown
luojiyin1987 Apr 21, 2026
e565a66
refactor: clarify hackathon countdown reference time
luojiyin1987 Apr 21, 2026
67c97d9
refactor: rename hero countdown client clock
luojiyin1987 Apr 21, 2026
e31bfea
refactor: rename hackathon agenda reference time
luojiyin1987 Apr 21, 2026
1b47f1a
refactor: rename team agenda reference time
luojiyin1987 Apr 21, 2026
ba6607f
fix: align hackathon hero fallback action
luojiyin1987 Apr 21, 2026
1e5b881
fix: align team hero fallback action
luojiyin1987 Apr 21, 2026
dc8ee07
fix: avoid epoch fallback for missing hackathon times
luojiyin1987 Apr 22, 2026
02fd7aa
fix: normalize hackathon time parsing
luojiyin1987 Apr 22, 2026
01f3472
feat: add live hackathon countdown state hook
luojiyin1987 Apr 22, 2026
aa5f4a6
fix: compute hackathon detail countdown on client
luojiyin1987 Apr 22, 2026
180ce0e
fix: compute hackathon team countdown on client
luojiyin1987 Apr 22, 2026
fb28d94
fix: remove public creator email links
luojiyin1987 Apr 22, 2026
e4d5075
fix: normalize hackathon rich text extraction
luojiyin1987 Apr 22, 2026
3351ccb
fix: guard hackathon detail schema access
luojiyin1987 Apr 22, 2026
f85c138
fix: harden hackathon team public access
luojiyin1987 Apr 22, 2026
0c45861
fix: localize hackathon team breadcrumb label
luojiyin1987 Apr 22, 2026
b5a70ed
feat: add english breadcrumb translation
luojiyin1987 Apr 22, 2026
729455e
feat: add simplified chinese breadcrumb translation
luojiyin1987 Apr 22, 2026
7ca1d72
feat: add traditional chinese breadcrumb translation
luojiyin1987 Apr 22, 2026
8d7649e
fix: stabilize live countdown refresh callback
luojiyin1987 Apr 22, 2026
26a42d2
fix: return standard hackathon detail notFound
luojiyin1987 Apr 22, 2026
72b77bf
fix: return standard hackathon team notFound
luojiyin1987 Apr 22, 2026
d30960b
fix: support string product timestamps
luojiyin1987 Apr 22, 2026
eb20e2d
fix: sort hackathon detail agenda safely
luojiyin1987 Apr 22, 2026
236fee5
fix: sort hackathon team agenda safely
luojiyin1987 Apr 22, 2026
79c8b39
refactor: clarify hackathon countdown fallback logic
luojiyin1987 Apr 22, 2026
ba51623
fix: require valid hackathon detail form links
luojiyin1987 Apr 22, 2026
2fd23a2
fix: require valid hackathon team form links
luojiyin1987 Apr 22, 2026
dc2661a
fix: ignore empty product timestamps
luojiyin1987 Apr 22, 2026
0e5e716
refactor: reuse hackathon time parsing
luojiyin1987 Apr 22, 2026
d14dde3
fix: use grouped hackathon forms consistently
luojiyin1987 Apr 22, 2026
e7e5249
fix: require shareable hackathon team forms
luojiyin1987 Apr 22, 2026
e8f504f
fix: del no use import
luojiyin1987 Apr 22, 2026
f5c705c
fix: ignore boolean hackathon text values
luojiyin1987 Apr 22, 2026
7759941
refactor: focus hackathon team page on team content
luojiyin1987 Apr 23, 2026
00f8e86
style: compact hackathon team hero layout
luojiyin1987 Apr 23, 2026
cb7340e
fix: normalize hackathon countdown target
luojiyin1987 Apr 23, 2026
7416884
fix: normalize hackathon team project queries
luojiyin1987 Apr 23, 2026
2cbc9ef
fix: restore automatic env sync on install
luojiyin1987 Apr 28, 2026
ef8ec5f
fix: restore countdown state naming
luojiyin1987 Apr 28, 2026
6ffb732
fix: restore hackathon route param assumption
luojiyin1987 Apr 28, 2026
bcea1fa
refactor: use bootstrap utilities in hackathon team page
luojiyin1987 Apr 28, 2026
bbe4ff9
refactor: simplify hackathon team less rules
luojiyin1987 Apr 28, 2026
848345f
[refactor] simplify AI codes
TechQuery Apr 28, 2026
f057917
[optimize] simplify AI codes
TechQuery Apr 28, 2026
6edd8e0
[fix] Prettier formatting bug of LESS mixin
TechQuery Apr 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions components/Activity/Hackathon/TeamMember.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@import './theme.less';

.card {
// prettier-ignore
.panel-card();
padding: 1.35rem;
}

.avatar {
flex-shrink: 0;
box-shadow: 0 0 24px rgba(44, 232, 255, 0.14);
border: 1px solid rgba(44, 232, 255, 0.26);
width: 4rem;
height: 4rem;
}

.name {
margin: 0;
color: #fff;
font-weight: 700;
font-size: 1.05rem;
}

.link {
color: @cyan;
text-decoration: none;

&:hover {
color: #fff;
}
}

.summary {
margin: 1rem 0 0;
color: @muted;
line-height: 1.75;
}

.skill {
// prettier-ignore
.chip();
padding: 0.45rem 0.72rem;
color: rgba(255, 255, 255, 0.8);
font-size: 0.82rem;
}
48 changes: 48 additions & 0 deletions components/Activity/Hackathon/TeamMember.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Avatar } from 'idea-react';
import { TableCellUser } from 'mobx-lark';
import { FC } from 'react';
import { Card } from 'react-bootstrap';

import { Member } from '../../../models/Hackathon';
import styles from './TeamMember.module.less';

export const TeamMemberCard: FC<Member> = ({ person, githubAccount, summary, skills }) => {
const member = person as TableCellUser;
const githubName = githubAccount as string;
const memberSummary = summary as string;
const memberSkills = (skills as string[]).slice(0, 6);

return (
<Card className={`${styles.card} h-100`} body>
<div className="d-flex align-items-center gap-3">
<Avatar className={styles.avatar} src={member?.avatar_url} />
<div>
<h3 className={styles.name}>{member?.name || '-'}</h3>

{githubName && (
<a
className={styles.link}
href={`https://github.com/${githubName}`}
target="_blank"
rel="noreferrer"
>
@{githubName}
</a>
)}
</div>
</div>

{memberSummary && <p className={styles.summary}>{memberSummary}</p>}

{memberSkills[0] && (
<ul className="d-flex flex-wrap gap-2 mt-3 mb-0 p-0 list-unstyled">
{memberSkills.map(skill => (
<li key={skill} className={styles.skill}>
{skill}
</li>
))}
</ul>
)}
</Card>
);
};
7 changes: 2 additions & 5 deletions components/Activity/Hackathon/constant.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { TableCellUser } from 'mobx-lark';

import { Activity, ActivityModel } from '../../../models/Activity';
import { Agenda, Organization, Person, Prize, Project, Template } from '../../../models/Hackathon';
import { i18n } from '../../../models/Translation';
Expand Down Expand Up @@ -336,7 +335,6 @@ export const buildProjectItems = (
{ projects, activity }: { projects: Project[]; activity: Activity },
) =>
projects.map(({ id, name, score, summary, createdBy, members }) => {
const creator = createdBy as TableCellUser | undefined;
const scoreText = score === null || score === undefined || score === '' ? '—' : `${score}`;

return {
Expand All @@ -346,11 +344,10 @@ export const buildProjectItems = (
score: scoreText,
description: (summary as string) || '',
meta: [
creator
createdBy
? {
label: t('created_by'),
value: creator.name || '—',
valueHref: creator.email ? `mailto:${creator.email}` : undefined,
value: (createdBy as TableCellUser)?.name || '—',
}
: { label: t('created_by'), value: '—' },
{
Expand Down
44 changes: 44 additions & 0 deletions components/Activity/Hackathon/useLiveCountdownState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { TableCellValue } from 'mobx-lark';
import { useCallback, useEffect, useMemo, useState } from 'react';

import { CountdownWindow, firstTextOf, resolveCountdownState, timeOf } from './utility';

export const useLiveCountdownState = <T extends CountdownWindow>(
items: T[],
startTime?: TableCellValue,
endTime?: TableCellValue,
) => {
const [referenceTime, setReferenceTime] = useState<number | null>(null);
const refreshReferenceTime = useCallback(() => setReferenceTime(Date.now()), []);

useEffect(() => refreshReferenceTime(), [refreshReferenceTime]);

const countdownState = useMemo(
() =>
referenceTime === null
? {
nextItem: undefined as T | undefined,
countdownTo: firstTextOf(startTime) || firstTextOf(endTime) || undefined,
}
: resolveCountdownState(items, referenceTime, startTime, endTime),
[endTime, items, referenceTime, startTime],
);

useEffect(() => {
if (referenceTime === null) return;

const targetTime = timeOf(countdownState.countdownTo);

if (!Number.isFinite(targetTime)) return;

const delay = Math.min(2_147_483_647, Math.max(1000, targetTime - Date.now() + 1000));
const timer = window.setTimeout(
refreshReferenceTime,
delay,
);
Comment thread
luojiyin1987 marked this conversation as resolved.

return () => window.clearTimeout(timer);
}, [countdownState.countdownTo, referenceTime, refreshReferenceTime]);

return countdownState;
};
124 changes: 117 additions & 7 deletions components/Activity/Hackathon/utility.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TableCellValue, TableFormView } from 'mobx-lark';
import { formatDate } from 'web-utility';
import { Day, formatDate } from 'web-utility';

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

type NamedLike = { name?: string | null };
type TextLike = TableCellValue | NamedLike | null | undefined;
type TextListLike = TextLike | TextLike[];

const textOf = (value: TextLike) => {
if (value === null || value === undefined) return '';
if (typeof value === 'boolean') return '';

if (typeof value === 'object' && !Array.isArray(value)) {
const {
name,
text,
value: primitiveValue,
displayName,
display_name,
title,
content,
plainText,
plain_text,
user,
} = value as NamedLike & {
text?: string | null;
value?: string | number | null;
displayName?: string | null;
display_name?: string | null;
title?: string | null;
content?: string | null;
plainText?: string | null;
plain_text?: string | null;
user?: {
name?: string | null;
displayName?: string | null;
display_name?: string | null;
} | null;
};
const candidate = [
name,
text,
primitiveValue,
displayName,
display_name,
title,
content,
plainText,
plain_text,
user?.displayName,
user?.display_name,
user?.name,
].find(item => item !== null && item !== undefined && `${item}`.trim());

return candidate === null || candidate === undefined ? '' : `${candidate}`.trim();
}

const text = value.toString().trim();

return text === '[object Object]' ? '' : text;
};

export const firstTextOf = (value: TextListLike) =>
(Array.isArray(value) ? value.map(textOf).find(Boolean) : textOf(value)) || '';

export const formatMoment = (value?: TableCellValue) => (value ? formatDate(value as string) : '');

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

export const timeOf = (value?: TableCellValue) => {
if (value instanceof Date) return value.getTime();

if (typeof value === 'number') return Number.isFinite(value) ? value : NaN;

const text = firstTextOf(value as TextListLike);

if (!text) return NaN;

const time = Date.parse(text);

return Number.isFinite(time) ? time : NaN;
};
Comment thread
luojiyin1987 marked this conversation as resolved.

export interface CountdownWindow {
startedAt?: TableCellValue;
endedAt?: TableCellValue;
}

const countdownTextOf = (value?: TableCellValue) => {
const time = timeOf(value);

return Number.isFinite(time) ? new Date(time).toISOString() : undefined;
};

export const resolveCountdownState = <T extends CountdownWindow>(
items: T[],
referenceTime: number,
startTime?: TableCellValue,
endTime?: TableCellValue,
) => {
const nextItem = items.find(({ startedAt, endedAt }) => {
const started = timeOf(startedAt);
const ended = timeOf(endedAt);

return Number.isFinite(started) && Number.isFinite(ended) && referenceTime <= ended;
});
const nextStartedAt = timeOf(nextItem?.startedAt);
const nextCountdownTarget =
Number.isFinite(nextStartedAt) && nextStartedAt > referenceTime
? nextItem?.startedAt
: nextItem?.endedAt;
const fallbackCountdownTarget = timeOf(startTime) > referenceTime ? startTime : endTime;
const countdownTo =
countdownTextOf(nextCountdownTarget) || countdownTextOf(fallbackCountdownTarget);

return { nextItem, countdownTo };
};

export const previewText = (items: TableCellValue[], fallback: string) =>
items
.map(item => item?.toString())
.map(item => textOf(item))
.filter(Boolean)
.slice(0, 2)
.join(' · ') || fallback;
Expand Down Expand Up @@ -75,10 +185,10 @@ export const compactSummaryOf = (
) => {
const source = Array.isArray(text)
? text
.map(item => item?.toString())
.map(item => textOf(item))
.filter(Boolean)
.join(' · ')
: text?.toString() || '';
: textOf(text);
const normalized = source.replace(/\s+/g, ' ').trim();

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

export const daysBetween = (startedAt?: TableCellValue, endedAt?: TableCellValue) => {
const start = new Date((startedAt as string) || '').getTime();
const end = new Date((endedAt as string) || '').getTime();
const start = timeOf(startedAt);
const end = timeOf(endedAt);

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

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

export const normalizeAgendaType = (value?: TableCellValue) =>
Expand Down
6 changes: 5 additions & 1 deletion components/Activity/ProductCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ export const ProductCard: FC<ProductCardProps> = observer(
</div>
)}

<time className="text-dark opacity-75 small" dateTime={new Date(createdAt as number).toJSON()}>
<time
suppressHydrationWarning
className="text-dark opacity-75 small"
dateTime={new Date(createdAt as number).toJSON()}
>
📅 {formatDate(createdAt as number)}
</time>
</Card.Body>
Expand Down
Loading
Loading