From af788825ca8918258cb00447f2e9edcbe30fbe8c Mon Sep 17 00:00:00 2001 From: luojiyin Date: Tue, 21 Apr 2026 23:31:34 +0800 Subject: [PATCH 01/51] chore: make env sync explicit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4afaa1..81d43b8 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "prepare": "husky", - "install": "pnpx git-utility download https://github.com/Open-Source-Bazaar/key-vault main Open-Source-Bazaar.github.io || true", + "sync-env": "pnpx git-utility download https://github.com/Open-Source-Bazaar/key-vault main Open-Source-Bazaar.github.io", "postinstall": "next typegen", "dev": "next dev --webpack", "build": "next build --webpack", From ebf697213628fc59cf1f3bf71296ebe054fb6978 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Tue, 21 Apr 2026 23:31:34 +0800 Subject: [PATCH 02/51] docs: document explicit env sync --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ae839d2..2d02890 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,13 @@ ## 开始 ```bash +pnpm run sync-env pnpm install pnpm dev ``` +`pnpm run sync-env` 会在你显式执行时,从 `Open-Source-Bazaar/key-vault` 同步当前项目所需的私有环境文件。出于安全考虑,仓库不会在 `pnpm install` 阶段自动下载 `.env.local`。 + 可访问 http://localhost:3000. [1]: https://github.com/idea2app/Lark-Next-Bootstrap-ts From 7d82b014256178ab24c296a9bc0fcec8518210fe Mon Sep 17 00:00:00 2001 From: luojiyin Date: Tue, 21 Apr 2026 23:31:35 +0800 Subject: [PATCH 03/51] fix: stabilize hackathon detail hydration --- pages/hackathon/[id].tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pages/hackathon/[id].tsx b/pages/hackathon/[id].tsx index 5ee064e..f2000c4 100644 --- a/pages/hackathon/[id].tsx +++ b/pages/hackathon/[id].tsx @@ -72,6 +72,7 @@ interface HackathonDetailProps { projects: Project[]; templates: Template[]; }; + renderedAt: number; } export const getServerSideProps = compose<{ id: string }>( @@ -99,12 +100,13 @@ export const getServerSideProps = compose<{ id: string }>( props: { activity, hackathon: { people, organizations, agenda, prizes, templates, projects }, + renderedAt: Date.now(), }, }; }, ); -const HackathonDetail: FC = observer(({ activity, hackathon }) => { +const HackathonDetail: FC = observer(({ activity, hackathon, renderedAt }) => { const i18n = useContext(I18nContext); const { t } = i18n; const { @@ -182,7 +184,7 @@ const HackathonDetail: FC = observer(({ activity, hackatho }; }) .filter(({ date, label }) => Boolean(date && label)); - const now = Date.now(); + const now = renderedAt; const nextAgendaItem = agendaItems.find(({ startedAt, endedAt }) => { const started = new Date((startedAt as string) || 0).getTime(); const ended = new Date((endedAt as string) || 0).getTime(); From 14d64e9aa0ed4c43323a3869c3a6e84f90865f79 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Tue, 21 Apr 2026 23:31:35 +0800 Subject: [PATCH 04/51] fix: suppress product card hydration mismatch --- components/Activity/ProductCard.tsx | 136 +++++++++++++++------------- 1 file changed, 75 insertions(+), 61 deletions(-) diff --git a/components/Activity/ProductCard.tsx b/components/Activity/ProductCard.tsx index 630cc06..de63b95 100644 --- a/components/Activity/ProductCard.tsx +++ b/components/Activity/ProductCard.tsx @@ -10,68 +10,82 @@ import styles from '../../styles/Hackathon.module.less'; export type ProductCardProps = Product & Omit; export const ProductCard: FC = observer( - ({ className = '', id, createdAt, name, sourceLink, link = sourceLink, summary, ...props }) => ( - - - - {(name || link) as string} - -

{summary as string}

