Skip to content

Feat/hackathon team page#67

Open
luojiyin1987 wants to merge 19 commits intomainfrom
feat/hackathon-team-page
Open

Feat/hackathon team page#67
luojiyin1987 wants to merge 19 commits intomainfrom
feat/hackathon-team-page

Conversation

@luojiyin1987
Copy link
Copy Markdown
Contributor

@luojiyin1987 luojiyin1987 commented Apr 21, 2026

PR-67 PR-67 PR-67 Powered by Pull Request Badge

Checklist(清单):

  • Labels
  • Assignees
  • Reviewers

Closes #64

Summary by CodeRabbit

发布说明

  • 错误修复

    • 修复产品卡片在无效/不可解析日期时错误显示时间的问题(不再渲染无效时间标签)。
  • 功能改进

    • 优化黑客松详情与团队页面的倒计时与议程计算,统一使用参考时间以提高一致性。
    • 重构团队页布局与表单选择逻辑,改善英雄区操作与计分/表单展示。
    • 更新倒计时 Hook 行为以稳定客户端时间显示。
  • 重构

    • 增加通用时间与文本处理工具以规范日期、文本与用户信息提取。

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

重构与功能调整:ProductCard 条件化渲染 createdAt;引入服务端参考时间(agendaReferenceTime / renderedAt)并将倒计时/下一议程决策移入通用 util;Hackathon 团队页重构为基于 HackathonHero 的分节锚点布局,新增议程拉取与多项数据标准化辅助函数与计时解析。

Changes

Cohort / File(s) Summary
产品卡片渲染
components/Activity/ProductCard.tsx
将隐式 JSX 返回改为函数体,计算 createdAtValue/createdAtISO/createdAtText,仅当时间为有限数值时渲染 <time>,并添加 suppressHydrationWarning
站点级倒计时与页面 props
pages/hackathon/[id].tsx
getServerSideProps 现在注入服务端时间戳(agendaReferenceTime/renderedAt),组件 props 与倒计时逻辑改为调用 resolveCountdownState,提取 hero primaryAction 到局部变量。
团队页全面重构
pages/hackathon/[id]/team/[tid].tsx
服务端新增 AgendaModel.getAll(),传入 agendaReferenceTime,页面切换到 HackathonHero 架构,样式换为 HackathonTeam.module.less,引入锚点分节、表单选择/评分逻辑重写及成员/产品字段标准化渲染。
倒计时钩子调整
components/Activity/Hackathon/Hero.tsx
useCountdown 使用 clientNow state 并在 interval 中更新,memo 计算以 target - clientNow 进行。
常量/用户解析调整
components/Activity/Hackathon/constant.ts
移除直接的 TableCellUser 强制转换,改为使用 userOf(createdBy) 以解析 creator 信息。
工具集扩展
components/Activity/Hackathon/utility.ts
新增文本提取 firstTextOf/textListOf/relationNameOfuserOftimeOfCountdownWindow 接口及通用 resolveCountdownStatedaysBetween 改用 timeOf

Sequence Diagram(s)

sequenceDiagram
    participant Server as Server (getServerSideProps)
    participant DB as AgendaModel / DB
    participant Page as Next.js Page (HackathonDetail/Team)
    participant Client as Browser (HackathonHero / useCountdown)
    participant Util as resolveCountdownState

    Server->>DB: AgendaModel.getAll() / fetch activity/project
    DB-->>Server: agenda records
    Server->>Page: render props (activity, project, agenda, agendaReferenceTime)
    Page->>Util: resolveCountdownState(agenda, agendaReferenceTime, startTime, endTime)
    Util-->>Page: { nextItem, countdownTo }
    Page->>Client: render HackathonHero with countdownTo, phaseBadges, primaryForm
    Client->>Client: useCountdown interval updates clientNow
    Client->>Util: (client) compute display countdown from target and clientNow
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 分钟

Possibly related PRs

Suggested labels

feature

Suggested reviewers

  • TechQuery

浏览

优化黑客松项目详情页组件结构,增强服务端数据获取机制。ProductCard 组件重构了条件化时间元素渲染,HackathonDetail 页面引入服务端时间戳用于稳定倒计时逻辑,HackathonTeam 页面完全重设计为 HackathonHero 架构,新增议程数据获取与多个辅助函数处理数据标准化。

变更

分类 / 文件 摘要
时间渲染优化
components/Activity/ProductCard.tsx
重构隐式 JSX 返回为显式函数体,新增 createdAtValue 有限性校验,条件化 <time> 元素渲染,增加 suppressHydrationWarning 处理 SSR 不匹配风险。
服务端时间戳
pages/hackathon/[id].tsx
扩展 getServerSideProps 返回 agendaReferenceTime,更新 HackathonDetailProps 接口,组件改用服务端时间戳并将倒计时/下一议程决策逻辑委托给 resolveCountdownState
页面重设计
pages/hackathon/[id]/team/[tid].tsx
新增 AgendaModel.getAll() 数据获取,集成 HackathonHero 组件,切换样式至 HackathonTeam.module.less,新增数据标准化助手(如 firstTextOftextListOfrelationNameOfuserOftimeOf),重构 UI 为锚点导航分节结构,改进表单选择与评分面板逻辑。

预期代码审查工作量(快速参考)

🎯 4 (Complex) | ⏱️ ~60 分钟

相关 PR

  • PR #59:同样修改 pages/hackathon/[id].tsx,涉及英雄区、表单分组和服务端数据获取重写,与本 PR 在代码层面高度相关。

建议标签

feature

建议审查者

  • TechQuery

诗句

项目页焕新序,服务端时钟稳如初,
议程驰骋在英雄区,锚点导航牵四方,
文本净化与表单择优,评分与倒计时共舞 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive 大部分改动与 #64 需求相关,但 ProductCard.tsx、pages/hackathon/[id].tsx、Hero.tsx、constant.ts、utility.ts 的重构虽然支撑 team 页面功能,属于相关基础设施完善而非核心 team 页面设计本身。 建议澄清这些辅助文件的改动是否为 #64 的必须依赖,或是否应拆分为独立重构 PR。
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 标题「Feat/hackathon team page」明确指向主要功能变更(黑客松团队页面重设计),与链接问题 #64 及多个文件的核心改动一致,特别是 pages/hackathon/[id]/team/[tid].tsx 的大幅重构。
Linked Issues check ✅ Passed PR 实现了问题 #64 的核心需求:重新设计飞书黑客松参赛项目详情页(team/[tid].tsx),采用锚点导航(#overview/#members/#works/#creator)、HackathonHero 组件、议程倒计时、表单处理和成员数据聚合等设计模板特性。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hackathon-team-page

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pages/hackathon/[id]/team/[tid].tsx (1)

