Skip to content

[fix] Activity lists with User Activity model#36

Open
TechQuery wants to merge 6 commits intomainfrom
connect-new-API
Open

[fix] Activity lists with User Activity model#36
TechQuery wants to merge 6 commits intomainfrom
connect-new-API

Conversation

@TechQuery
Copy link
Copy Markdown
Member

@TechQuery TechQuery commented May 3, 2026

PR-36 PR-36 PR-36 Powered by Pull Request Badge

Summary by CodeRabbit

  • Chores

    • Enhanced security linting to warn on unsafe HTML usage.
  • Style

    • Refined homepage statistics list styling.
  • New Features

    • Added "Fill with default questions" button in questionnaire editor with translations.
    • Added a default "LinkedIn or CV" question option.
  • Bug Fixes / Improvements

    • Reorganized activity page tabs and detail rendering for consistent display.
    • Enrollment creation now uses the correct request method.
    • User activity tabs load with per-tab data handling for more reliable lists.
    • Award selection and labeling guarded for missing targets.

Co-authored-by: Copilot <copilot@github.com>
Copilot AI review requested due to automatic review settings May 3, 2026 23:09
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

📝 Walkthrough

Walkthrough

Introduces a MobX-backed UserActivityModel, lets ActivityList accept an optional store, changes ActivityModel.signOne to POST enrollments, migrates several local types to type-only imports from @freecodecamp-chengdu/hop-service, adds a "Fill with default questions" feature (with translations and UI), adjusts award/evaluation wiring, and enables a Biome lint rule warning for dangerouslySetInnerHTML.

Changes

User Activity Model & ActivityList wiring

Layer / File(s) Summary
Data Shape / Imports
models/Activity/index.ts, models/User/index.ts
Adds buildURLData import; getUserTopList() now uses 'activity-log/user-rank' literal.
Core Model
models/Activity/index.ts
Adds exported UserActivityModel extends ActivityModel with constructor(userId, role) and loadPage(pageIndex, pageSize, filter); changes ActivityModel.signOne to POST to ${baseURI}/${name}/enrollment.
Component Props / Wiring
components/Activity/ActivityList.tsx, pages/user/[id].tsx
ActivityListLayoutProps gains store?: ActivityModel; ActivityList.store initializes from `props.store
Page Markup / Minor UX
pages/activity/[name]/index.tsx, pages/index.tsx
Moves "award" and conditional "team-rank" tabs into <Tabs>; detail content wrapped with <article dangerouslySetInnerHTML={...} />; homepage hero <ul> adds list-unstyled.
Config / Linting
biome.json
Enables linter.rules.security.noDangerouslySetInnerHtml at warn severity.

External types, awards, evaluation, questionnaire defaults

Layer / File(s) Summary
Types / Imports
models/Activity/Award.ts, models/Activity/Question.ts, models/Activity/Team.ts, many components
Replaces several local runtime type declarations with type-only imports from @freecodecamp-chengdu/hop-service (Award, AwardAssignment, Question, QuestionType, Answer, and related types).
Model Cleanup
models/Activity/Award.ts, models/Activity/Team.ts
Removes local Award/AwardAssignment interfaces and relies on external types; removes redundant runtime imports.
Award UI & Evaluation Flow
components/Activity/AwardList.tsx, components/Team/TeamAwardAssignment.tsx, pages/activity/[name]/manage/evaluation.tsx
Guards Award target rendering (... ?? ''), simplifies map destructuring in team award assignment, renames form field awardIdaward and changes submit flow to fetch award and call assignmentStore.updateOne({ team: store.currentOne }) instead of assigneeId payload.
Questionnaire defaults & wiring
models/Activity/Question.ts, pages/activity/[name]/manage/questionnaire.tsx, components/Activity/Questionnaire*
Adds a new question linkein_or_cv; imports questions helper; adds fillWithDefaults() method to ActivityQuestionnaireEditor and a button labeled fill_default_questions to populate defaults; several components switch to type-only imports for Question/QuestionType.
Pages / Registration
pages/activity/[name]/register.tsx
Switches Answer/Question imports to type-only, adds QuestionnaireForm, PageHead, and activity store imports.
Translations
translation/en-US.ts, translation/zh-CN.ts, translation/zh-TW.ts
Adds fill_default_questions translation key in EN/ZN-CN/ZN-TW.

Sequence Diagram

sequenceDiagram
    participant UP as User Page
    participant UEM as UserActivityModel
    participant AL as ActivityList
    participant API as Activity Service

    UP->>UEM: instantiate new UserActivityModel(userId, role)
    UP->>AL: render ActivityList with store={UEM}
    AL->>AL: initialize store = props.store || new ActivityModel()
    AL->>UEM: request loadPage(pageIndex, pageSize, filter)
    UEM->>API: GET /... (buildURLData user/role paginated)
    API-->>UEM: returns { list, count }
    UEM-->>AL: provides page data
    AL->>AL: render activities
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

feature

Poem

🐰 I hopped through models, left seeds to sow,
Role-based lists now fetch and grow,
Awards and questions tidied with care,
Lint whispers warnings for HTML there,
A little rabbit cheers the code's new glow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[fix] Activity lists with User Activity model' is partially related to the changeset. It references the UserActivityModel class addition and the ActivityList component modifications, which are significant changes in the PR. However, the changeset is substantially broader, encompassing major refactoring work including migration of Award and Question types from local models to the hop-service package, updates to security linting rules, and addition of a survey question fill-with-defaults feature. The title emphasizes only one aspect (User Activity model) rather than capturing the main intent of aligning local models with hop-service types as described in the PR objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch connect-new-API

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

❤️ Share

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates profile activity lists to use a dedicated UserActivityModel, aiming to align the user page with role-based activity endpoints while also cleaning up a few UI and lint-related details elsewhere in the app.

Changes:

  • Add UserActivityModel and switch the user profile tabs to load activities through role-specific stores.
  • Update ActivityList so callers can inject a custom store instead of always using its internal ActivityModel.
  • Apply small markup/style cleanups on the home page, activity detail page, and Biome config.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pages/user/[id].tsx Rewires profile activity tabs to use UserActivityModel instances for enrollee/creator/staff roles.
pages/index.tsx Minor import reordering and list styling cleanup for the homepage stats section.
pages/activity/[name]/index.tsx Cleans up tab markup and keeps award/rank tabs inside the main Tabs container.
models/User/index.ts Formatting-only change in the top-user ranking request.
models/Activity/index.ts Adds UserActivityModel and changes the enrollment submission request method.
components/Activity/ActivityList.tsx Adds optional injected-store support for activity lists.
biome.json Enables a Biome warning for dangerouslySetInnerHTML.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +53 to 54
store = this.props.store || new ActivityModel();

Comment thread models/Activity/index.ts
@toggle('uploading')
async signOne(name: string, form: Enrollment['form'] = []) {
await this.client.put(`${this.baseURI}/${name}/enrollment`, { form });
await this.client.post(`${this.baseURI}/${name}/enrollment`, { form });
Comment thread pages/user/[id].tsx
Comment on lines 161 to +165
<Tab.Pane eventKey="created">
<ActivityList type="created" userId={id} />
<ActivityList store={new UserActivityModel(id, 'creator')} />
</Tab.Pane>
<Tab.Pane eventKey="admin">
<ActivityList type="admin" userId={id} />
<ActivityList store={new UserActivityModel(id, 'staff')} />
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
models/User/index.ts (1)

13-13: Add a clarifying comment for the cross-service endpoint.

The endpoint 'activity-log/user-rank' is a legitimate cross-service call to the activity-log service (not the user service). Consider adding an inline comment explaining why this endpoint doesn't follow the baseURI pattern:

// User rankings are managed by the activity-log service, not the user service
const { body } = await this.client.get<UserRankListChunk>('activity-log/user-rank');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/User/index.ts` at line 13, Add an inline clarifying comment
immediately above the call to
this.client.get<UserRankListChunk>('activity-log/user-rank') in
models/User/index.ts explaining that 'activity-log/user-rank' is a cross-service
endpoint owned by the activity-log service (not the user service) and therefore
intentionally does not use the local baseURI pattern; place the comment next to
the call that references the UserRankListChunk type so future readers understand
why this endpoint differs from other user service calls.
biome.json (1)

