Skip to content

Feat/hackathon team page#67

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

Feat/hackathon team page#67
luojiyin1987 wants to merge 51 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

发布说明

  • 错误修复

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

    • 更严格的页面不存在处理:缺失或非法路由/数据时返回 404 页面。
    • 黑客松详情与团队页:统一并改进议程与倒计时逻辑、锚点式导航与英雄区主操作的默认行为。
    • 团队页:改进成员/作品展示、表单选择与计分按钮显示,评分 iframe 源地址修正。
  • 新增

    • 新增客户端实时倒计时 Hook,提升倒计时准确性与更新稳定性。
  • 重构

    • 引入通用文本与时间处理工具,统一摘要、技能与用户信息展示;移除创建者邮箱的 mailto 链接。

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

重构与功能调整:ProductCard 条件化渲染 createdAt;新增时间归一化与倒计时工具(timeOf / resolveCountdownState)及实时钩子 useLiveCountdownState;将页面内联倒计时逻辑抽离;Hackathon 活动页与团队页重构为基于 HackathonHero 的锚点式布局并统一文本/表单/议程处理(t() 假定已使用)。

Changes

Cohort / File(s) Summary
ProductCard 渲染
components/Activity/ProductCard.tsx
将隐式 JSX 返回改为显式函数体;新增 timeOf(createdAt) 验证并仅在时间为有限数值时渲染 <time>,使用 dateTime={new Date(createdAtTime).toJSON()}formatDate(createdAtTime) 并添加 suppressHydrationWarning
Hackathon 页面(详情)
pages/hackathon/[id].tsx
移除对 BiTableSchema 的强制转换,改用辅助函数 firstTextOf/textListOf/timeOf;getServerSideProps 在缺少 params 或必须表映射时返回 { notFound: true };议程排序与下一项/倒计时逻辑替换为 useLiveCountdownState;hero primaryAction 统一提前计算并提供默认。
Hackathon 团队页重构
pages/hackathon/[id]/team/[tid].tsx
增强 getServerSideProps 的存在性校验并返回 notFound;页面改为锚点导航(#overview/#members/#works/#creator)与 HackathonHero 驱动;使用 userOf/firstTextOf/textListOf 等工具规范成员与作品展示;评分 iframe 与表单选择逻辑调整;样式切换到 HackathonTeam.module.less,移除旧 Tab 依赖。
倒计时 Hook 与 Hero 调整
components/Activity/Hackathon/useLiveCountdownState.ts, components/Activity/Hackathon/Hero.tsx
新增导出钩子 useLiveCountdownState(管理 referenceTime、定时刷新与调用 resolveCountdownState);useCountdown 改为跟踪内部 clientNow,以 target - clientNow 计算倒计时。
通用工具扩展
components/Activity/Hackathon/utility.ts
新增并导出 firstTextOftextListOfrelationNameOfuserOftimeOfCountdownWindowresolveCountdownState;重构 previewText/compactSummaryOf/daysBetween 使用这些规范化函数以提高鲁棒性。
常量与用户解析
components/Activity/Hackathon/constant.ts
移除对 TableCellUser 的直接断言,改用 userOf(createdBy) 获取 creator;移除基于 email 的 mailto 链接生成。

Sequence Diagram(s)

sequenceDiagram
    participant Server as Server (getServerSideProps)
    participant DB as DB / AgendaModel
    participant Page as Next.js Page
    participant Util as resolveCountdownState
    participant Client as Browser (useLiveCountdownState / Hero)

    Server->>DB: 查询 activity / agenda 数据
    DB-->>Server: 返回 activity 与 agenda 列表
    Server->>Page: 提供 props (activity, agenda, renderedAt)
    Page->>Util: resolveCountdownState(agenda, renderedAt, startTime, endTime)
    Util-->>Page: { nextItem, countdownTo }
    Page->>Client: 渲染 Hero,传入 countdownTo / primaryForm
    Client->>Client: useLiveCountdownState 设置 referenceTime 并 schedule 刷新
    Client->>Util: 定时触发 resolveCountdownState 更新 UI 显示
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement, feature

Suggested reviewers

  • TechQuery

Poem

锁定时刻与议程声,英雄区里锚点行,
文本净化与表单序,倒计时声轻轻鸣,
团队页新样展风采,代码静好夜未央 ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive 部分变更超出 issue #64 的范围,包括更新 ProductCard、主页面、Hero 组件和工具函数,这些是为支持团队页面重设计而进行的基础设施改造。 建议明确文档说明基础设施改动的必要性,或在后续 PR 中分离这些通用改造,以保持变更聚焦于 team 页面本身。
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题「Feat/hackathon team page」虽简洁但能准确反映本次 PR 的主要变更——重新设计黑客松参赛项目详情页面,与链接的 issue #64 对应。
Linked Issues check ✅ Passed 代码变更涵盖了 issue #64 的核心需求:按照设计样板重新设计黑客松参赛项目详情页(team/[tid].tsx),包括英雄区域、导航、成员信息、作品展示和评估表单集成。
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 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 Outdated
Comment thread pages/hackathon/[id]/team/[tid].tsx Outdated
Comment thread pages/hackathon/[id]/team/[tid].tsx Outdated
Comment thread pages/hackathon/[id]/team/[tid].tsx Outdated
Comment thread pages/hackathon/[id]/team/[tid].tsx Outdated
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 thread components/Activity/Hackathon/utility.ts
Comment thread pages/hackathon/[id]/team/[tid].tsx
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: 3

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].tsx (1)