50-56: ⚠️ Potential issue | 🟠 Major

先校验 Agenda 表配置再查询。

新增的 AgendaModel(appId, tableIdMap.Agenda) 会让缺少 Agenda 表的 activity 直接 SSR 500;这里建议和详情页保持一致,对 appId / tableIdMap / 必需表做 notFound 兜底。

建议修复
-    const { appId, tableIdMap } = activity.databaseSchema;
+    const { appId, tableIdMap } = (activity.databaseSchema || {}) as BiTableSchema;
+
+    if (
+      !appId ||
+      !tableIdMap?.Project ||
+      !tableIdMap?.Member ||
+      !tableIdMap?.Product ||
+      !tableIdMap?.Agenda
+    )
+      return { notFound: true, props: {} };

     const project = await new ProjectModel(appId, tableIdMap.Project).getOne(params!.tid);

As per coding guidelines, **/*.{ts,tsx}: Use optional chaining and modern ECMAScript features.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id]/team/[tid].tsx around lines 50 - 56, The code
instantiates AgendaModel without verifying the schema, causing SSR 500 when
Agenda is missing; before calling new AgendaModel(appId, tableIdMap.Agenda) (and
similarly when using ProjectModel/getOne with params!.tid), validate that
activity.databaseSchema, appId and tableIdMap?.Agenda exist (use optional
chaining like activity?.databaseSchema?.tableIdMap?.Agenda) and return a
notFound response or early exit if any required piece is missing; update the
Promise.all block to only create AgendaModel when tableIdMap?.Agenda is present
and keep the same notFound guard used on the details page.
🧹 Nitpick comments (3)
components/Activity/ProductCard.tsx (2)

37-76: 按钮组的 URL 拼接与可读性可以收敛

四个按钮都基于 sourceLink 做字符串替换,建议提取一个本地常量 repoPath = (sourceLink as string).replace('https://github.com/', ''),再用数组 + map 渲染,避免重复计算与重复 JSX;同时 GitPod 的 URL 其实是 https://gitpod.io/#${sourceLink}(保留完整 URL 而非 repoPath),数组化后可以更直观地表达差异。

另外按编码规范,一级列表类内容可考虑用 <ul className="list-unstyled ..."> 承载按钮组以体现语义;但此处是操作按钮组,沿用 <div> 也可接受,属优化项。

♻️ Proposed refactor
-          {sourceLink && (
-            <div className="d-flex flex-wrap gap-2 mb-3">
-              <Button variant="dark" size="sm" href={sourceLink as string} target="_blank" rel="noreferrer">
-                GitHub
-              </Button>
-              <Button variant="primary" size="sm" href={`https://github.dev/${(sourceLink as string).replace('https://github.com/', '')}`} target="_blank" rel="noreferrer">
-                GitHub.dev
-              </Button>
-              <Button variant="dark" size="sm" href={`https://codespaces.new/${(sourceLink as string).replace('https://github.com/', '')}`} target="_blank" rel="noreferrer">
-                Codespaces
-              </Button>
-              <Button variant="warning" size="sm" href={`https://gitpod.io/#${sourceLink as string}`} target="_blank" rel="noreferrer">
-                GitPod
-              </Button>
-            </div>
-          )}
+          {sourceLink && (() => {
+            const repo = (sourceLink as string).replace('https://github.com/', '');
+            const links: { label: string; href: string; variant: string }[] = [
+              { label: 'GitHub',     href: sourceLink as string,              variant: 'dark' },
+              { label: 'GitHub.dev', href: `https://github.dev/${repo}`,      variant: 'primary' },
+              { label: 'Codespaces', href: `https://codespaces.new/${repo}`,  variant: 'dark' },
+              { label: 'GitPod',     href: `https://gitpod.io/#${sourceLink}`, variant: 'warning' },
+            ];
+            return (
+              <div className="d-flex flex-wrap gap-2 mb-3">
+                {links.map(({ label, href, variant }) => (
+                  <Button key={label} variant={variant} size="sm" href={href} target="_blank" rel="noreferrer">
+                    {label}
+                  </Button>
+                ))}
+              </div>
+            );
+          })()}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Activity/ProductCard.tsx` around lines 37 - 76, Extract a local
constant repoPath = (sourceLink as string).replace('https://github.com/', '')
inside the ProductCard component and replace the repeated string operations by
building an array of button configs (label, variant, href) where href uses
either repoPath (for github.dev and codespaces) or the full sourceLink (for
GitHub and GitPod), then render the buttons via map to eliminate duplication;
keep the outer wrapper (div) as-is or optionally change to <ul
className="list-unstyled ..."> if you want semantic grouping.

26-32: 过量 as string 类型断言,建议收敛到模型层

name as stringlink as stringsummary as stringsourceLink as string 多处出现(Line 26、28、30、32、34、42、51、60、69),说明 Product 模型里这些字段被定义成了联合类型或 unknown。按编码规范应让 TypeScript 自然推断强类型,建议在 models/Hackathon 中把这些字段收敛成 string | undefined,调用处就不必再断言,也能顺带避免 undefined as string 这种潜在空值风险(例如 (sourceLink as string).replace(...)sourceLink 为空时会崩,但此处已有 Line 37 的守卫,问题主要在类型信号不清)。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Activity/ProductCard.tsx` around lines 26 - 32, The code is full
of redundant `as string` assertions (e.g., in ProductCard.tsx usages of name,
link, summary, sourceLink) because the Product model types are too loose; update
the Product type in models/Hackathon to tighten these properties to `string |
undefined` (for name, link, summary, sourceLink as appropriate) so callers can
rely on proper type inference and remove the inline `as string` casts in
ProductCard (and other callers); ensure any nullable fields are handled with
existing runtime guards (e.g., the sourceLink guard before calling .replace) or
use safe conditional rendering so no `undefined` is cast to string at call
sites.
pages/hackathon/[id]/team/[tid].tsx (1)

286-309: 建议将 metric grid 重构为 React Bootstrap 组件。

lines 286-309 的 metric grid 使用了原生 <div><article><span><strong> 元素,但项目已在本文件其他位置使用 React Bootstrap(ContainerRowColCard 等),建议统一改用 Bootstrap 组件组合:使用 Row + Col 承载栅栏布局,用 Card 包装卡片结构,保持组件一致性。这样既符合《编码规范》要求,也避免 UI 结构漂移。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id]/team/[tid].tsx around lines 286 - 309, The metric grid
currently built with raw elements (div/article/span/strong) using classNames
metricGrid, metricCard, metricLabel, metricValue, and metricMeta should be
refactored to use React Bootstrap layout and card components: replace the outer
div.metricGrid with a Row, render each metricCard as a Col containing a Card
(Card.Body for content), move the label/value/meta into Card.Title/Card.Text or
semantic elements inside the Card, and preserve displayed values
(members.length, products.length, (location as TableCellLocation |
undefined)?.name || '-', locationText, scoreText || '--', eventRange) so
behavior doesn't change; ensure imports for Row, Col, Card are added at top of
the file and update any CSS reliance if necessary to align with Bootstrap
classes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/Activity/ProductCard.tsx`:
- Around line 14-18: The code wrongly calls Number(createdAt) which turns ISO
date strings into NaN and skips rendering; update the logic in ProductCard to
treat createdAt as either a numeric timestamp or an ISO string: if typeof
createdAt === 'number' or Number.isFinite(Number(createdAt)) use new
Date(Number(createdAt)).toJSON() and formatDate(Number(createdAt)), else if
typeof createdAt === 'string' and Date.parse(createdAt) is valid set
createdAtISO = new Date(createdAt).toJSON() and createdAtText = formatDate(new
Date(createdAt).getTime()); keep references to the existing identifiers
createdAtValue/createdAtISO/createdAtText and formatDate when applying the fix.

In `@pages/hackathon/`[id]/team/[tid].tsx:
- Line 203: creatorText currently combines creator?.name and creator?.email and
surfaces the email (PII) on public pages; update the logic to only use
creator?.name (e.g., set creatorText = creator?.name || '') and remove any uses
that render creator?.email in the Hero and creator card components; search for
the same pattern at the other occurrences (the blocks around the symbols
referencing creatorText or where creator details are rendered at the other noted
locations) and ensure they only display name or an explicitly authorized public
contact field instead of the email.
- Around line 154-166: scoreForm currently pulls the first Evaluation link from
formLinkMap without applying isPublicForm and the button gating uses scoreText
which prevents opening the scorer for unscored items; update the selection logic
so scoreForm is derived by filtering forms.Evaluation (or
Object.values(formLinkMap?.Evaluation || {})) with isPublicForm (same predicate
used for primaryForm/publicForms) and use the existence of scoreForm (not
scoreText) to control the button/iframe entry; adjust any references to
scoreForm selection and the button condition accordingly (look for symbols
scoreForm, isPublicForm, forms, formLinkMap, scoreText).
- Around line 261-262: The Breadcrumb element's aria-label is hardcoded as
"breadcrumb"; update the component to use the i18n t() function (e.g.,
aria-label={t('breadcrumb.ariaLabel')}) and add the corresponding locale key in
your translation files, and ensure the component obtains t via useTranslation
(or the project's i18n hook) so the aria-label is localized; locate the
Breadcrumb JSX (className={styles.breadcrumb}) and replace the literal string
with a t() call and add the new translation key.
- Around line 78-129: The code is casting Feishu cell values to strings (e.g.,
using description as string and toString() in helpers) which drops rich-cell
contents; update the extraction helpers (firstText, textListOf, relationNameOf)
to detect common Feishu cell shapes (e.g., objects with text, value, name, or
user.display_name/displayName) and return those text fields instead of relying
on toString(), then replace calls that pass raw casts (such as
compactSummaryOf(description as string, ...)) with firstText(description) or
textListOf(...) as appropriate so compactSummaryOf always receives a real
string; touch the functions named firstText, textListOf, relationNameOf and the
call site where compactSummaryOf is invoked to implement this behavior.

---

Outside diff comments:
In `@pages/hackathon/`[id]/team/[tid].tsx:
- Around line 50-56: The code instantiates AgendaModel without verifying the
schema, causing SSR 500 when Agenda is missing; before calling new
AgendaModel(appId, tableIdMap.Agenda) (and similarly when using
ProjectModel/getOne with params!.tid), validate that activity.databaseSchema,
appId and tableIdMap?.Agenda exist (use optional chaining like
activity?.databaseSchema?.tableIdMap?.Agenda) and return a notFound response or
early exit if any required piece is missing; update the Promise.all block to
only create AgendaModel when tableIdMap?.Agenda is present and keep the same
notFound guard used on the details page.

---

Nitpick comments:
In `@components/Activity/ProductCard.tsx`:
- Around line 37-76: Extract a local constant repoPath = (sourceLink as
string).replace('https://github.com/', '') inside the ProductCard component and
replace the repeated string operations by building an array of button configs
(label, variant, href) where href uses either repoPath (for github.dev and
codespaces) or the full sourceLink (for GitHub and GitPod), then render the
buttons via map to eliminate duplication; keep the outer wrapper (div) as-is or
optionally change to <ul className="list-unstyled ..."> if you want semantic
grouping.
- Around line 26-32: The code is full of redundant `as string` assertions (e.g.,
in ProductCard.tsx usages of name, link, summary, sourceLink) because the
Product model types are too loose; update the Product type in models/Hackathon
to tighten these properties to `string | undefined` (for name, link, summary,
sourceLink as appropriate) so callers can rely on proper type inference and
remove the inline `as string` casts in ProductCard (and other callers); ensure
any nullable fields are handled with existing runtime guards (e.g., the
sourceLink guard before calling .replace) or use safe conditional rendering so
no `undefined` is cast to string at call sites.

In `@pages/hackathon/`[id]/team/[tid].tsx:
- Around line 286-309: The metric grid currently built with raw elements
(div/article/span/strong) using classNames metricGrid, metricCard, metricLabel,
metricValue, and metricMeta should be refactored to use React Bootstrap layout
and card components: replace the outer div.metricGrid with a Row, render each
metricCard as a Col containing a Card (Card.Body for content), move the
label/value/meta into Card.Title/Card.Text or semantic elements inside the Card,
and preserve displayed values (members.length, products.length, (location as
TableCellLocation | undefined)?.name || '-', locationText, scoreText || '--',
eventRange) so behavior doesn't change; ensure imports for Row, Col, Card are
added at top of the file and update any CSS reliance if necessary to align with
Bootstrap classes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e60b7847-5be0-4147-933a-aa3bdbd9d19e

📥 Commits

Reviewing files that changed from the base of the PR and between d935d7a and 6e4f39e.

⛔ Files ignored due to path filters (6)
  • README.md is excluded by !**/*.md and included by none
  • package.json is excluded by none and included by none
  • styles/HackathonTeam.module.less is excluded by none and included by none
  • translation/en-US.ts is excluded by none and included by none
  • translation/zh-CN.ts is excluded by none and included by none
  • translation/zh-TW.ts is excluded by none and included by none