76-76: ⚡ Quick win

Prefer "error" + a targeted biome-ignore comment over a global "warn" downgrade.

Setting noDangerouslySetInnerHtml to "warn" globally means future unsafe usages anywhere in the codebase will silently pass CI. The idiomatic approach is to keep the rule at "error" and suppress it only at the one intentional callsite.

♻️ Proposed refactor
- "noDangerouslySetInnerHtml": "warn"
+ "noDangerouslySetInnerHtml": "error"

Then at the callsite in pages/activity/[name]/index.tsx:

- <article dangerouslySetInnerHTML={{ __html: detail }} />
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: detail is admin-authored HTML, sanitized server-side */}
+ <article dangerouslySetInnerHTML={{ __html: detail ?? '' }} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@biome.json` at line 76, The rule "noDangerouslySetInnerHtml" was globally
downgraded to "warn"; revert it to "error" in biome.json and instead add a
targeted suppression at the exact intentional callsite (e.g., the usage in
pages/activity/[name]/index.tsx) using a biome-ignore comment for that rule;
locate the unsafe usage (search for dangerouslySetInnerHTML in
pages/activity/[name]/index.tsx) and add a single-line biome-ignore for
noDangerouslySetInnerHtml immediately above that line so the rest of the
codebase remains enforced as an error.
components/Activity/ActivityList.tsx (1)

52-53: ⚡ Quick win

store is captured once at construction — prop updates after mount are silently ignored

store = this.props.store || new ActivityModel() is a class field initializer: it runs exactly once when ActivityList is mounted. If UserDetailPage (an observer component) re-renders and passes a fresh UserActivityModel instance, this.store retains the original value. PureComponent's shallow diff sees a new store prop reference and calls render(), but render() reads this.store, not this.props.store.

This is not a bug when callers memoize the store (see the useMemo suggestion in pages/user/[id].tsx), but a comment or a componentDidUpdate guard would prevent accidental misuse in the future:

♻️ Option A — guard against unintentional prop drift (minimal change)
 export default class ActivityList extends PureComponent<ActivityListProps> {
   store = this.props.store || new ActivityModel();
+
+  // `store` is initialised once at mount; pass a stable reference (useMemo) from the parent.
+  // Replacing the store after mount is not supported without remounting (use a `key` prop).
♻️ Option B — honour prop changes reactively
+  componentDidUpdate(prevProps: ActivityListProps) {
+    if (prevProps.store !== this.props.store && this.props.store) {
+      this.store = this.props.store;
+      this.forceUpdate();
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Activity/ActivityList.tsx` around lines 52 - 53, ActivityList
currently captures store once via the class field "store = this.props.store ||
new ActivityModel()", so later updates to props.store are ignored; update the
component to either (A) guard against prop drift by implementing
componentDidUpdate(prevProps) to detect a changed props.store and replace
this.store (and optionally warn) or (B) honor prop changes by removing the
cached this.store and always read from this.props.store (falling back to
creating a new ActivityModel when undefined) in render; reference ActivityList,
the store field, ActivityModel, props.store, and componentDidUpdate when making
the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@models/Activity/index.ts`:
- Around line 169-175: The subclass UserActivityModel is inheriting
ActivityModel's restored field (which runs restore(this, 'Activity') during
super()), causing all instances to share the singleton's localStorage key; to
fix, override the restored field in UserActivityModel to opt out of persistence
by adding a class field like restored = false (so after super() runs the
subclass resets it), ensuring UserActivityModel instances do not call
restore(this, 'Activity') and therefore don't overwrite the singleton
cache—update UserActivityModel (constructor/class body) to define restored =
false and leave ActivityModel.restore logic unchanged.

In `@pages/activity/`[name]/index.tsx:
- Around line 280-282: The Tab rendering currently injects unsanitized HTML via
dangerouslySetInnerHTML using the variable detail; sanitize detail before
rendering and guard against undefined. Install and import DOMPurify (or use
server-side sanitize in getServerSideProps), then when rendering in the
component that contains <Tab ...> and <article dangerouslySetInnerHTML={{
__html: detail }} />, replace detail with a sanitized value (e.g.,
sanitizedDetail = DOMPurify.sanitize(detail || '') ), ensuring DOMPurify is only
invoked in the browser (use the existing isServer() guard) or perform
sanitization inside getServerSideProps and pass the safe string down to the
component.

In `@pages/user/`[id].tsx:
- Around line 157-166: UserDetailPage currently constructs three new
UserActivityModel instances inline in JSX (new UserActivityModel(id, 'enrollee')
/ 'creator' / 'staff'), causing recreation on every observer-triggered re-render
(and redundant restore(...) calls) and also being ignored by mounted
ActivityList components; change to memoize or reuse one model instance per (id,
role) — e.g., create/get models from a stable cache or use React.useMemo keyed
on id and role and pass those cached instances into ActivityList instead of
constructing inline; update references around UserDetailPage, UserActivityModel,
and ActivityList (and remove inline new ... in Tab.Pane) and ensure the
PublicUser/User.id type matches the UserActivityModel constructor parameter
userId:number.

---

Nitpick comments:
In `@biome.json`:
- Line 76: The rule "noDangerouslySetInnerHtml" was globally downgraded to
"warn"; revert it to "error" in biome.json and instead add a targeted
suppression at the exact intentional callsite (e.g., the usage in
pages/activity/[name]/index.tsx) using a biome-ignore comment for that rule;
locate the unsafe usage (search for dangerouslySetInnerHTML in
pages/activity/[name]/index.tsx) and add a single-line biome-ignore for
noDangerouslySetInnerHtml immediately above that line so the rest of the
codebase remains enforced as an error.

In `@components/Activity/ActivityList.tsx`:
- Around line 52-53: ActivityList currently captures store once via the class
field "store = this.props.store || new ActivityModel()", so later updates to
props.store are ignored; update the component to either (A) guard against prop
drift by implementing componentDidUpdate(prevProps) to detect a changed
props.store and replace this.store (and optionally warn) or (B) honor prop
changes by removing the cached this.store and always read from this.props.store
(falling back to creating a new ActivityModel when undefined) in render;
reference ActivityList, the store field, ActivityModel, props.store, and
componentDidUpdate when making the change.

In `@models/User/index.ts`:
- Line 13: Add an inline clarifying comment immediately above the call to
this.client.get<UserRankListChunk>('activity-log/user-rank') in
models/User/index.ts explaining that 'activity-log/user-rank' is a cross-service
endpoint owned by the activity-log service (not the user service) and therefore
intentionally does not use the local baseURI pattern; place the comment next to
the call that references the UserRankListChunk type so future readers understand
why this endpoint differs from other user service calls.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e85857b7-d29d-4360-ae02-e3138514c743

📥 Commits

Reviewing files that changed from the base of the PR and between 742d0dc and 28c643c.

📒 Files selected for processing (7)
  • biome.json
  • components/Activity/ActivityList.tsx
  • models/Activity/index.ts
  • models/User/index.ts
  • pages/activity/[name]/index.tsx
  • pages/index.tsx
  • pages/user/[id].tsx

Comment thread models/Activity/index.ts
Comment on lines +169 to +175
export class UserActivityModel extends ActivityModel {
constructor(
public userId: number,
public role: 'creator' | 'staff' | 'enrollee',
) {
super();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

UserActivityModel inherits restore(this, 'Activity') — all instances share the same localStorage key as the singleton

ActivityModel line 37 defines restored = !isServer() && restore(this, 'Activity') as a class field initializer. Because UserActivityModel calls super(), that initializer runs for every instance, binding each one to the same 'Activity' localStorage key used by the export default new ActivityModel() singleton (line 185).

Consequences:

  • When UserActivityModel.loadPage stores its paginated result, mobx-restful's restore reaction writes to 'Activity', overwriting the singleton's cache.
  • Any singleton read that later triggers a restore reload (e.g., navigation back to the main activity list) may re-hydrate with the stale user-filtered list.
  • Multiple UserActivityModel instances (enrollee / creator / staff) overwrite each other on the same key.

Override restored in the subclass to opt out of persistence:

🛡️ Proposed fix
 export class UserActivityModel extends ActivityModel {
+  override restored = false as const;
+
   constructor(
     public userId: number,
     public role: 'creator' | 'staff' | 'enrollee',
   ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@models/Activity/index.ts` around lines 169 - 175, The subclass