-
- -
+ ({ className = '', id, createdAt, name, sourceLink, link = sourceLink, summary, ...props }) => { + const createdAtValue = Number(createdAt); + const createdAtISO = Number.isFinite(createdAtValue) + ? new Date(createdAtValue).toJSON() + : undefined; + const createdAtText = Number.isFinite(createdAtValue) ? formatDate(createdAtValue) : ''; - {sourceLink && ( -
- - - - + return ( + + + + {(name || link) as string} + +

{summary as string}

+
+
- )} - -
-
- ), + {sourceLink && ( +
+ + + + +
+ )} + + {createdAtISO && ( + + )} + + + ); + }, ); From f194794655092cbccdb22ac957690a4e881a504a Mon Sep 17 00:00:00 2001 From: luojiyin Date: Tue, 21 Apr 2026 23:31:35 +0800 Subject: [PATCH 05/51] i18n: add hackathon team showcase label in en-US --- translation/en-US.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/translation/en-US.ts b/translation/en-US.ts index 724cc00..f4ef7fa 100644 --- a/translation/en-US.ts +++ b/translation/en-US.ts @@ -334,6 +334,7 @@ export default { score: 'Score', // Team detail page + hackathon_team_showcase: 'Team Showcase', team_members: 'Team Members', team_works: 'Team Works', no_news_yet: 'No news yet', From f30369f8e61e89e8afb959c82cca4170e8343db9 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Tue, 21 Apr 2026 23:31:36 +0800 Subject: [PATCH 06/51] i18n: add hackathon team showcase label in zh-CN --- translation/zh-CN.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/translation/zh-CN.ts b/translation/zh-CN.ts index 99d776f..9d47313 100644 --- a/translation/zh-CN.ts +++ b/translation/zh-CN.ts @@ -310,6 +310,7 @@ export default { score: '评分', // Team detail page + hackathon_team_showcase: '团队展示', team_members: '团队成员', team_works: '团队作品', no_news_yet: '暂无动态', From d00122bccea800f2b07f038113ca73ed30f3c72c Mon Sep 17 00:00:00 2001 From: luojiyin Date: Tue, 21 Apr 2026 23:31:36 +0800 Subject: [PATCH 07/51] i18n: add hackathon team showcase label in zh-TW --- translation/zh-TW.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/translation/zh-TW.ts b/translation/zh-TW.ts index 457192c..e5d132d 100644 --- a/translation/zh-TW.ts +++ b/translation/zh-TW.ts @@ -310,6 +310,7 @@ export default { score: '評分', // Team detail page + hackathon_team_showcase: '團隊展示', team_members: '團隊成員', team_works: '團隊作品', no_news_yet: '暫無動態', From cab62c143e77a26c89cc81a1c06b0cb9d7d364d1 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Tue, 21 Apr 2026 23:31:36 +0800 Subject: [PATCH 08/51] feat: add hackathon team page styles --- styles/HackathonTeam.module.less | 298 +++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 styles/HackathonTeam.module.less diff --git a/styles/HackathonTeam.module.less b/styles/HackathonTeam.module.less new file mode 100644 index 0000000..528adcc --- /dev/null +++ b/styles/HackathonTeam.module.less @@ -0,0 +1,298 @@ +@import '../components/Activity/Hackathon/theme.less'; + +.page { + background: + radial-gradient(circle at top left, rgba(44, 232, 255, 0.18), transparent 32%), + radial-gradient(circle at 85% 12%, rgba(255, 120, 186, 0.15), transparent 24%), + linear-gradient(180deg, #0b1328 0%, #091022 48%, #050814 100%); +} + +.section-frame(); + +.section { + scroll-margin-top: 5rem; +} + +.introPanel, +.metricCard, +.memberCard, +.summaryCard, +.creatorCard, +.emptyState { + .panel-card(); +} + +.introPanel, +.summaryCard, +.creatorCard { + padding: 1.5rem; +} + +.introPanel { + display: grid; + gap: 1rem; +} + +.breadcrumb { + margin-bottom: 0; + + :global(.breadcrumb) { + margin-bottom: 0; + } + + :global(.breadcrumb-item), + :global(.breadcrumb-item a), + :global(.breadcrumb-item.active) { + color: rgba(255, 255, 255, 0.76); + text-decoration: none; + } + + :global(.breadcrumb-item + .breadcrumb-item::before) { + color: rgba(255, 255, 255, 0.35); + } +} + +.introTitle { + margin: 0; + color: #fff; + font-family: @heading; + font-size: clamp(1.35rem, 2vw, 1.75rem); + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.introText { + margin: 0; + color: @muted; + font-size: 1rem; + line-height: 1.8; +} + +.metaList { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin: 0; + padding: 0; + list-style: none; +} + +.metaItem { + .chip(); + padding: 0.55rem 0.9rem; + color: rgba(255, 255, 255, 0.82); + font-size: 0.88rem; +} + +.metricGrid, +.creatorGrid { + display: grid; + gap: 1.25rem; +} + +.metricGrid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin-top: 1.5rem; +} + +.creatorGrid { + grid-template-columns: minmax(0, 1.15fr) minmax(280px, 0.85fr); +} + +.metricCard { + padding: 1.2rem; + min-height: 152px; +} + +.metricLabel, +.creatorLabel { + .eyebrow(); + display: block; +} + +.metricValue { + display: block; + margin-top: 0.9rem; + color: #fff; + font-family: @heading; + font-size: clamp(1.5rem, 2.6vw, 2.1rem); + line-height: 1.1; +} + +.metricMeta { + display: block; + margin-top: 0.9rem; + color: @muted; + font-size: 0.92rem; + line-height: 1.6; +} + +.memberCard { + height: 100%; + padding: 1.35rem; +} + +.memberTop { + display: flex; + align-items: center; + gap: 1rem; +} + +.avatar { + flex-shrink: 0; + width: 4rem; + height: 4rem; + border: 1px solid rgba(44, 232, 255, 0.26); + box-shadow: 0 0 24px rgba(44, 232, 255, 0.14); +} + +.memberName { + margin: 0; + color: #fff; + font-weight: 700; + font-size: 1.05rem; +} + +.memberLink { + color: @cyan; + text-decoration: none; + + &:hover { + color: #fff; + } +} + +.memberSummary { + margin: 1rem 0 0; + color: @muted; + line-height: 1.75; +} + +.skillList { + display: flex; + flex-wrap: wrap; + gap: 0.55rem; + margin: 1rem 0 0; + padding: 0; + list-style: none; +} + +.skill { + .chip(); + padding: 0.45rem 0.72rem; + color: rgba(255, 255, 255, 0.8); + font-size: 0.82rem; +} + +.summaryTitle, +.creatorValue { + margin: 0; + color: #fff; + font-family: @heading; +} + +.summaryTitle { + font-size: clamp(1.3rem, 2vw, 1.7rem); + line-height: 1.5; +} + +.summaryText, +.creatorText { + margin: 1rem 0 0; + color: @muted; + line-height: 1.8; +} + +.creatorValue { + margin-top: 0.85rem; + font-size: 1.45rem; + line-height: 1.4; +} + +.scoreButton { + .button-primary(); + margin-top: 1rem; + width: 100%; +} + +.productCardOverride { + border-color: rgba(44, 232, 255, 0.16) !important; + background: linear-gradient(180deg, rgba(7, 18, 39, 0.92), rgba(6, 13, 30, 0.88)) !important; + + :global(.card-title), + :global(p), + :global(time), + :global(figcaption), + :global(.bi) { + color: @copy !important; + } + + :global(p), + :global(time), + :global(figcaption) { + opacity: 0.76 !important; + } + + :global(.btn-dark) { + border-color: rgba(44, 232, 255, 0.26); + background: rgba(44, 232, 255, 0.08); + color: #fff; + } + + :global(.btn-primary) { + border-color: rgba(123, 97, 255, 0.3); + background: rgba(123, 97, 255, 0.18); + } + + :global(.btn-warning) { + border-color: rgba(255, 201, 77, 0.26); + background: rgba(255, 201, 77, 0.16); + color: #fff; + } +} + +.emptyState { + padding: 2rem 1.5rem; + color: @muted; + text-align: center; + font-size: 1rem; +} + +.commentWrap { + margin-top: clamp(3rem, 5vw, 4rem); +} + +.scoreModal { + :global(.modal-content) { + border: 1px solid rgba(44, 232, 255, 0.18); + border-radius: 24px; + background: rgba(8, 18, 39, 0.96); + color: @copy; + } + + :global(.modal-header) { + border-bottom-color: rgba(255, 255, 255, 0.08); + } + + :global(.btn-close) { + filter: invert(1) grayscale(1); + } +} + +@media (max-width: 1199px) { + .creatorGrid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 991px) { + .metricGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 767px) { + .metricGrid { + grid-template-columns: 1fr; + } +} From 6e4f39eca80b1e51db2d0bcc63f33459fb669f8d Mon Sep 17 00:00:00 2001 From: luojiyin Date: Tue, 21 Apr 2026 23:31:37 +0800 Subject: [PATCH 09/51] feat: redesign hackathon team detail page --- pages/hackathon/[id]/team/[tid].tsx | 529 ++++++++++++++++++++-------- 1 file changed, 385 insertions(+), 144 deletions(-) diff --git a/pages/hackathon/[id]/team/[tid].tsx b/pages/hackathon/[id]/team/[tid].tsx index 8f6ae7b..6699b75 100644 --- a/pages/hackathon/[id]/team/[tid].tsx +++ b/pages/hackathon/[id]/team/[tid].tsx @@ -1,8 +1,8 @@ import { Avatar } from 'idea-react'; -import { BiTableSchema, TableCellUser } from 'mobx-lark'; +import { BiTableSchema, TableCellLocation, TableCellUser, TableFormView } from 'mobx-lark'; import { observer } from 'mobx-react'; import { cache, compose, errorLogger } from 'next-ssr-middleware'; -import { FC, useContext, useState } from 'react'; +import { FC, useContext, useMemo, useState } from 'react'; import { Breadcrumb, Button, @@ -12,15 +12,25 @@ import { Modal, Ratio, Row, - Tab, - Tabs, } from 'react-bootstrap'; import { CommentBox } from '../../../../components/Activity/CommentBox'; +import { buildCountdownUnitLabels } from '../../../../components/Activity/Hackathon/constant'; +import { HackathonHero } from '../../../../components/Activity/Hackathon/Hero'; +import { + agendaTypeLabelOf, + compactDateKeyOf, + compactSummaryOf, + formatMoment, + formatPeriod, + isPublicForm, +} from '../../../../components/Activity/Hackathon/utility'; import { ProductCard } from '../../../../components/Activity/ProductCard'; import { PageHead } from '../../../../components/Layout/PageHead'; import { Activity, ActivityModel } from '../../../../models/Activity'; import { + Agenda, + AgendaModel, Member, MemberModel, Product, @@ -28,8 +38,8 @@ import { Project, ProjectModel, } from '../../../../models/Hackathon'; -import { I18nContext } from '../../../../models/Translation'; -import styles from '../../../../styles/Hackathon.module.less'; +import { i18n, I18nContext } from '../../../../models/Translation'; +import styles from '../../../../styles/HackathonTeam.module.less'; export const getServerSideProps = compose>( cache(), @@ -42,7 +52,8 @@ export const getServerSideProps = compose>( const project = await new ProjectModel(appId, tableIdMap.Project).getOne(params!.tid); // Get approved members for this project - const [members, products] = await Promise.all([ + const [agenda, members, products] = await Promise.all([ + new AgendaModel(appId, tableIdMap.Agenda).getAll(), new MemberModel(appId, tableIdMap.Member).getAll({ project: project.name as string, status: 'approved', @@ -51,155 +62,385 @@ export const getServerSideProps = compose>( project: project.name as string, }), ]); - return { props: { activity, project, members, products } }; + return { props: { activity, project, agenda, members, products, renderedAt: Date.now() } }; }, ); interface ProjectPageProps { activity: Activity; + agenda: Agenda[]; project: Project; members: Member[]; products: Product[]; + renderedAt: number; } -const ProjectPage: FC = observer(({ activity, project, members, products }) => { +const firstText = (value: unknown) => + (Array.isArray(value) ? value.find(Boolean) : value)?.toString().trim() || ''; + +const textListOf = (value: unknown) => + Array.isArray(value) + ? value + .map(item => item?.toString().trim()) + .filter(text => text && text !== '[object Object]') + : firstText(value) + ? [firstText(value)] + : []; + +const relationNameOf = (value: unknown) => + Array.isArray(value) + ? value + .map(item => + typeof item === 'object' && item && 'name' in item + ? String((item as { name?: string }).name || '') + : item?.toString() || '', + ) + .find(Boolean) || '' + : firstText(value); + +const ProjectPage: FC = observer( + ({ activity, agenda, project, members, products, renderedAt }) => { const { t } = useContext(I18nContext); const [showScoreModal, setShowScoreModal] = useState(false); - const { name: activityName, databaseSchema } = activity; - const { formLinkMap } = databaseSchema as unknown as BiTableSchema; - const { name: displayName, summary: description, createdBy, score } = project; - - const currentRoute = [ - { title: activityName as string, href: ActivityModel.getLink(activity) }, - { title: displayName as string }, - ]; - - return ( - <> - - - {/* Hero Section */} -
- - - {currentRoute.map(({ title, href }, index, { length }) => { - const isActive = index === length - 1; - - return ( - - {title} - - ); - })} - - -

{displayName as string}

-

{description as string}

- - {score != null && ( -
- -
- )} -
-
- - - {/* Team Members Section */} -
-

👥 {t('team_members')}

- - {members.map(({ id, person, githubAccount }) => ( - - -
- -
-

- {(person as TableCellUser).name} -

- - {githubAccount && ( - - @{githubAccount as string} - - )} -
-
-
- - ))} -
-
- - {/* Team Products Section */} -
-

💡 {t('team_works')}

- - {products && products.length > 0 ? ( - - {products.map(product => ( - - - - ))} - - ) : ( -
{t('no_news_yet')}
- )} -
- - {/* Creator Information Section */} -
-

👤 {t('created_by')}

- -
{(createdBy as TableCellUser).name}
- - {(createdBy as TableCellUser).email} - -
-
- - -
- - setShowScoreModal(false)}> - - {t('score')} - - - -