📒 Files selected for processing (3)
  • components/Activity/ProductCard.tsx
  • pages/hackathon/[id].tsx
  • pages/hackathon/[id]/team/[tid].tsx

Comment thread components/Activity/ProductCard.tsx
Comment thread pages/hackathon/[id]/team/[tid].tsx Outdated
Comment on lines +154 to +166
const publicForms = useMemo(
() =>
Object.values(forms || {})
.flat()
.filter(Boolean)
.filter(isPublicForm as (value: TableFormView) => boolean),
[forms],
);
const primaryForm =
((forms?.Person || []).filter(isPublicForm as (value: TableFormView) => boolean)[0] ||
(forms?.Project || []).filter(isPublicForm as (value: TableFormView) => boolean)[0] ||
publicForms[0]);
const scoreForm = Object.values(formLinkMap?.Evaluation || {})[0];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

评分表单也应使用公开表单,并且不应依赖已有分数。

scoreForm 当前绕过了 isPublicForm,可能把非公开 Evaluation 链接塞进 iframe;同时按钮条件 scoreText && scoreForm 会让“尚未评分”的项目无法打开评分表。建议从 forms.Evaluation 中筛公开表单,并仅以表单是否存在控制入口。

建议修复
-    const scoreForm = Object.values(formLinkMap?.Evaluation || {})[0];
+    const scoreForm = (forms?.Evaluation || []).filter(
+      isPublicForm as (value: TableFormView) => boolean,
+    )[0];
-                  {scoreText && scoreForm && (
+                  {scoreForm && (
                     <Button className={styles.scoreButton} onClick={() => setShowScoreModal(true)}>
                       {t('score')}
                     </Button>
                   )}
-              <iframe className="w-100 h-100 border-0" title={t('score')} src={scoreForm} />
+              <iframe
+                className="w-100 h-100 border-0"
+                title={t('score')}
+                src={scoreForm?.shared_url}
+              />

Also applies to: 410-437

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id]/team/[tid].tsx around lines 154 - 166, scoreForm
currently pulls the first Evaluation link from formLinkMap without applying
isPublicForm and the button gating uses scoreText which prevents opening the
scorer for unscored items; update the selection logic so scoreForm is derived by
filtering forms.Evaluation (or Object.values(formLinkMap?.Evaluation || {}))
with isPublicForm (same predicate used for primaryForm/publicForms) and use the
existence of scoreForm (not scoreText) to control the button/iframe entry;
adjust any references to scoreForm selection and the button condition
accordingly (look for symbols scoreForm, isPublicForm, forms, formLinkMap,
scoreText).

Comment thread pages/hackathon/[id]/team/[tid].tsx
Comment on lines +261 to +262
<article className={styles.introPanel}>
<Breadcrumb aria-label="breadcrumb" className={styles.breadcrumb}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

aria-label 也需要走 i18n。

"breadcrumb" 是辅助技术可见文案,建议补充对应 locale key 后改为 t()

建议修复
-                <Breadcrumb aria-label="breadcrumb" className={styles.breadcrumb}>
+                <Breadcrumb aria-label={t('breadcrumb')} className={styles.breadcrumb}>

As per coding guidelines, {pages,components}/**/*.tsx: All user-facing text MUST use the i18n t() function (no hardcoded strings).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<article className={styles.introPanel}>
<Breadcrumb aria-label="breadcrumb" className={styles.breadcrumb}>
<article className={styles.introPanel}>
<Breadcrumb aria-label={t('breadcrumb')} className={styles.breadcrumb}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id]/team/[tid].tsx around lines 261 - 262, The Breadcrumb
element's aria-label is hardcoded as "breadcrumb"; update the component to use
the i18n t() function (e.g., aria-label={t('breadcrumb.ariaLabel')}) and add the
corresponding locale key in your translation files, and ensure the component
obtains t via useTranslation (or the project's i18n hook) so the aria-label is
localized; locate the Breadcrumb JSX (className={styles.breadcrumb}) and replace
the literal string with a t() call and add the new translation key.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
pages/hackathon/[id].tsx (2)

85-89: ⚠️ Potential issue | 🟠 Major

先保护 databaseSchema 再解构。

如果活动未配置数据库 schema,这里会先抛 TypeError,后面的 notFound 分支不会执行;页面会变成 500。

建议修复
-    const { appId, tableIdMap } = activity.databaseSchema;
+    const { appId, tableIdMap } = activity.databaseSchema || {};
 
     if (!appId || !tableIdMap) return { notFound: true, props: {} };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id].tsx around lines 85 - 89, Don't destructure