UserActivityModel is inheriting ActivityModel's restored field (which runs
restore(this, 'Activity') during super()), causing all instances to share the
singleton's localStorage key; to fix, override the restored field in
UserActivityModel to opt out of persistence by adding a class field like
restored = false (so after super() runs the subclass resets it), ensuring
UserActivityModel instances do not call restore(this, 'Activity') and therefore
don't overwrite the singleton cache—update UserActivityModel (constructor/class
body) to define restored = false and leave ActivityModel.restore logic
unchanged.

Comment on lines +280 to 282
<Tab className="pt-2" eventKey="detail" title={t('hackathon_detail')}>
<article dangerouslySetInnerHTML={{ __html: detail }} />
</Tab>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sanitize detail HTML before rendering to prevent XSS.

detail is injected verbatim into the DOM via dangerouslySetInnerHTML. If this field is admin- or user-authored (rich-text editor output from the API), any embedded <script>, inline event handlers, or javascript: URIs will execute in the viewer's browser. The static analysis tool (CWE-79) flagged this as an unsanitized injection.

Additionally, detail may be undefined for hackathons that haven't populated this field, which should be guarded against.

The recommended fix is to sanitize on the client with DOMPurify:

🛡️ Proposed fix

Install DOMPurify:

pnpm add dompurify
pnpm add -D `@types/dompurify`

Apply at the callsite:

+ import DOMPurify from 'dompurify';

- <article dangerouslySetInnerHTML={{ __html: detail }} />
+ <article dangerouslySetInnerHTML={{ __html: isServer() ? '' : DOMPurify.sanitize(detail ?? '') }} />

Note: DOMPurify requires a DOM environment, so you must guard with isServer() (already imported) or use the SSR build (dompurify/dist/purify.cjs with a JSDOM window shim). Alternatively, sanitize the HTML on the server before returning it from getServerSideProps using sanitize-html or DOMPurify with JSDOM.

🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 280-280: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

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

In `@pages/activity/`[name]/index.tsx around lines 280 - 282, The Tab rendering
currently injects unsanitized HTML via dangerouslySetInnerHTML using the
variable detail; sanitize detail before rendering and guard against undefined.
Install and import DOMPurify (or use server-side sanitize in
getServerSideProps), then when rendering in the component that contains <Tab
...> and <article dangerouslySetInnerHTML={{ __html: detail }} />, replace
detail with a sanitized value (e.g., sanitizedDetail = DOMPurify.sanitize(detail
|| '') ), ensuring DOMPurify is only invoked in the browser (use the existing
isServer() guard) or perform sanitization inside getServerSideProps and pass the
safe string down to the component.