84-91: ⚠️ Potential issue | 🔴 Critical

notFound 返回值不能混入 props 属性。

Next.js 的 getServerSideProps 返回值应为 propsnotFoundredirect 三选一。当前代码在多处混用 { notFound: true, props: {} },违反 Next.js 规范,会导致 SSR 返回值无效。同时可以移除冗余的非空断言。

建议修复
-    if (!params?.id) return { notFound: true, props: {} };
+    if (!params?.id) return { notFound: true };
 
-    const activity = await new ActivityModel().getOne(params!.id);
+    const activity = await new ActivityModel().getOne(params.id);
 
-    if (!appId || !tableIdMap) return { notFound: true, props: {} };
+    if (!appId || !tableIdMap) return { notFound: true };
 
-    for (const key of RequiredTableKeys) if (!tableIdMap[key]) return { notFound: true, props: {} };
+    for (const key of RequiredTableKeys) if (!tableIdMap[key]) return { notFound: true };

同样问题存在于 pages/search/[model]/index.tsxpages/hackathon/[id]/team/[tid].tsx 等其他文件,建议一并修正。参考 Next.js 文档

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

In `@pages/hackathon/`[id].tsx around lines 84 - 91, The getServerSideProps
returns are incorrectly mixing notFound with props; update each early-return to
return { notFound: true } (no props) where you currently have patterns like "if
(!params?.id) return { notFound: true, props: {} }" and similar checks after
ActivityModel().getOne using appId/tableIdMap and RequiredTableKeys; also remove
redundant non-null assertions like params! when you already guard params?.id,
and apply the same change to the other files noted
(pages/search/[model]/index.tsx, pages/hackathon/[id]/team/[tid].tsx) so every
branch returns one of props, notFound, or redirect only.
♻️ Duplicate comments (2)
pages/hackathon/[id]/team/[tid].tsx (2)

130-133: ⚠️ Potential issue | 🟡 Minor

排序时不要把缺失开始时间当成 1970。

(left as string) || 0 会把空时间排到最前,进而影响 Hero badge 和倒计时选项顺序;建议复用 timeOf() 并把无效时间放到末尾。

♻️ 建议修复
 import {
   agendaTypeLabelOf,
   compactDateKeyOf,
   compactSummaryOf,
   firstTextOf,
   formatMoment,
   formatPeriod,
   isPublicForm,
   relationNameOf,
   textListOf,
+  timeOf,
   userOf,
 } from '../../../../components/Activity/Hackathon/utility';