activity.databaseSchema before verifying it exists; add an early guard like if
(!activity || !activity.databaseSchema) return { notFound: true, props: {} }
before const { appId, tableIdMap } = activity.databaseSchema, then proceed to
check appId, tableIdMap and iterate RequiredTableKeys to return notFound when
any required tableId is missing (references: activity, databaseSchema, appId,
tableIdMap, RequiredTableKeys).

79-80: ⚠️ Potential issue | 🟠 Major

不要在可缓存的 SSR props 中包含倒计时参考时间。

cache() 会将整个响应(包括 agendaReferenceTime: Date.now() 的快照)复用,直到缓存失效。在此期间,resolveCountdownState() 基于旧的参考时间计算倒计时目标,导致阶段切换后倒计时停留在过期状态上。

建议方案:移除 SSR 缓存(改用 ISR 或 SSR 无缓存);或将倒计时逻辑迁至客户端,使用当前时间重新解析 countdownTo

同样问题出现在 pages/hackathon/[id]/team/[tid].tsx 第 77 行。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id].tsx around lines 79 - 80, The SSR props include a
snapshot timestamp (agendaReferenceTime / countdownTo) that is being cached by
cache() inside export const getServerSideProps = compose(...), causing stale
countdowns; to fix, remove cache() from the compose call (or switch to
non-cached SSR/ISR) and instead move countdown resolution to the client by
returning the raw target/time identifier and calling resolveCountdownState (or
equivalent client-side logic) in the page component; make the same change for
the analogous export const getServerSideProps in
pages/hackathon/[id]/team/[tid].tsx (remove cache() and stop embedding
Date.now() reference in SSR props).
pages/hackathon/[id]/team/[tid].tsx (1)