Comment thread pages/user/[id].tsx
Comment on lines 157 to 166
<Tab.Content>
<Tab.Pane eventKey="enrolled">
<ActivityList type="enrolled" userId={id} />
<ActivityList store={new UserActivityModel(id, 'enrollee')} />
</Tab.Pane>
<Tab.Pane eventKey="created">
<ActivityList type="created" userId={id} />
<ActivityList store={new UserActivityModel(id, 'creator')} />
</Tab.Pane>
<Tab.Pane eventKey="admin">
<ActivityList type="admin" userId={id} />
<ActivityList store={new UserActivityModel(id, 'staff')} />
</Tab.Pane>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the type of User.id from the hop-service package
echo "=== User interface from hop-service ==="
fd -i "index.d.ts" node_modules/@freecodecamp-chengdu/hop-service --exec grep -n "id" {}

echo ""
echo "=== User type in codebase ==="
rg -n --type ts "interface User|type User" -C 5

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 804


🏁 Script executed:

# Find the pages directory and the specific file
fd "user.*\[id\]" pages/
# Also look for pages/user directory
ls -la pages/user/ 2>/dev/null || echo "pages/user not found"
# Check if it's TypeScript or JavaScript
fd "\[id\]" pages/

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 408


🏁 Script executed:

# Find UserActivityModel definition
rg -n "class UserActivityModel|export.*UserActivityModel" --type ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 152


🏁 Script executed:

# Find ActivityList component and check if it's PureComponent
rg -n "class ActivityList|const ActivityList|export.*ActivityList" --type ts --type tsx

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 96


🏁 Script executed:

# Check the User type definition in codebase
rg -n "^export.*interface User|^export.*type User" --type ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 142


🏁 Script executed:

# Get the full pages/user/[id].tsx file or at least lines around 157-166
fd -e tsx -e ts "user" pages/ | head -10

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 50


🏁 Script executed:

# Read pages/user/[id].tsx to verify the problematic code
cat -n pages/user/[id].tsx

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 7780


🏁 Script executed:

# Find ActivityList definition
rg -n "ActivityList" --type ts -A 3

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 5468


🏁 Script executed:

# Find UserActivityModel definition and constructor
cat -n models/Activity/index.ts | head -200

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 6694


🏁 Script executed:

# Check for PureComponent in ActivityList
rg -n "PureComponent|extends.*Component" models/Activity/ --type ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 50


🏁 Script executed:

# Check User import and type
rg -n "import.*User.*from.*hop-service" --type ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 904


new UserActivityModel(...) inline in JSX creates new instances on every re-render

UserDetailPage is wrapped with observer(), so it re-renders whenever any MobX observable it reads changes (e.g., sessionStore.user loading after login). Each render creates three new UserActivityModel instances. Because each instance inherits restore(this, 'Activity'), every re-render triggers three redundant localStorage operations.

Additionally, ActivityList (a PureComponent) stores the store prop in a class field at construction time, so any ActivityList that has already mounted will silently ignore replacement instances passed on subsequent renders.

Memoize the models keyed on id:

