Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough重构与功能调整:ProductCard 条件化渲染 createdAt;新增时间归一化与倒计时工具(timeOf / resolveCountdownState)及实时钩子 useLiveCountdownState;将页面内联倒计时逻辑抽离;Hackathon 活动页与团队页重构为基于 HackathonHero 的锚点式布局并统一文本/表单/议程处理(t() 假定已使用)。 Changes
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 显示
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 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 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
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
There was a problem hiding this comment.
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返回值应为props、notFound、redirect三选一。当前代码在多处混用{ 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.tsx、pages/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
📒 Files selected for processing (5)
components/Activity/Hackathon/constant.tscomponents/Activity/Hackathon/useLiveCountdownState.tscomponents/Activity/Hackathon/utility.tspages/hackathon/[id].tsxpages/hackathon/[id]/team/[tid].tsx
There was a problem hiding this comment.
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].tsxL177 直接传入完整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:relationNameOf是firstTextOf的纯别名,可考虑合并。目前
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
⛔ Files ignored due to path filters (3)
translation/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 (5)
components/Activity/Hackathon/useLiveCountdownState.tscomponents/Activity/Hackathon/utility.tscomponents/Activity/ProductCard.tsxpages/hackathon/[id].tsxpages/hackathon/[id]/team/[tid].tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- components/Activity/Hackathon/useLiveCountdownState.ts
Signed-off-by: luojiyin <luojiyin@hotmail.com>
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
styles/HackathonTeam.module.lessis excluded by none and included by none
📒 Files selected for processing (3)
components/Activity/Hackathon/utility.tspages/hackathon/[id].tsxpages/hackathon/[id]/team/[tid].tsx
Checklist(清单):
Closes #64
Summary by CodeRabbit
发布说明
错误修复
功能改进
新增
重构