55-62: ⚠️ Potential issue | 🟠 Major

校验表配置后再实例化模型。

当前直接使用 tableIdMap.Project/Agenda/Member/Product;任一表缺失都会在 SSR 中抛错,无法返回 notFound。团队页应和详情页一样先兜底 schema 与必需表。

建议修复
-    const { appId, tableIdMap } = activity.databaseSchema;
+    const { appId, tableIdMap } = activity.databaseSchema || {};
+
+    if (
+      !appId ||
+      !tableIdMap?.Project ||
+      !tableIdMap?.Agenda ||
+      !tableIdMap?.Member ||
+      !tableIdMap?.Product
+    )
+      return { notFound: true, props: {} };
 
     const project = await new ProjectModel(appId, tableIdMap.Project).getOne(params!.tid);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id]/team/[tid].tsx around lines 55 - 62, Before
instantiating any models, validate activity.databaseSchema and required table
IDs (e.g., tableIdMap.Project, tableIdMap.Agenda, tableIdMap.Member,
tableIdMap.Product) exist; if any are missing, short-circuit to the same
notFound handling used in the details page. Specifically, check
activity.databaseSchema and the presence of
tableIdMap.Project/Agenda/Member/Product before calling new
ProjectModel(...).getOne(params!.tid) or Promise.all with new
AgendaModel(...).getAll(), new MemberModel(...).getAll(), new
ProductModel(...).getAll(); if validation fails, return notFound to avoid SSR
errors.
♻️ Duplicate comments (6)
components/Activity/Hackathon/constant.ts (1)

348-353: ⚠️ Potential issue | 🟠 Major

不要在公开项目卡片中暴露创建者邮箱。

valueHref: mailto:${creator.email} 会把 Feishu 用户邮箱暴露到公开页面;建议只展示姓名,联系入口改用明确授权的公开联系字段。这个问题与之前团队页创建者邮箱暴露的反馈同类。

建议修复
           ? {
               label: t('created_by'),
               value: creator.name || '—',
-              valueHref: creator.email ? `mailto:${creator.email}` : undefined,
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Activity/Hackathon/constant.ts` around lines 348 - 353, The
creator entry currently exposes creator.email via valueHref
(`mailto:${creator.email}`) which leaks private emails on public cards; update
the creator object construction (the creator block that sets label:
t('created_by'), value: creator.name || '—', valueHref: ...) to stop injecting
mailto links for public views—remove or set valueHref to undefined for public
cards and instead use an explicitly authorized public contact field (e.g.,
creator.publicContact or a dedicated contact consent flag) if present; ensure
only the name is displayed when no public contact is available.
pages/hackathon/[id]/team/[tid].tsx (4)

119-120: ⚠️ Potential issue | 🟠 Major

先用文本 helper 归一化摘要再截断。

compactSummaryOf(description, ...) 仍会对对象型 Feishu 单元格走 toString(),项目摘要可能显示 [object Object] 或丢失文本;这里应先 textListOf()。这个问题与之前 Feishu cell 文本提取反馈同类。

建议修复
+    const projectDescription = textListOf(description).join(' · ');
     const projectSummary =
-      compactSummaryOf(description, firstTextOf(activitySummary) || displayTitle, 140);
+      compactSummaryOf(projectDescription, firstTextOf(activitySummary) || displayTitle, 140);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id]/team/[tid].tsx around lines 119 - 120, The project
summary currently passes raw Feishu cell objects to compactSummaryOf which can
yield "[object Object]"; instead normalize cell content first by calling
textListOf on the description (and on activitySummary if it's an array) before
truncation. Update the call site using compactSummaryOf to pass
textListOf(description) (and keep firstTextOf(activitySummary) || displayTitle
as-is or replace firstTextOf(activitySummary) with
firstTextOf(textListOf(activitySummary)) if activitySummary may be object cells)
so compactSummaryOf receives plain text.

241-242: ⚠️ Potential issue | 🟡 Minor

aria-label 也需要走 i18n。

"breadcrumb" 是辅助技术可见文案,应使用 t(),避免多语言页面混入硬编码英文。这个问题之前已经指出,当前代码仍存在。As per coding guidelines, {pages,components}/**/*.tsx: All user-facing text MUST use the i18n t() function (no hardcoded strings).

建议修复
-                <Breadcrumb aria-label="breadcrumb" className={styles.breadcrumb}>
+                <Breadcrumb aria-label={t('breadcrumb')} className={styles.breadcrumb}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id]/team/[tid].tsx around lines 241 - 242, Replace the
hardcoded aria-label="breadcrumb" with a localized string via the i18n t()
function: import or use the existing translation hook (t) in this component and
change the Breadcrumb's aria-label to t('breadcrumb') or a more specific key
(e.g., t('team.breadcrumb')) so the assistive text is localized; update any
nearby uses of aria-label on the same JSX tree (the <Breadcrumb> element inside
the article with className={styles.introPanel}) to follow the same pattern.

107-107: ⚠️ Potential issue | 🟠 Major

评分入口应只使用公开 Evaluation 表单,并且不依赖已有分数。

scoreForm 当前绕过 isPublicForm,且按钮条件 scoreText && scoreForm 会让未评分项目无法打开评分表。这个问题之前已经指出,当前代码仍存在。

建议修复
-    const { forms, formLinkMap } = databaseSchema;
+    const { forms } = databaseSchema;
-    const scoreForm = Object.values(formLinkMap?.Evaluation || {})[0];
+    const scoreForm = (forms?.Evaluation || []).filter(isPublicForm)[0];
-                  {scoreText && scoreForm && (
+                  {scoreForm && (
                     <Button className={styles.scoreButton} onClick={() => setShowScoreModal(true)}>
                       {t('score')}
                     </Button>
                   )}
-              <iframe className="w-100 h-100 border-0" title={t('score')} src={scoreForm} />
+              <iframe
+                className="w-100 h-100 border-0"
+                title={t('score')}
+                src={scoreForm?.shared_url}
+              />

Also applies to: 145-157, 390-417

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id]/team/[tid].tsx at line 107, The scoring entry currently
selects scoreForm and gates the score button using existing score text, which
incorrectly bypasses the form's public flag and prevents scoring unscored items;
change the selection logic where scoreForm is derived from
databaseSchema.forms/formLinkMap to explicitly pick the Evaluation form that is
public (e.g., find form => form.type === 'Evaluation' && form.isPublicForm) and
remove dependence on existing scoreText/score in the button
visibility/enablement logic so the score modal/button is enabled whenever a
public Evaluation form exists; update all occurrences referencing scoreForm,
scoreText, forms, and formLinkMap (including the other spots flagged) to use the
new public-Evaluation check and to open the scoring form regardless of prior
score.

184-184: ⚠️ Potential issue | 🟠 Major

不要在公开团队页展示创建者邮箱。

creatorText 和创建者卡片仍会渲染 creator?.email,邮箱属于 PII;公开页建议只展示姓名,联系方式改用明确授权的公开字段。这个问题之前已经指出,当前代码仍存在。

建议修复
-    const creatorText = [creator?.name, creator?.email].filter(Boolean).join(' · ');
+    const creatorText = creator?.name || '';
-              description: creatorText || locationText,
+              description: creatorText || locationText,
                   <span className={styles.creatorLabel}>{t('created_by')}</span>
                   <h3 className={styles.creatorValue}>{creator?.name || '-'}</h3>
-                  <p className={styles.creatorText}>{creator?.email || locationText}</p>
+                  <p className={styles.creatorText}>{locationText}</p>

Also applies to: 222-226, 385-389

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id]/team/[tid].tsx at line 184, creatorText currently
includes creator?.email and the creator card renders the email (PII) on the
public team page; remove any use of creator?.email and stop rendering the email
in the creator card so only the creator's name (e.g., creator?.name) and other
explicitly public fields are shown; update the occurrences that render or build
strings with creator?.email (including the creatorText constant and the other
instances flagged) to exclude the email and ensure any contact info uses only
approved public fields.
components/Activity/Hackathon/utility.ts (1)

36-49: ⚠️ Potential issue | 🟠 Major

补齐 Feishu 富文本/关联对象的字段提取。

textOf() 目前只认 name,对象型单元格如果是 { text }{ value }displayName 等形态会被直接置空,摘要、技能、成员信息仍可能丢失。这个问题与之前“不要把 Feishu cell 强转成字符串”的反馈同类,建议在公共 helper 里一次性兜住。

建议修复
-type NamedLike = { name?: string | null };
+type NamedLike = {
+  name?: string | null;
+  text?: string | null;
+  value?: string | number | null;
+  displayName?: string | null;
+  display_name?: string | null;
+};
 type TextLike = TableCellValue | NamedLike | null | undefined;
 type TextListLike = TextLike | TextLike[];
 
 const textOf = (value: TextLike) => {
   if (!value) return '';
 
-  if (typeof value === 'object' && !Array.isArray(value))
-    return 'name' in value ? (value.name || '').trim() : '';
+  if (typeof value === 'object' && !Array.isArray(value)) {
+    const candidate =
+      value.text ?? value.name ?? value.displayName ?? value.display_name ?? value.value;
+
+    return typeof candidate === 'string' || typeof candidate === 'number'
+      ? `${candidate}`.trim()
+      : '';
+  }

可用下面的只读检查确认调用点是否还依赖对象型单元格文本:

#!/bin/bash
# Description: Inspect changed hackathon code paths that normalize Feishu cell values.
rg -n -C3 '\b(firstTextOf|textListOf|relationNameOf|compactSummaryOf|TableCellValue)\b' components pages models
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Activity/Hackathon/utility.ts` around lines 36 - 49, The textOf
helper currently only extracts object.name and returns empty for other object
shapes; update textOf (and associated TextLike/TextListLike handling) to support
richer Feishu/related-cell shapes by: detect arrays and objects, for objects
check a prioritized set of fields
['name','text','value','displayName','display_name','title','content','plainText','plain_text']
and return the first non-empty trimmed string; if the value is an array
map/flatten and join elements via ', ' (or call textOf recursively) and ensure
TableCellValue union handling still returns '' for truly empty/unknown objects;
adjust signatures/comments and any callers (e.g., firstTextOf, textListOf,
relationNameOf, compactSummaryOf) only if their behavior depends on array vs
scalar results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/Activity/Hackathon/utility.ts`:
- Around line 69-73: timeOf 当前把 undefined/空字符串解析为 new Date(0) 导致返回
0(1970-01-01),从而误导 daysBetween() 和 resolveCountdownState() 把缺失时间当作有效时间;请在 timeOf
函数中先检测 value 是否为 null/undefined/空字符串(或非有效时间字符串/数字),对这类情况直接返回 NaN;只有在 value
是非空字符串或数字时才尝试用 Date.parse/new Date 解析并在解析失败(isNaN 或 !Number.isFinite)时返回
NaN,从而确保依赖 timeOf 的 daysBetween() 和 resolveCountdownState() 不会把“无时间”当成
1970-01-01。

In `@pages/hackathon/`[id]/team/[tid].tsx:
- Around line 49-50: getServerSideProps is currently wrapped with
compose/cache() and is returning a time-sensitive prop agendaReferenceTime (used
with resolveCountdownState to set nextAgendaItem and countdownTo), which gets
cached and leads to stale countdowns; remove agendaReferenceTime from the props
returned by the server (in getServerSideProps composed with cache()) and stop
calling resolveCountdownState on the server for cached responses. Instead,
compute resolveCountdownState (and derive nextAgendaItem and countdownTo) inside
the client component using Date.now() or a periodic timer (useEffect +
setInterval) so the countdown uses the current client time; apply the same
change to pages/hackathon/[id].tsx where agendaReferenceTime is returned. Ensure
server props still include the agenda data but not any cached reference
timestamp.

---

Outside diff comments:
In `@pages/hackathon/`[id].tsx:
- Around line 85-89: Don't destructure activity.databaseSchema before verifying
it exists; add an early guard like if (!activity || !activity.databaseSchema)
return { notFound: true, props: {} } before const { appId, tableIdMap } =
activity.databaseSchema, then proceed to check appId, tableIdMap and iterate
RequiredTableKeys to return notFound when any required tableId is missing
(references: activity, databaseSchema, appId, tableIdMap, RequiredTableKeys).
- Around line 79-80: The SSR props include a snapshot timestamp
(agendaReferenceTime / countdownTo) that is being cached by cache() inside
export const getServerSideProps = compose(...), causing stale countdowns; to
fix, remove cache() from the compose call (or switch to non-cached SSR/ISR) and
instead move countdown resolution to the client by returning the raw target/time
identifier and calling resolveCountdownState (or equivalent client-side logic)
in the page component; make the same change for the analogous export const
getServerSideProps in pages/hackathon/[id]/team/[tid].tsx (remove cache() and
stop embedding Date.now() reference in SSR props).

In `@pages/hackathon/`[id]/team/[tid].tsx:
- Around line 55-62: Before instantiating any models, validate
activity.databaseSchema and required table IDs (e.g., tableIdMap.Project,
tableIdMap.Agenda, tableIdMap.Member, tableIdMap.Product) exist; if any are
missing, short-circuit to the same notFound handling used in the details page.
Specifically, check activity.databaseSchema and the presence of
tableIdMap.Project/Agenda/Member/Product before calling new
ProjectModel(...).getOne(params!.tid) or Promise.all with new
AgendaModel(...).getAll(), new MemberModel(...).getAll(), new
ProductModel(...).getAll(); if validation fails, return notFound to avoid SSR
errors.

---

Duplicate comments:
In `@components/Activity/Hackathon/constant.ts`:
- Around line 348-353: The creator entry currently exposes creator.email via
valueHref (`mailto:${creator.email}`) which leaks private emails on public
cards; update the creator object construction (the creator block that sets
label: t('created_by'), value: creator.name || '—', valueHref: ...) to stop
injecting mailto links for public views—remove or set valueHref to undefined for
public cards and instead use an explicitly authorized public contact field
(e.g., creator.publicContact or a dedicated contact consent flag) if present;
ensure only the name is displayed when no public contact is available.

In `@components/Activity/Hackathon/utility.ts`:
- Around line 36-49: The textOf helper currently only extracts object.name and
returns empty for other object shapes; update textOf (and associated
TextLike/TextListLike handling) to support richer Feishu/related-cell shapes by:
detect arrays and objects, for objects check a prioritized set of fields
['name','text','value','displayName','display_name','title','content','plainText','plain_text']
and return the first non-empty trimmed string; if the value is an array
map/flatten and join elements via ', ' (or call textOf recursively) and ensure
TableCellValue union handling still returns '' for truly empty/unknown objects;
adjust signatures/comments and any callers (e.g., firstTextOf, textListOf,
relationNameOf, compactSummaryOf) only if their behavior depends on array vs
scalar results.

In `@pages/hackathon/`[id]/team/[tid].tsx:
- Around line 119-120: The project summary currently passes raw Feishu cell
objects to compactSummaryOf which can yield "[object Object]"; instead normalize
cell content first by calling textListOf on the description (and on
activitySummary if it's an array) before truncation. Update the call site using
compactSummaryOf to pass textListOf(description) (and keep
firstTextOf(activitySummary) || displayTitle as-is or replace
firstTextOf(activitySummary) with firstTextOf(textListOf(activitySummary)) if
activitySummary may be object cells) so compactSummaryOf receives plain text.
- Around line 241-242: Replace the hardcoded aria-label="breadcrumb" with a
localized string via the i18n t() function: import or use the existing
translation hook (t) in this component and change the Breadcrumb's aria-label to
t('breadcrumb') or a more specific key (e.g., t('team.breadcrumb')) so the
assistive text is localized; update any nearby uses of aria-label on the same
JSX tree (the <Breadcrumb> element inside the article with
className={styles.introPanel}) to follow the same pattern.
- Line 107: The scoring entry currently selects scoreForm and gates the score
button using existing score text, which incorrectly bypasses the form's public
flag and prevents scoring unscored items; change the selection logic where
scoreForm is derived from databaseSchema.forms/formLinkMap to explicitly pick
the Evaluation form that is public (e.g., find form => form.type ===
'Evaluation' && form.isPublicForm) and remove dependence on existing
scoreText/score in the button visibility/enablement logic so the score
modal/button is enabled whenever a public Evaluation form exists; update all
occurrences referencing scoreForm, scoreText, forms, and formLinkMap (including
the other spots flagged) to use the new public-Evaluation check and to open the
scoring form regardless of prior score.
- Line 184: creatorText currently includes creator?.email and the creator card
renders the email (PII) on the public team page; remove any use of
creator?.email and stop rendering the email in the creator card so only the
creator's name (e.g., creator?.name) and other explicitly public fields are
shown; update the occurrences that render or build strings with creator?.email
(including the creatorText constant and the other instances flagged) to exclude
the email and ensure any contact info uses only approved public fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fb9e860d-107f-40fc-9578-85e9c3133983

📥 Commits

Reviewing files that changed from the base of the PR and between 6e4f39e and 1e5b881.

📒 Files selected for processing (5)
  • components/Activity/Hackathon/Hero.tsx
  • components/Activity/Hackathon/constant.ts
  • components/Activity/Hackathon/utility.ts
  • pages/hackathon/[id].tsx
  • pages/hackathon/[id]/team/[tid].tsx

Comment on lines +69 to +73
export const timeOf = (value?: TableCellValue) => {
const time = new Date((value as string) || 0).getTime();

return Number.isFinite(time) ? time : NaN;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

不要把空时间解析成 1970-01-01。

timeOf(undefined) 当前返回 0,会让缺失开始时间被当作有效时间;daysBetween() 可能算出从 1970 年开始的超大天数,resolveCountdownState() 也可能选中时间不完整的议程。

建议修复
 export const timeOf = (value?: TableCellValue) => {
-  const time = new Date((value as string) || 0).getTime();
+  const text = firstTextOf(value as TextListLike);
+
+  if (!text) return NaN;
+
+  const time = new Date(text).getTime();
 
   return Number.isFinite(time) ? time : NaN;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Activity/Hackathon/utility.ts` around lines 69 - 73, timeOf 当前把
undefined/空字符串解析为 new Date(0) 导致返回 0(1970-01-01),从而误导 daysBetween() 和
resolveCountdownState() 把缺失时间当作有效时间;请在 timeOf 函数中先检测 value 是否为
null/undefined/空字符串(或非有效时间字符串/数字),对这类情况直接返回 NaN;只有在 value 是非空字符串或数字时才尝试用
Date.parse/new Date 解析并在解析失败(isNaN 或 !Number.isFinite)时返回 NaN,从而确保依赖 timeOf 的
daysBetween() 和 resolveCountdownState() 不会把“无时间”当成 1970-01-01。

Comment on lines 49 to 50
export const getServerSideProps = compose<Record<'id' | 'tid', string>>(
cache(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find cached SSR countdown reference-time patterns that may go stale.
rg -n -C4 'cache\(\)|agendaReferenceTime|Date\.now\(\)|resolveCountdownState' pages

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 12375


🏁 Script executed:

# 查找 cache() 函数的定义
rg -n "export.*cache|const cache|function cache" --type ts --type tsx | head -20

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 118


🏁 Script executed:

# 查看 resolveCountdownState 的完整实现
rg -n "resolveCountdownState" -A 15 components/Activity/Hackathon/utility

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 168


🏁 Script executed:

# 找到所有 utility 相关文件
fd -name "utility" --type f

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 321


🏁 Script executed:

# 查找 cache 函数的定义
rg -n "cache\s*\(" --type ts | grep -E "export|const|function" | head -20

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 590


🏁 Script executed:

# 找到 cache 函数的定义
rg "cache\s*=" -A 10 | head -50

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 1490


🏁 Script executed:

# 查找 utility 文件
fd "utility" -type f

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 261


🏁 Script executed:

# 查找 resolveCountdownState 函数的完整实现
rg "resolveCountdownState" -A 20

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 7038


🏁 Script executed:

# 找到 cache 函数的定义位置
rg "export.*cache" -A 10 | grep -E "function|const.*cache|=>" | head -20

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 1070


🏁 Script executed:

# 查看 utils 或 middleware 中的 cache 函数
fd "cache" -type f

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 261


🏁 Script executed:

# 在所有 ts/tsx 文件中查找 cache 的导入
rg "from.*cache|import.*cache" | head -20

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 1026


🏁 Script executed:

# 查看 next-ssr-middleware 包的文档或配置
rg "next-ssr-middleware" -A 5 | head -40

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 2202


🏁 Script executed:

# 查看 package.json 中的依赖版本
cat package.json | grep -A 50 "dependencies"

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 1693


🏁 Script executed:

# 查看 pages/hackathon/[id]/team/[tid].tsx 的完整 getServerSideProps 实现
cat pages/hackathon/[id]/team/[tid].tsx | sed -n '49,81p'

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 1031


🏁 Script executed:

# 查看 next-ssr-middleware 的缓存默认值
npm view next-ssr-middleware --json | head -100

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 3125


🏁 Script executed:

# 检查是否在客户端组件中重新计算倒计时时间
rg "useEffect|useMemo" pages/hackathon/\[id\]/team/\[tid\].tsx -A 5 | head -50

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 397


🏁 Script executed:

# 查看完整的 ProjectPage 组件实现,特别是倒计时相关部分
cat pages/hackathon/\[id\]/team/\[tid\].tsx | sed -n '92,200p'

Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io

Length of output: 3943


避免在 SSR 缓存中包含时间敏感的倒计时参考时间。

cache() 会缓存整个 getServerSideProps 的返回值,包括 agendaReferenceTime: Date.now()(第 77 行)。缓存命中时,页面会继续使用过期的参考时间调用 resolveCountdownState() 来计算 nextAgendaItemcountdownTo,导致议程阶段显示不准确或过期。

同样问题也出现在 pages/hackathon/[id].tsx(第 104 行)。

建议将倒计时参考时间从缓存的 props 中移除,改为在客户端组件中使用当前时间或定期更新。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/hackathon/`[id]/team/[tid].tsx around lines 49 - 50, getServerSideProps
is currently wrapped with compose/cache() and is returning a time-sensitive prop
agendaReferenceTime (used with resolveCountdownState to set nextAgendaItem and
countdownTo), which gets cached and leads to stale countdowns; remove
agendaReferenceTime from the props returned by the server (in getServerSideProps
composed with cache()) and stop calling resolveCountdownState on the server for
cached responses. Instead, compute resolveCountdownState (and derive
nextAgendaItem and countdownTo) inside the client component using Date.now() or
a periodic timer (useEffect + setInterval) so the countdown uses the current
client time; apply the same change to pages/hackathon/[id].tsx where
agendaReferenceTime is returned. Ensure server props still include the agenda
data but not any cached reference timestamp.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

重新设计飞书黑客松参赛项目详情页

1 participant