@@
-    const agendaItems = [...agenda].sort(
-      ({ startedAt: left }, { startedAt: right }) =>
-        new Date((left as string) || 0).getTime() - new Date((right as string) || 0).getTime(),
-    );
+    const agendaItems = [...agenda].sort(({ startedAt: left }, { startedAt: right }) => {
+      const leftTime = timeOf(left);
+      const rightTime = timeOf(right);
+
+      if (!Number.isFinite(leftTime)) return 1;
+      if (!Number.isFinite(rightTime)) return -1;
+
+      return leftTime - rightTime;
+    });
🤖 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 130 - 133, The sort treats
missing startedAt as 0 (1970) which pushes empty times to the front; update the
comparator used in the agendaItems creation to call timeOf() for each startedAt
(e.g., timeOf(left) and timeOf(right)), detect invalid/NaN results and treat
them as "after all valid times" (for example by using Infinity) so items with
missing or invalid start times sort to the end; keep the rest of the spreading
and variable names (agendaItems, agenda) unchanged.

244-244: ⚠️ Potential issue | 🟡 Minor

aria-label 也需要走 i18n。

"breadcrumb" 是辅助技术可见文案,请改为翻译 key。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 at line 244, The Breadcrumb's aria-label
currently uses a hardcoded string ("breadcrumb") which must use i18n; import and
invoke the translation function (e.g., useTranslation / t) in the component and
replace aria-label="breadcrumb" with aria-label={t('breadcrumb')} (or the chosen
key like t('aria.breadcrumb')), and add the corresponding key/value to your
locale translation files so assistive tech gets the localized label; ensure the
component references the same t() used elsewhere in this file.
🤖 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/useLiveCountdownState.ts`:
- Around line 34-37: The setTimeout call in useLiveCountdownState (the timer
created with window.setTimeout calling refreshReferenceTime) can overflow for
delays > 2,147,483,647ms; clamp the computed delay before passing to setTimeout
by bounding it between 1000 and the browser 32-bit signed integer max (e.g.,
2_147_483_647 or slightly lower) so you compute delay = Math.max(1000,
Math.min(MAX_SAFE_TIMEOUT_MS, targetTime - Date.now() + 1000)) and then call
window.setTimeout(refreshReferenceTime, delay) to avoid integer overflow and
unpredictable behavior.
- Around line 2-14: The effect uses a value returned from useEffectEvent
(refreshReferenceTime) in dependency arrays which changes every render; replace
useEffectEvent with a stable useCallback: create const refreshReferenceTime =
useCallback(() => setReferenceTime(Date.now()), [setReferenceTime]) inside
useLiveCountdownState, remove refreshReferenceTime from the effect dependency
arrays (call it from an effect with an empty deps array to run on mount) and
update the timer/reset effect (the effect that currently resets the timer using
refreshReferenceTime) to use the new stable refreshReferenceTime (or call
setReferenceTime(Date.now()) directly) so the timer is not reset each render;
keep references to setReferenceTime, useLiveCountdownState,
refreshReferenceTime, and the timer effect to locate changes.

In `@pages/hackathon/`[id]/team/[tid].tsx:
- Around line 44-56: The getServerSideProps branch returns an invalid shape by
including props: {} alongside notFound: true; update both early-return checks
(the params guard and the databaseSchema validation) to return only { notFound:
true } (e.g., replace "return { notFound: true, props: {} }" with "return {
notFound: true }") and keep the existing logic around ActivityModel().getOne,
activity.databaseSchema, appId and tableIdMap unchanged.

---

Outside diff comments:
In `@pages/hackathon/`[id].tsx:
- Around line 84-91: The getServerSideProps returns are incorrectly mixing
notFound with props; update each early-return to return { notFound: true } (no
props) where you currently have patterns like "if (!params?.id) return {
notFound: true, props: {} }" and similar checks after ActivityModel().getOne
using appId/tableIdMap and RequiredTableKeys; also remove redundant non-null
assertions like params! when you already guard params?.id, and apply the same
change to the other files noted (pages/search/[model]/index.tsx,
pages/hackathon/[id]/team/[tid].tsx) so every branch returns one of props,
notFound, or redirect only.

---

Duplicate comments:
In `@pages/hackathon/`[id]/team/[tid].tsx:
- Around line 130-133: The sort treats missing startedAt as 0 (1970) which
pushes empty times to the front; update the comparator used in the agendaItems
creation to call timeOf() for each startedAt (e.g., timeOf(left) and
timeOf(right)), detect invalid/NaN results and treat them as "after all valid
times" (for example by using Infinity) so items with missing or invalid start
times sort to the end; keep the rest of the spreading and variable names
(agendaItems, agenda) unchanged.
- Line 244: The Breadcrumb's aria-label currently uses a hardcoded string
("breadcrumb") which must use i18n; import and invoke the translation function
(e.g., useTranslation / t) in the component and replace aria-label="breadcrumb"
with aria-label={t('breadcrumb')} (or the chosen key like t('aria.breadcrumb')),
and add the corresponding key/value to your locale translation files so
assistive tech gets the localized label; ensure the component references the
same t() used elsewhere in this file.
🪄 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: 5978a9b2-bfc8-4f89-a072-cfd74b3209bb

📥 Commits

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

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

Comment thread components/Activity/Hackathon/useLiveCountdownState.ts Outdated
Comment thread components/Activity/Hackathon/useLiveCountdownState.ts
Comment thread pages/hackathon/[id]/team/[tid].tsx Outdated
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: 1

🧹 Nitpick comments (2)
pages/hackathon/[id]/team/[tid].tsx (1)

38-38: { t } as typeof i18n 存在类型变宽风险。

这里把仅含 t 的对象强转为完整 typeof i18n,一旦 buildCountdownUnitLabels 未来访问 i18n.language 等其他属性会运行时踩空。pages/hackathon/[id].tsx L177 直接传入完整 i18n 是更稳的模式;建议对齐:保留 i18n 引用,传原对象而非断言。

♻️ 建议调整
-    const { t } = useContext(I18nContext);
+    const i18n = useContext(I18nContext);
+    const { t } = i18n;
@@
-            countdownUnitLabels={buildCountdownUnitLabels({ t } as typeof i18n)}
+            countdownUnitLabels={buildCountdownUnitLabels(i18n)}

同时可移除 import { i18n, I18nContext } 里的 i18n 类型导入(若不再用于断言)。

Also applies to: 94-94, 220-220

🤖 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 38, 当前将仅含 t 的对象强制断言为完整 typeof
i18n(在 import 中的 i18n 与 I18nContext)存在类型变宽风险;请改为保留并传入原始 i18n 对象而非断言(例如把传给
buildCountdownUnitLabels 的参数从 { t } as typeof i18n 改为直接传入 i18n/从 I18nContext
获取的完整对象),以避免未来访问 i18n.language 等属性时运行时为空;同时在不再使用类型断言后从 import 列表中移除未使用的 i18n
导入(保留 I18nContext 如需)。
components/Activity/Hackathon/utility.ts (1)

93-104: relationNameOffirstTextOf 的纯别名,可考虑合并。

目前 relationNameOf(value) => firstTextOf(value) 没有任何附加语义。若仅为可读性保留也可接受;否则建议直接复用 firstTextOf,减少一个出口表面。

🤖 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 93 - 104, The
relationNameOf function is just a pure alias of firstTextOf; remove the
duplicate implementation by deleting relationNameOf and update all call sites to
use firstTextOf instead; if you want to keep a readable alias, replace the
current relationNameOf implementation with a simple re-export/alias (e.g.,
relationNameOf = firstTextOf) so there is a single implementation to maintain
and no behavioral duplication — update references to relationNameOf in the
codebase to use firstTextOf or keep the alias pointing to firstTextOf.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pages/hackathon/`[id]/team/[tid].tsx:
- Around line 164-201: primaryForm can become a form without shared_url because
only publicForms was filtered for shared_url; update the selection so
forms?.Person and forms?.Project are also filtered for shared_url (e.g. filter
by isPublicForm AND f.shared_url) before falling back to publicForms[0],
ensuring primaryForm.shared_url is defined when used by heroPrimaryAction; touch
the primaryForm declaration (forms?.Person, forms?.Project, isPublicForm,
publicForms) and leave heroPrimaryAction (primaryForm.shared_url) unchanged.

---

Nitpick comments:
In `@components/Activity/Hackathon/utility.ts`:
- Around line 93-104: The relationNameOf function is just a pure alias of
firstTextOf; remove the duplicate implementation by deleting relationNameOf and
update all call sites to use firstTextOf instead; if you want to keep a readable
alias, replace the current relationNameOf implementation with a simple
re-export/alias (e.g., relationNameOf = firstTextOf) so there is a single
implementation to maintain and no behavioral duplication — update references to
relationNameOf in the codebase to use firstTextOf or keep the alias pointing to
firstTextOf.

In `@pages/hackathon/`[id]/team/[tid].tsx:
- Line 38: 当前将仅含 t 的对象强制断言为完整 typeof i18n(在 import 中的 i18n 与
I18nContext)存在类型变宽风险;请改为保留并传入原始 i18n 对象而非断言(例如把传给 buildCountdownUnitLabels 的参数从
{ t } as typeof i18n 改为直接传入 i18n/从 I18nContext 获取的完整对象),以避免未来访问 i18n.language
等属性时运行时为空;同时在不再使用类型断言后从 import 列表中移除未使用的 i18n 导入(保留 I18nContext 如需)。
🪄 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: 69c6f06d-5555-4f16-9630-7b56a08c187e

📥 Commits