🛠️ Proposed fix
-import { FC, useContext, useEffect, useState } from 'react';
+import { FC, useMemo, useContext, useEffect, useState } from 'react';

 const UserDetailPage: FC<PublicUser> = observer(({ id, name, avatar }) => {
   const { t } = useContext(I18nContext);
   const { user } = sessionStore;
   const [isOwner, setIsOwner] = useState(false);
+
+  const enrolleeStore = useMemo(() => new UserActivityModel(id, 'enrollee'), [id]);
+  const creatorStore = useMemo(() => new UserActivityModel(id, 'creator'), [id]);
+  const staffStore = useMemo(() => new UserActivityModel(id, 'staff'), [id]);
 
   // ...
 
                     <Tab.Pane eventKey="enrolled">
-                      <ActivityList store={new UserActivityModel(id, 'enrollee')} />
+                      <ActivityList store={enrolleeStore} />
                     </Tab.Pane>
                     <Tab.Pane eventKey="created">
-                      <ActivityList store={new UserActivityModel(id, 'creator')} />
+                      <ActivityList store={creatorStore} />
                     </Tab.Pane>
                     <Tab.Pane eventKey="admin">
-                      <ActivityList store={new UserActivityModel(id, 'staff')} />
+                      <ActivityList store={staffStore} />
                     </Tab.Pane>

Also verify that User.id (the type of id in PublicUser) matches the userId: number constructor parameter of UserActivityModel.

📝 Committable suggestion

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

Suggested change
<Tab.Content>
<Tab.Pane eventKey="enrolled">
<ActivityList type="enrolled" userId={id} />
<ActivityList store={new UserActivityModel(id, 'enrollee')} />
</Tab.Pane>
<Tab.Pane eventKey="created">
<ActivityList type="created" userId={id} />
<ActivityList store={new UserActivityModel(id, 'creator')} />
</Tab.Pane>
<Tab.Pane eventKey="admin">
<ActivityList type="admin" userId={id} />
<ActivityList store={new UserActivityModel(id, 'staff')} />
</Tab.Pane>
<Tab.Content>
<Tab.Pane eventKey="enrolled">
<ActivityList store={enrolleeStore} />
</Tab.Pane>
<Tab.Pane eventKey="created">
<ActivityList store={creatorStore} />
</Tab.Pane>
<Tab.Pane eventKey="admin">
<ActivityList store={staffStore} />
</Tab.Pane>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/user/`[id].tsx around lines 157 - 166, UserDetailPage currently
constructs three new UserActivityModel instances inline in JSX (new
UserActivityModel(id, 'enrollee') / 'creator' / 'staff'), causing recreation on
every observer-triggered re-render (and redundant restore(...) calls) and also
being ignored by mounted ActivityList components; change to memoize or reuse one
model instance per (id, role) — e.g., create/get models from a stable cache or
use React.useMemo keyed on id and role and pass those cached instances into
ActivityList instead of constructing inline; update references around
UserDetailPage, UserActivityModel, and ActivityList (and remove inline new ...
in Tab.Pane) and ensure the PublicUser/User.id type matches the
UserActivityModel constructor parameter userId:number.

@TechQuery
Copy link
Copy Markdown
Member Author

@copilotAward 替换为 hop-service 包里的同名类型,再将整个项目中引用它的地方修改为适配新类型的代码:

import { User, Base, Media, Team } from '@freecodecamp-chengdu/hop-service';
import { ListModel, Stream, toggle } from 'mobx-restful';
import { createListStream, InputData, TableModel } from '../Base';
import sessionStore from '../User/Session';
export interface Award extends Record<'hackathonName' | 'name' | 'description', string>, Base {
quantity: number;
target: 'team' | 'individual';
pictures: Media[];
}

Copy link
Copy Markdown

Copilot AI commented May 5, 2026

@copilot 将 Award 替换为 hop-service 包里的同名类型,再将整个项目中引用它的地方修改为适配新类型的代码:
https://github.com/FreeCodeCamp-Chengdu/HOP/blob/742d0d...

Done in commit d292915. Here's a summary of the changes made:

  • models/Activity/Award.ts: Removed the local Award and AwardAssignment interface definitions; now imports and re-exports Award, AwardAssignment, and AwardTarget directly from @freecodecamp-chengdu/hop-service.

  • components/Activity/AwardList.tsx: Updated AwardTargetName to use AwardTarget enum keys (AwardTarget.Individual, AwardTarget.Team); added safe fallback for optional target field (target ? (AwardTargetName(i18n)[target] ?? '') : '').

  • components/Team/TeamAwardAssignment.tsx: Removed description and updatedAt from destructuring — these are no longer part of the AwardAssignment type from hop-service.

  • pages/activity/[name]/manage/evaluation.tsx: Replaced custom awardId/assigneeId form fields with the AwardAssignment shape from hop-service ({ award } form field, { team: store.currentOne } assignment payload, and target === AwardTarget.Team comparison).

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
pages/activity/[name]/manage/evaluation.tsx (1)

49-66: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix misleading number type assertion on string form value.

In the submit handler, formToJSON<{ award: number }>(form) is a type assertion that lies to TypeScript. The formToJSON utility returns raw DOM input values, which are always strings—not coerced to numbers. At runtime, award will be a string, creating a type mismatch if Award['id'] is actually typed as number in the service.

Either coerce explicitly to the expected type or align the type annotation with the actual string:

Suggested fix
-    const { awardStore, store } = this,
-      form = event.currentTarget,
-      { award: awardId } = formToJSON<{ award: number }>(form);
+    const { awardStore, store } = this,
+      form = event.currentTarget,
+      { award } = formToJSON<{ award: string }>(form),
+      awardId = Number(award);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pages/activity/`[name]/manage/evaluation.tsx around lines 49 - 66, The
handler handleSubmit currently asserts formToJSON<{ award: number }>(form) but
form inputs are strings; change this to parse/coerce the award value before
using it: read award as a string from formToJSON (e.g., formToJSON<{ award:
string }>(form) or let raw = formToJSON(form)), then convert award to the
correct type (use Number(award) or parseInt(award, 10)) and store that result in
awardId before calling awardStore.getOne(awardId) and
awardStore.assignmentOf(awardId); ensure the converted awardId matches the
expected Award['id'] type used by awardStore.getOne and
assignmentStore.updateOne.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@pages/activity/`[name]/manage/evaluation.tsx:
- Around line 49-66: The handler handleSubmit currently asserts formToJSON<{
award: number }>(form) but form inputs are strings; change this to parse/coerce
the award value before using it: read award as a string from formToJSON (e.g.,
formToJSON<{ award: string }>(form) or let raw = formToJSON(form)), then convert
award to the correct type (use Number(award) or parseInt(award, 10)) and store
that result in awardId before calling awardStore.getOne(awardId) and
awardStore.assignmentOf(awardId); ensure the converted awardId matches the
expected Award['id'] type used by awardStore.getOne and
assignmentStore.updateOne.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 88439a3e-3381-4f4e-92ba-effd33c53426

📥 Commits

Reviewing files that changed from the base of the PR and between 28c643c and 6239405.

📒 Files selected for processing (4)
  • components/Activity/AwardList.tsx
  • components/Team/TeamAwardAssignment.tsx
  • models/Activity/Award.ts
  • pages/activity/[name]/manage/evaluation.tsx
✅ Files skipped from review due to trivial changes (1)
  • components/Team/TeamAwardAssignment.tsx

@TechQuery
Copy link
Copy Markdown
Member Author

@copilot 将以下后端接口及其对应类型引入:
https://github.com/FreeCodeCamp-Chengdu/HOP-service/blob/ea2249c62533d306e9aac0c0a0047f3c23717535/source/controller/Survey.ts#L21-L43

把原有问卷问题类型替换,再将原有静态数据作为新建问卷时的备选项,在编辑页面新增一个按钮一键填入:

export interface Question {
id?: string;
title: string;
options?: string[];
multiple?: boolean;
type?: 'text' | 'url';
required?: boolean;
}
export const questions = ({ t }: typeof i18n): Question[] => [
{
title: t('what_is_your_occupation'),
options: [
t('front_end_engineer'),
t('back_end_engineer'),
t('client_engineer'),
t('game_developerment_engineer'),
t('algorithm_engineer'),
t('blockchain_engineer'),
t('operations'),
t('quality_assurance_engineer'),
t('architect'),
t('project_manager'),
t('student'),
t('others'),
],
multiple: true,
},
{
title: t('what_are_your_commonly_used_programming_languages'),
options: [
'JavaScript/TypeScript',
'Java/Scala/Groovy/Kotlin',
'C#',
'Python',
'PHP',
'Ruby',
'Dart',
'Objective-C/Swift',
'Rust',
'Go',
'C/C++',
t('others'),
],
multiple: true,
},
{
title: t('linkein_or_cv'),
type: 'url',
},
{
title: t('social_media_account_or_twitter_or_weibo'),
type: 'url',
},
{
title: t('which_of_these_particular_areas_are_you_interested_in_going_into_this_event'),
type: 'text',
},
{
title: t('what_do_you_hope_to_learn_from_the_workshops'),
type: 'text',
},
{
title: t('do_you_plan__on_hacking_solo_or_with_a_team'),
options: [t('yes'), t('no_i_prefer_solo'), t('no_i_have_a_team')],
},
{
title: t('would_you_be_open_to_having_a_beginner_join_your_team'),
options: [t('yes'), t('no')],
},
{
title: t('anything_else_we_should_know_about_what_youre_looking_for_in_a_team_or_teammate'),
type: 'text',
},
];

Copy link
Copy Markdown

Copilot AI commented May 5, 2026

@copilot 将以下后端接口及其对应类型引入:
https://github.com/FreeCodeCamp-Chengdu/HOP-service/blob/ea2249c62533d306e9aac0c0a0047f3c23717535/source/controller/Survey.ts#L21-L43
...

Done in commit c82a700. Here's what was changed:

  • models/Activity/Question.ts: Removed the local Question interface; now imports and re-exports Question and QuestionType from @freecodecamp-chengdu/hop-service. The static questions preset function is kept and updated to use QuestionType.Text/QuestionType.URL enum values.

  • components/Activity/QuestionnaireTable.tsx: Updated import to use the QuestionType re-export and uses QuestionType.Text as the default for the type field.

  • pages/activity/[name]/manage/questionnaire.tsx: Imports the questions preset function. Added a fillWithDefaults method (with a confirmation prompt if existing questions would be overwritten) and a "Fill with default questions" secondary button on the edit page.

  • translation/{en-US,zh-CN,zh-TW}.ts: Added fill_default_questions translation key.

Copy link
Copy Markdown
Member Author

@TechQuery TechQuery left a comment

Choose a reason for hiding this comment

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

@copilot 根据我的以上意见来修改。

Comment thread models/Activity/Award.ts Outdated
team?: Team;
award: Award;
}
export { Award, AwardAssignment, AwardTarget };
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

  1. 所有的类型都直接引用,不要重复导出
  2. 注意这个包是纯类型包,枚举等运行时不可用,要直接写枚举的具体值,并可能需要断言

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Done in commit 0dca77b:

  • Removed export { Award, AwardAssignment, AwardTarget } from models/Activity/Award.ts — all consumers now import directly from @freecodecamp-chengdu/hop-service
  • Same done for models/Activity/Question.ts
  • All imports from hop-service changed to import type since the package is type-only
  • Runtime enum value AwardTarget.Team replaced with the concrete string 'team' in evaluation.tsx
  • Runtime enum values QuestionType.Text/QuestionType.URL replaced with 'text' as QuestionType / 'url' as QuestionType

)
return;

const defaultQuestions = questions(this.observedContext).map((q, i) => ({
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

变量名用英文单词全写。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Done in commit 0dca77b — renamed qquestion and iindex.

Copy link
Copy Markdown

Copilot AI commented May 5, 2026

@copilot 根据我的以上意见来修改。

Done in commit 0dca77b. Both points addressed — re-exports removed and runtime enum values replaced with string literals.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
models/Activity/index.ts (1)

169-183: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

restored field inherited from ActivityModel ties every UserActivityModel instance to the singleton's 'Activity' localStorage key.

ActivityModel.restored = !isServer() && restore(this, 'Activity') runs as a class field initializer for every subclass instance via super(), so every per-tab UserActivityModel (creator/staff/enrollee) hydrates from and persists into the same key as the default-export singleton, causing them to overwrite each other and the singleton cache.

🛡️ Suggested fix
 export class UserActivityModel extends ActivityModel {
+  override restored = false as const;
+
   constructor(
     public userId: number,
     public role: 'creator' | 'staff' | 'enrollee',
   ) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@models/Activity/index.ts` around lines 169 - 183, ActivityModel currently
initializes restored with a fixed key via ActivityModel.restored = !isServer()
&& restore(this, 'Activity'), causing every UserActivityModel (created via
super()) to hydrate/persist to the same 'Activity' localStorage key and
overwrite each other; change the persistence key to be instance-specific (e.g.,
include userId and role) by refactoring ActivityModel to accept a storageKey (or
compute it from this.constructor plus identifiers) and replace restore(this,
'Activity') with restore(this, storageKey), then update UserActivityModel to
pass a unique key like `Activity:${this.userId}:${this.role}` (or otherwise
compute the unique key before calling super) so each instance uses its own
localStorage entry.
🧹 Nitpick comments (4)
pages/activity/[name]/manage/questionnaire.tsx (1)

110-113: 💤 Low value

Sequential string IDs from index can collide with later user-added items.

String(index + 1) produces ids '1'..'9'. After filling defaults, if the user adds more questions via addQuestionnaireItem and another flow assigns ids based on length, or if defaults are filled twice, ids may collide and addQuestionnaireItem will short-circuit with the “question_id_repeat” alert. Consider a more unique scheme (e.g., crypto.randomUUID() or a monotonic counter prefixed like default-${i+1}) to avoid surprise collisions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pages/activity/`[name]/manage/questionnaire.tsx around lines 110 - 113,
defaultQuestions currently assigns sequential string ids via String(index + 1)
which can collide with later user-added items and trigger the question_id_repeat
short-circuit in addQuestionnaireItem; replace the sequential id generation in
the defaultQuestions mapping (the expression that wraps
questions(this.observedContext)) with a unique scheme such as
crypto.randomUUID() or a monotonic prefixed id like `default-${index+1}` so each
default item has a globally-unique id and cannot collide with user-added ids or
repeated defaults.
models/Activity/index.ts (1)

169-183: 💤 Low value

UserActivityModel only specializes loadPage; other inherited operations still hit the global hackathon endpoint.

baseURI remains 'hackathon', so any inherited TableModel methods (getOne, updateOne, deleteOne, getList, etc.) invoked on a UserActivityModel instance will operate on the global hackathon resource, not on user/${userId}/hackathon/${role}. If only loadPage is intended to be used here, consider documenting that or narrowing the public surface; otherwise, override baseURI (or those methods) so the user-scoped URL is honored consistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@models/Activity/index.ts` around lines 169 - 183, UserActivityModel currently
only overrides loadPage so inherited TableModel methods (getOne, updateOne,
deleteOne, getList, etc.) still use the default baseURI 'hackathon', causing
requests to the global resource; either restrict the public surface or override
baseURI (or those specific methods) to use the user-scoped path
`user/${this.userId}/hackathon/${this.role}` so all operations target the same
endpoint—update the class to set a per-instance baseURI or implement wrapper
methods for getOne/updateOne/deleteOne/getList that prepend
`user/${this.userId}/hackathon/${this.role}` to requests, and keep loadPage
as-is.
pages/activity/[name]/register.tsx (1)

26-28: 💤 Low value

Local activityStore shadows the imported singleton.

Line 12 imports activityStore (the singleton) and ActivityModel, then line 26 declares const activityStore = new ActivityModel(); inside getServerSideProps, shadowing the module-level binding. Functionally fine because the SSR-only block uses a fresh instance, but a different name (e.g., store) avoids the shadowing and makes intent clearer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pages/activity/`[name]/register.tsx around lines 26 - 28, The local
declaration const activityStore = new ActivityModel() inside getServerSideProps
shadows the imported activityStore singleton; rename the local variable (e.g.,
to store or activityModel) so it doesn't shadow the module-level activityStore
and update subsequent references (status, questionnaire calls) to use the new
name; ensure you reference ActivityModel when creating the instance and keep
getServerSideProps logic unchanged otherwise.
components/Activity/QuestionnaireTable.tsx (1)

37-37: 💤 Low value

Consider the cast pattern consistency before removing as QuestionType.

While type = 'text' may appear to be valid without the cast, note that this as QuestionType pattern is consistently used throughout the codebase (see models/Activity/Question.ts lines 44, 48, 52, 56, 68). Since QuestionType is imported from the external @freecodecamp-chengdu/hop-service package, the cast may be intentional for type safety or narrowing. If removing it elsewhere, ensure consistency across the codebase and that no TypeScript compiler issues arise.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/Activity/QuestionnaireTable.tsx` at line 37, The default parameter
in the QuestionnaireTable mapping currently uses type = 'text' without the cast,
which breaks the existing pattern of casting to QuestionType; restore the cast
so the parameter reads type = 'text' as QuestionType to match the rest of the
codebase (see QuestionType usage in models/Activity/Question.ts) and avoid
TypeScript narrowing/import mismatches; update the mapping callback signature ({
id, title, options, multiple, type = 'text' as QuestionType, required }, index,
{ length }) to reintroduce the cast so the component and other modules remain
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@models/Activity/index.ts`:
- Around line 162-166: The signOne method currently discards the POST response
and returns currentEnrollment?.getSessionOne(), which can be undefined when
currentEnrollment is not set (see enrollmentOf usage) and causes a
redundant/racy GET via getSessionOne(); update signOne to capture and return the
POST response body (the newly created enrollment/session data) and also update
the model state (set currentEnrollment or sessionOne on the ActivityModel) from
that response so callers get the authoritative data without an extra GET; ensure
you check the enrollment POST response shape and map it into the model (or
fallback to calling getSessionOne only if the response lacks the expected data).

In `@models/Activity/Question.ts`:
- Around line 42-45: In models/Activity/Question.ts replace the typo'd
translation key t('linkein_or_cv') with t('linkedin_or_cv') and update all
corresponding translation entries (e.g., translation/en-US.ts, zh-CN.ts,
zh-TW.ts) to add/rename the key to "linkedin_or_cv" so the UI and stored
analytics use the corrected identifier; ensure you remove or alias the old
"linkein_or_cv" key to avoid duplicates.

In `@pages/activity/`[name]/manage/questionnaire.tsx:
- Around line 101-116: The confirmation message in fillWithDefaults currently
reuses t('confirm_to_delete_questionnaire') which is misleading; update
fillWithDefaults to call a new translation key (e.g.,
t('confirm_overwrite_with_defaults')) instead of
t('confirm_to_delete_questionnaire'), then add that key with the suggested copy
("Existing questions will be replaced with the default set. Continue?") to your
translation files (e.g., translation/en-US.ts, zh-CN.ts, zh-TW.ts) so the prompt
accurately reflects that existing questions will be overwritten with defaults.

---

Duplicate comments:
In `@models/Activity/index.ts`:
- Around line 169-183: ActivityModel currently initializes restored with a fixed
key via ActivityModel.restored = !isServer() && restore(this, 'Activity'),
causing every UserActivityModel (created via super()) to hydrate/persist to the
same 'Activity' localStorage key and overwrite each other; change the
persistence key to be instance-specific (e.g., include userId and role) by
refactoring ActivityModel to accept a storageKey (or compute it from
this.constructor plus identifiers) and replace restore(this, 'Activity') with
restore(this, storageKey), then update UserActivityModel to pass a unique key
like `Activity:${this.userId}:${this.role}` (or otherwise compute the unique key
before calling super) so each instance uses its own localStorage entry.

---

Nitpick comments:
In `@components/Activity/QuestionnaireTable.tsx`:
- Line 37: The default parameter in the QuestionnaireTable mapping currently
uses type = 'text' without the cast, which breaks the existing pattern of
casting to QuestionType; restore the cast so the parameter reads type = 'text'
as QuestionType to match the rest of the codebase (see QuestionType usage in
models/Activity/Question.ts) and avoid TypeScript narrowing/import mismatches;
update the mapping callback signature ({ id, title, options, multiple, type =
'text' as QuestionType, required }, index, { length }) to reintroduce the cast
so the component and other modules remain consistent.

In `@models/Activity/index.ts`:
- Around line 169-183: UserActivityModel currently only overrides loadPage so
inherited TableModel methods (getOne, updateOne, deleteOne, getList, etc.) still
use the default baseURI 'hackathon', causing requests to the global resource;
either restrict the public surface or override baseURI (or those specific
methods) to use the user-scoped path
`user/${this.userId}/hackathon/${this.role}` so all operations target the same
endpoint—update the class to set a per-instance baseURI or implement wrapper
methods for getOne/updateOne/deleteOne/getList that prepend
`user/${this.userId}/hackathon/${this.role}` to requests, and keep loadPage
as-is.

In `@pages/activity/`[name]/manage/questionnaire.tsx:
- Around line 110-113: defaultQuestions currently assigns sequential string ids
via String(index + 1) which can collide with later user-added items and trigger
the question_id_repeat short-circuit in addQuestionnaireItem; replace the
sequential id generation in the defaultQuestions mapping (the expression that
wraps questions(this.observedContext)) with a unique scheme such as
crypto.randomUUID() or a monotonic prefixed id like `default-${index+1}` so each
default item has a globally-unique id and cannot collide with user-added ids or
repeated defaults.

In `@pages/activity/`[name]/register.tsx:
- Around line 26-28: The local declaration const activityStore = new
ActivityModel() inside getServerSideProps shadows the imported activityStore
singleton; rename the local variable (e.g., to store or activityModel) so it
doesn't shadow the module-level activityStore and update subsequent references
(status, questionnaire calls) to use the new name; ensure you reference
ActivityModel when creating the instance and keep getServerSideProps logic
unchanged otherwise.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5077a851-ffdb-4e1e-9689-9189d3c1d27a

📥 Commits

Reviewing files that changed from the base of the PR and between 6239405 and 0dca77b.

📒 Files selected for processing (16)
  • components/Activity/AwardList.tsx
  • components/Activity/QuestionnaireCreate.tsx
  • components/Activity/QuestionnairePreview.tsx
  • components/Activity/QuestionnaireTable.tsx
  • components/Team/TeamAwardAssignment.tsx
  • models/Activity/Award.ts
  • models/Activity/Question.ts
  • models/Activity/Team.ts
  • models/Activity/index.ts
  • pages/activity/[name]/manage/award.tsx
  • pages/activity/[name]/manage/evaluation.tsx
  • pages/activity/[name]/manage/questionnaire.tsx
  • pages/activity/[name]/register.tsx
  • translation/en-US.ts
  • translation/zh-CN.ts
  • translation/zh-TW.ts
✅ Files skipped from review due to trivial changes (1)
  • components/Activity/QuestionnairePreview.tsx

Comment thread models/Activity/index.ts
Comment on lines 162 to 166
async signOne(name: string, form: Enrollment['form'] = []) {
await this.client.put(`${this.baseURI}/${name}/enrollment`, { form });
await this.client.post(`${this.baseURI}/${name}/enrollment`, { form });

return this.currentEnrollment?.getSessionOne();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all callers of signOne and whether currentEnrollment is guaranteed to be initialized first.
rg -nP -C3 '\bsignOne\s*\('
rg -nP -C3 '\benrollmentOf\s*\('

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 5175


🏁 Script executed:

# Check the register.tsx file to see if enrollmentOf is called before signOne
head -100 pages/activity/[name]/register.tsx

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 2698


🏁 Script executed:

# Check the full context of the index.tsx file where signOne is called
sed -n '60,130p' pages/activity/[name]/index.tsx

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 1936


🏁 Script executed:

# Check the getSessionOne implementation in EnrollmentModel
rg -nP -A5 'getSessionOne\s*\(' models/

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 1242


🏁 Script executed:

# Check the getOne method in Activity model to see the full initialization flow
sed -n '110,140p' models/Activity/index.ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 924


🏁 Script executed:

# Check EnrollmentModel constructor and class definition
sed -n '1,50p' models/Activity/Enrollment.ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 1622


🏁 Script executed:

# Check if there's any watch or auto-fetch in the model
rg -nP 'makeAutoObservable|makeObservable|reaction|watch' models/Activity/Enrollment.ts

Repository: FreeCodeCamp-Chengdu/HOP

Length of output: 50


The POST enrollment response is discarded and may return undefined in some call paths.

signOne discards the POST response and calls currentEnrollment?.getSessionOne() instead. Two issues:

  1. Silent failure in register.tsx: The page's server-side getOne() is called on a local ActivityModel instance, not the global activityStore. When the client calls signOne(), currentEnrollment is undefined on the global store, so the return silently fails. The enrollmentOf() method only creates a new model without fetching data.

  2. Wasteful redundant fetch: The POST response is discarded and a separate GET request is made via getSessionOne(). This is wasteful and potentially racy depending on backend semantics.

The index.tsx page is safer (calls enrollmentOf() before signOne()), but still depends on getSessionOne() being called elsewhere to populate sessionOne. Confirm the POST enrollment endpoint already returns the new enrollment data, and either return it directly or defer the GET until actually needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@models/Activity/index.ts` around lines 162 - 166, The signOne method
currently discards the POST response and returns
currentEnrollment?.getSessionOne(), which can be undefined when
currentEnrollment is not set (see enrollmentOf usage) and causes a
redundant/racy GET via getSessionOne(); update signOne to capture and return the
POST response body (the newly created enrollment/session data) and also update
the model state (set currentEnrollment or sessionOne on the ActivityModel) from
that response so callers get the authoritative data without an extra GET; ensure
you check the enrollment POST response shape and map it into the model (or
fallback to calling getSessionOne only if the response lacks the expected data).

Comment on lines 42 to 45
{
title: t('linkein_or_cv'),
type: 'url',
type: 'url' as QuestionType,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Typo: linkein_or_cv should be linkedin_or_cv.

The translation key (and presumably the entries in translation/en-US.ts/zh-CN.ts/zh-TW.ts) use linkein instead of linkedin. Easy to fix now before it becomes a load-bearing string in stored data or analytics.

✏️ Proposed fix (apply across translation files too)
-    title: t('linkein_or_cv'),
+    title: t('linkedin_or_cv'),
📝 Committable suggestion

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

Suggested change
{
title: t('linkein_or_cv'),
type: 'url',
type: 'url' as QuestionType,
},
{
title: t('linkedin_or_cv'),
type: 'url' as QuestionType,
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@models/Activity/Question.ts` around lines 42 - 45, In
models/Activity/Question.ts replace the typo'd translation key
t('linkein_or_cv') with t('linkedin_or_cv') and update all corresponding
translation entries (e.g., translation/en-US.ts, zh-CN.ts, zh-TW.ts) to
add/rename the key to "linkedin_or_cv" so the UI and stored analytics use the
corrected identifier; ensure you remove or alias the old "linkein_or_cv" key to
avoid duplicates.

Comment on lines +101 to +116
fillWithDefaults = () => {
const { t } = this.observedContext;

if (
activityStore.questionnaire.length > 0 &&
!confirm(t('confirm_to_delete_questionnaire'))
)
return;

const defaultQuestions = questions(this.observedContext).map((question, index) => ({
...question,
id: String(index + 1),
}));

return activityStore.editQuestionnaireStatus(defaultQuestions);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Confirmation message is misleading for the “fill defaults” action.

fillWithDefaults reuses t('confirm_to_delete_questionnaire'), but this flow overwrites the existing questions with defaults rather than deleting the questionnaire. A user who is OK with replacement may cancel because the prompt says “delete”, and vice versa. Add a dedicated translation key (e.g., confirm_overwrite_with_defaults) for clarity.

🌐 Suggested message wording
     if (
       activityStore.questionnaire.length > 0 &&
-      !confirm(t('confirm_to_delete_questionnaire'))
+      !confirm(t('confirm_overwrite_with_defaults'))
     )
       return;

Add the new key to translation/en-US.ts, zh-CN.ts, zh-TW.ts (e.g., “Existing questions will be replaced with the default set. Continue?”).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pages/activity/`[name]/manage/questionnaire.tsx around lines 101 - 116, The
confirmation message in fillWithDefaults currently reuses
t('confirm_to_delete_questionnaire') which is misleading; update
fillWithDefaults to call a new translation key (e.g.,
t('confirm_overwrite_with_defaults')) instead of
t('confirm_to_delete_questionnaire'), then add that key with the suggested copy
("Existing questions will be replaced with the default set. Continue?") to your
translation files (e.g., translation/en-US.ts, zh-CN.ts, zh-TW.ts) so the prompt
accurately reflects that existing questions will be overwritten with defaults.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants