Conversation
📝 WalkthroughWalkthrough重构与功能调整:ProductCard 条件化渲染 createdAt;引入服务端参考时间(agendaReferenceTime / renderedAt)并将倒计时/下一议程决策移入通用 util;Hackathon 团队页重构为基于 HackathonHero 的分节锚点布局,新增议程拉取与多项数据标准化辅助函数与计时解析。 Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 分钟 Possibly related PRs
Suggested labels
Suggested reviewers
浏览优化黑客松项目详情页组件结构,增强服务端数据获取机制。 变更
预期代码审查工作量(快速参考)🎯 4 (Complex) | ⏱️ ~60 分钟 相关 PR
建议标签
建议审查者
诗句
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 string、link as string、summary as string、sourceLink 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(Container、Row、Col、Card等),建议统一改用 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
⛔ Files ignored due to path filters (6)
README.mdis excluded by!**/*.mdand included by nonepackage.jsonis excluded by none and included by nonestyles/HackathonTeam.module.lessis excluded by none and included by nonetranslation/en-US.tsis excluded by none and included by nonetranslation/zh-CN.tsis excluded by none and included by nonetranslation/zh-TW.tsis excluded by none and included by none
📒 Files selected for processing (3)
components/Activity/ProductCard.tsxpages/hackathon/[id].tsxpages/hackathon/[id]/team/[tid].tsx
| 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]; |
There was a problem hiding this comment.
评分表单也应使用公开表单,并且不应依赖已有分数。
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).
| <article className={styles.introPanel}> | ||
| <Breadcrumb aria-label="breadcrumb" className={styles.breadcrumb}> |
There was a problem hiding this comment.
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.
| <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.
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
components/Activity/Hackathon/Hero.tsxcomponents/Activity/Hackathon/constant.tscomponents/Activity/Hackathon/utility.tspages/hackathon/[id].tsxpages/hackathon/[id]/team/[tid].tsx
| export const timeOf = (value?: TableCellValue) => { | ||
| const time = new Date((value as string) || 0).getTime(); | ||
|
|
||
| return Number.isFinite(time) ? time : NaN; | ||
| }; |
There was a problem hiding this comment.
不要把空时间解析成 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。
| export const getServerSideProps = compose<Record<'id' | 'tid', string>>( | ||
| cache(), |
There was a problem hiding this comment.
🧩 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' pagesRepository: 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 -20Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io
Length of output: 118
🏁 Script executed:
# 查看 resolveCountdownState 的完整实现
rg -n "resolveCountdownState" -A 15 components/Activity/Hackathon/utilityRepository: Open-Source-Bazaar/Open-Source-Bazaar.github.io
Length of output: 168
🏁 Script executed:
# 找到所有 utility 相关文件
fd -name "utility" --type fRepository: 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 -20Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io
Length of output: 590
🏁 Script executed:
# 找到 cache 函数的定义
rg "cache\s*=" -A 10 | head -50Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io
Length of output: 1490
🏁 Script executed:
# 查找 utility 文件
fd "utility" -type fRepository: Open-Source-Bazaar/Open-Source-Bazaar.github.io
Length of output: 261
🏁 Script executed:
# 查找 resolveCountdownState 函数的完整实现
rg "resolveCountdownState" -A 20Repository: 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 -20Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io
Length of output: 1070
🏁 Script executed:
# 查看 utils 或 middleware 中的 cache 函数
fd "cache" -type fRepository: Open-Source-Bazaar/Open-Source-Bazaar.github.io
Length of output: 261
🏁 Script executed:
# 在所有 ts/tsx 文件中查找 cache 的导入
rg "from.*cache|import.*cache" | head -20Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io
Length of output: 1026
🏁 Script executed:
# 查看 next-ssr-middleware 包的文档或配置
rg "next-ssr-middleware" -A 5 | head -40Repository: 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 -100Repository: 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 -50Repository: 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() 来计算 nextAgendaItem 和 countdownTo,导致议程阶段显示不准确或过期。
同样问题也出现在 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.
Checklist(清单):
Closes #64
Summary by CodeRabbit
发布说明
错误修复
功能改进
重构