Reviewing files that changed from the base of the PR and between f85c138 and 0e5e716.

⛔ Files ignored due to path filters (3)
  • 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 (5)
  • components/Activity/Hackathon/useLiveCountdownState.ts
  • components/Activity/Hackathon/utility.ts
  • components/Activity/ProductCard.tsx
  • pages/hackathon/[id].tsx
  • pages/hackathon/[id]/team/[tid].tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/Activity/Hackathon/useLiveCountdownState.ts

Comment thread pages/hackathon/[id]/team/[tid].tsx
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: 4

♻️ Duplicate comments (1)
pages/hackathon/[id]/team/[tid].tsx (1)

48-58: ⚠️ Potential issue | 🟠 Major

先规范化项目名再用于成员/作品查询。

project.name as string 仍可能把飞书富文本/关联单元格变成 "[object Object]",从而让成员和作品查询静默返回空列表。这里已经引入 firstTextOf(),建议在 SSR 查询条件中也使用同一套规范化逻辑。

🐛 建议修复
     const project = await new ProjectModel(appId, tableIdMap.Project).getOne(params!.tid);
+    const projectName = firstTextOf(project.name);
+
+    if (!projectName) return { notFound: true };
 
     // Get approved members for this project
     const [members, products] = await Promise.all([
       new MemberModel(appId, tableIdMap.Member).getAll({
-        project: project.name as string,
+        project: projectName,
         status: 'approved',
       }),
       new ProductModel(appId, tableIdMap.Product).getAll({
-        project: project.name as string,
+        project: projectName,
       }),
     ]);
#!/bin/bash
# Description: Inspect the Project model type and current server-side query normalization.
rg -n -C4 '\b(export\s+(interface|type)\s+Project|class\s+ProjectModel|name\s*:)' models
rg -n -C3 'project:\s*project\.name as string|firstTextOf\(project\.name\)' 'pages/hackathon/[id]/team/[tid].tsx'
🤖 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 48 - 58, The SSR code uses
project.name as string when querying MemberModel.getAll and ProductModel.getAll
which can produce "[object Object]"; update the query keys to use the normalizer
firstTextOf(project.name) instead of project.name as string so both MemberModel
and ProductModel receive the canonical project name; locate the ProjectModel
instantiation and the Promise.all block in pages/hackathon/[id]/team/[tid].tsx
and replace the two occurrences (used in MemberModel.getAll and
ProductModel.getAll) with firstTextOf(project.name) to ensure consistent
server-side normalization.
🤖 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 143-152: The countdown target currently uses firstTextOf(...)
which can produce a raw numeric string like "1767225600000" that Hero.tsx fails
to parse; update resolveCountdownState logic (variables: nextStartedAt,
nextCountdownTarget, fallbackCountdownTarget, countdownTo) so that when
nextCountdownTarget or fallbackCountdownTarget is a timestamp or Date you
convert it into a normalized ISO 8601 string (e.g., via new
Date(...).toISOString()) or a Date object before assigning countdownTo, rather
than relying on firstTextOf for time values, ensuring Hero.tsx can reliably
construct new Date(countdownTo).getTime().

In `@pages/hackathon/`[id]/team/[tid].tsx:
- Around line 108-112: Replace hardcoded fallbacks ('-' and '--') used in
locationText, groupName, scoreText, rankText, prizeText with the project's i18n
key (e.g. t('not_available') or the common fallback key) so all user-facing
placeholder text goes through the translation function; ensure the component
obtains the t function (useTranslation/useI18n already used in this file) and
update the other occurrences noted (around the other ranges) similarly so no
literal '-'/'--' remain in user-visible strings.
- Around line 216-230: The two CTA anchors in the heroActionRow should be
replaced with the imported React Bootstrap Button used as links: swap the first
<a> that uses
heroPrimaryAction.href/heroPrimaryAction.external/heroPrimaryAction.label for a
<Button as="a"> preserving href, conditionally adding target="_blank" and
rel="noreferrer" when heroPrimaryAction.external is true, and keep
className={styles.primaryAction}; likewise replace the second <a> linking to
"#works" with <Button as="a" href="#works"
className={styles.secondaryAction}>{t('team_works')}</Button>; preserve all
existing attributes and styling while removing the raw <a> elements.
- Line 6: Add the React Bootstrap Nav import and replace the raw <nav> and bare
<a> elements with Nav components: import Nav from 'react-bootstrap' (i.e.,
include Nav in the existing import list) and change the navigation block
currently rendering multiple <a> tags (around the code that builds the
breadcrumb-like links) to use Nav as="ul" with Nav.Item as="li" and Nav.Link for
each link so the structure becomes a semantic list built from Nav, Nav.Item, and
Nav.Link instead of raw anchors.

---

Duplicate comments:
In `@pages/hackathon/`[id]/team/[tid].tsx:
- Around line 48-58: The SSR code uses project.name as string when querying
MemberModel.getAll and ProductModel.getAll which can produce "[object Object]";
update the query keys to use the normalizer firstTextOf(project.name) instead of
project.name as string so both MemberModel and ProductModel receive the
canonical project name; locate the ProjectModel instantiation and the
Promise.all block in pages/hackathon/[id]/team/[tid].tsx and replace the two
occurrences (used in MemberModel.getAll and ProductModel.getAll) with
firstTextOf(project.name) to ensure consistent server-side normalization.
🪄 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: 346ecac5-1784-43b0-b0e0-835aa3ddcb11

📥 Commits

Reviewing files that changed from the base of the PR and between 0e5e716 and 00f8e86.

⛔ Files ignored due to path filters (1)
  • styles/HackathonTeam.module.less is excluded by none and included by none
📒 Files selected for processing (3)
  • components/Activity/Hackathon/utility.ts
  • pages/hackathon/[id].tsx
  • pages/hackathon/[id]/team/[tid].tsx

Comment thread components/Activity/Hackathon/utility.ts Outdated
Comment thread pages/hackathon/[id]/team/[tid].tsx Outdated
Comment thread pages/hackathon/[id]/team/[tid].tsx
Comment thread pages/hackathon/[id]/team/[tid].tsx
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