Conversation
…ncluding dedicated public campaign pages.
… pr-502 Merge into main
Replace the legacy ProjectLayout + ProjectSidebar shell with a new HeroSection + tabbed layout colocated under app/(landing)/projects/[slug]/components. All existing functionality is preserved: vote/back/follow/share actions, realtime vote counts, milestone submissions and disputes, backers list, and the existing comment system continues to render via ProjectComments. The campaigns/[slug] route is migrated to the same components so both routes share one source of truth, and the now-unused legacy files under components/project-details/ (project-layout, project-sidebar/*, project-details, project-team, project-about, project-backers/*, project-voters/*, project-loading, CampaignEndBanner, ProjectFundEscrow) are deleted. comment-section, funding-modal, and project-milestone are kept since they are still consumed by the new UI and standalone routes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@Benjtalkshow is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughRefactors campaign/project pages into a tab-driven composition (HeroSection, ProjectTabs, per-tab panels) with new skeletons and client components; centralizes and tightens data fetch/error flow (crowdfunding → submission fallback), adds silent refresh, tightens types, and removes legacy sidebar/layout components. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Page as CampaignPage / ProjectPage
participant Fetcher as fetchData
participant VM as ViewModelBuilder
participant UI as Tab UI (HeroSection + ProjectTabs)
participant Actions as ProjectActions
User->>Page: Request /campaigns/{slug} or /projects/{slug}
Page->>Fetcher: fetchData(slug, isSubmission?)
alt isSubmission
Fetcher->>Fetcher: fetchAsSubmission()
Fetcher-->>Page: submission data
else not submission
Fetcher->>Fetcher: getCrowdfundingProject()
alt crowdfunding success
Fetcher-->>Page: campaign data
else crowdfunding 404
Fetcher->>Fetcher: fetchAsSubmission()
Fetcher-->>Page: submission data
else crowdfunding error (non-404)
Fetcher-->>Page: throw error
end
end
Page->>VM: build ProjectViewModel
Page->>UI: render HeroSection + ProjectTabs + ActiveTabPanel (or Skeleton)
UI-->>User: display content or skeletons
User->>UI: select tab
UI->>UI: set activeTab and render tab panel (Details/Team/Milestones/Voters/Backers/Comments)
User->>Actions: Vote / Back / Follow / Share / Cancel
alt Vote
Actions->>Actions: optional on-chain vote -> createVote
Actions->>VM: update voteCounts (realtime)
Actions-->>User: toast/status
else Back
Actions->>User: open FundingModal
else Cancel
Actions->>Actions: cancelCampaign -> deleteCrowdfundingProject
Actions->>Page: call refreshData()
end
Page->>Fetcher: refreshData() (silent)
Fetcher-->>Page: updated VM (merge on success, keep existing on failure)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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: 11
🧹 Nitpick comments (4)
app/(landing)/projects/[slug]/components/backer-card.tsx (1)
28-48: DuplicateformatRelativeimplementation.This relative-time formatting logic is duplicated in
voter-row.tsx(lines 13-33). Consider extracting to a shared utility to maintain DRY principles.♻️ Suggested extraction
Create a shared utility in
utils.tsor a dedicateddate-utils.ts:// e.g., in app/(landing)/projects/[slug]/components/date-utils.ts export function formatRelative(iso: string): string { try { const then = new Date(iso).getTime(); const now = Date.now(); const diff = Math.max(1, Math.round((now - then) / 1000)); if (diff < 60) return `${diff}s ago`; const min = Math.round(diff / 60); if (min < 60) return `${min} minute${min === 1 ? '' : 's'} ago`; const hr = Math.round(min / 60); if (hr < 24) return `${hr} hour${hr === 1 ? '' : 's'} ago`; const day = Math.round(hr / 24); if (day < 30) return `${day} day${day === 1 ? '' : 's'} ago`; return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); } catch { return ''; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/projects/[slug]/components/backer-card.tsx around lines 28 - 48, The formatRelative function in backer-card.tsx is duplicated in voter-row.tsx; extract it into a shared module (e.g., date-utils.ts or utils.ts) and export it, then import and replace the local implementations in both backer-card.tsx and voter-row.tsx to call the shared formatRelative function; ensure the exported function signature and behavior match the existing formatRelative(iso: string) and update imports where used.app/(landing)/projects/[slug]/components/details-tab.tsx (1)
313-334: Placeholder button handlers scroll to top instead of implementing functionality.Both "Follow Project" and "Share" buttons in the empty state only scroll to top. Consider either:
- Implementing actual functionality
- Disabling the buttons with a tooltip explaining the feature is coming
- Adding a TODO comment for tracking
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/projects/[slug]/components/details-tab.tsx around lines 313 - 334, The "Follow Project" and "Share" Button components in details-tab.tsx currently only call window.scrollTo and should be updated: replace the placeholder onClick handlers for the Button that renders the "Follow Project" label and the Button that renders the "Share" label with real behavior (e.g., call a followProject(projectId) async handler and a shareProject() handler) or, if the features aren't ready, disable the buttons (set disabled prop) and add a tooltip explaining "Coming soon" plus a TODO comment referencing these Buttons so they can be implemented later; ensure you update the two Button elements (the one with the Bell icon and the one with the Share2 icon) and keep styling intact while removing the window.scrollTo placeholder.app/(landing)/projects/[slug]/page.tsx (1)
262-276: Consider adding error handling for params resolution.While unlikely in practice, if
params.then()were to reject, the component would remain in the skeleton state indefinitely. Adding a.catch()for defensive coding could improve robustness.🛡️ Suggested defensive handling
useEffect(() => { - params.then(resolved => setSlug(resolved.slug)); + params + .then(resolved => setSlug(resolved.slug)) + .catch(() => setSlug('')); // triggers notFound() via error state }, [params]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/projects/[slug]/page.tsx around lines 262 - 276, ProjectPage's useEffect currently calls params.then(...) without handling rejection, so add defensive error handling in that effect: update the effect that uses params (inside ProjectPage) to handle promise rejection (e.g., params.then(resolved => setSlug(resolved.slug)).catch(err => { setSlug(null); /* or set an error state */ })) or switch to an async IIFE with try/catch to set an error state; then render a fallback (ProjectPageSkeleton or an error UI) when the promise fails instead of leaving the component stuck. Ensure you modify the useEffect containing params, setSlug, and the conditional rendering to account for the error state so ProjectContent is only rendered when slug is valid.app/(landing)/projects/[slug]/components/project-actions.tsx (1)
69-73: Align component/helper declarations with repo TSX style rule.
ProjectActions,initialVoteCounts, andrenderPrimaryActionare function declarations. Repo guidance prefers typedconstarrow declarations for TS/TSX consistency.As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations".
Also applies to: 314-342
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/projects/[slug]/components/project-actions.tsx around lines 69 - 73, Change the named function declarations to typed const arrow functions: convert ProjectActions to a const React component with an explicit type annotation (e.g., const ProjectActions: React.FC<ProjectActionsProps> = (...) => { ... }), and likewise convert initialVoteCounts and renderPrimaryAction into const arrow functions with explicit parameter and return types; update any exported names to match the new consts and ensure usages remain unchanged (also apply the same refactor for the functions in the 314-342 region).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(landing)/campaigns/[slug]/page.tsx:
- Around line 57-95: The current fetchData flow treats any exception from
getCrowdfundingProject() as “not a crowdfunding campaign” and falls back to
fetchAsSubmission(), which causes transport/5xx errors to become 404s; change
the getCrowdfundingProject() catch to inspect the error (e.g., HTTP status or a
discriminant on the thrown error) and only fall through to fetchAsSubmission()
for explicit NotFound/404 cases, while for other errors rethrow or call
reportError(err, { context: 'campaigns-fetch', id }) and setError('Failed to
fetch project data') (respecting the cancelled guard); keep the existing
fallback to fetchAsSubmission(), fetchAsSubmission() error handling, and the
final setLoading(false) behavior.
- Around line 150-169: The activeTab state is only initialized from tabs[0] once
and can become stale when the tabs set changes; update the component to reset
activeTab whenever tabs changes by adding an effect that calls
setActiveTab(tabs[0]?.value ?? 'details') with tabs as the dependency (the code
uses buildProjectTabs(vm), activeTab/useState, and ProjectTabs) so the selected
tab always matches the current tabs for the page.
In `@app/`(landing)/projects/[slug]/components/backers-tab.tsx:
- Around line 45-48: The gating logic for canBack uses
normalizeCampaignStatus(vm.status) which reads only raw backend status; replace
that check with the same derived-status helper used elsewhere (e.g.,
derivedProjectStatus(vm) or the project's derived status function) so the
condition becomes something like derivedProjectStatus(vm) ===
CampaignStatus.CAMPAIGNING; update the import/usage to reference the
derived-status helper and keep the other parts of canBack (isSubmission,
!!vm.campaign) unchanged.
In `@app/`(landing)/projects/[slug]/components/milestones-tab.tsx:
- Around line 105-129: mapStatus currently falls through for reviewStatus ===
'resubmission_required' and returns 'awaiting', causing rows to be styled/count
as upcoming; update mapStatus(reviewStatus?: string): DisplayStatus to
explicitly handle 'resubmission_required' (and any close variants like
'resubmit_required') and return the actionable display state used by canSubmit
(e.g., 'submission') so those rows are styled and filtered correctly; also make
the same change in the other mapStatus occurrence referenced by the review (the
other mapping function handling reviewStatus) to keep behavior consistent.
- Around line 131-140: The formatDate function currently calls new
Date(endDate).toLocaleDateString(...) but malformed inputs produce an "Invalid
Date" string because new Date(...) doesn't throw; update formatDate to first
construct const d = new Date(endDate) and check validity (e.g.,
isNaN(d.getTime()) or !isFinite(d.getTime())) and return 'TBD' for invalid
dates, otherwise call d.toLocaleDateString('en-US', {...}) to format; reference
the formatDate function and the new local variable d when making this change.
In `@app/`(landing)/projects/[slug]/components/project-actions.tsx:
- Around line 178-186: The cancel flow conflates on-chain cancellation and
off-chain deletion; split them so cancelCampaign(vm.campaign.onChainId) runs in
its own try/catch and errors from
deleteCrowdfundingProject(vm.campaign.campaignId) are handled separately: call
cancelCampaign first, on success show a success toast for on-chain tx (use
transactionHash.slice), then attempt deleteCrowdfundingProject in a second
try/catch; if delete fails, parseCrowdfundError(err) and show a distinct
toast/error message indicating the on-chain cancellation succeeded but the
off-chain delete failed (offer retry guidance), log the delete error for
telemetry, and still call onRefresh?. Ensure you reference cancelCampaign and
deleteCrowdfundingProject when making these changes.
- Around line 88-110: Replace hardcoded VoteEntityType.CROWDFUNDING_CAMPAIGN
with the derived entityType variable so reads/listeners match writes: update the
useVoteRealtime call (first arg object with entityType) to use entityType
instead of VoteEntityType.CROWDFUNDING_CAMPAIGN, and change the getVoteCounts
call (currently passing VoteEntityType.CROWDFUNDING_CAMPAIGN) to pass the same
entityType used for writes (the variable referenced where vote submissions use
entityType) so that useVoteRealtime, getVoteCounts, and vote submission all use
the identical entityType.
In `@app/`(landing)/projects/[slug]/components/team-member-card.tsx:
- Around line 34-47: The card-level keyboard handler is intercepting Enter/Space
from inner interactive elements; update the onKeyDown handler in
team-member-card.tsx (the handler attached alongside handleCardClick and
role/tabIndex) to ignore events originating from interactive descendants by
returning early when event.target !== event.currentTarget (i.e., only handle
keys when the card itself is focused), then proceed to call
onProfileClick(member) and preventDefault only in that case; apply the same
guard to the other card-level keyboard handler block around lines 77-99 so
nested links are not stolen.
In `@app/`(landing)/projects/[slug]/components/team-tab.tsx:
- Around line 34-39: The duplicate-detection check in matchesCreator is
comparing member.email to vm.creator.username which is invalid because VMCreator
(vm.creator) has no email; remove the ineffective email comparison and keep (or
rely on) the username comparison. Update the matchesCreator expression (used
where vm.creator and member are referenced) to only check vm.creator exists and
compare member.username === vm.creator.username (and/or member.username
truthiness) so the continue logic correctly skips the creator.
In `@app/`(landing)/projects/[slug]/components/voters-tab.tsx:
- Around line 104-109: The voters list is being silently limited by the fixed
limit in the call to getProjectVotes (in voters-tab.tsx) — change the fetching
strategy so the tab no longer shows only 20 voters: either remove the hard-coded
limit or replace it with a dynamic/paginated approach (e.g., use response.total
or a "limit" prop to request all voters when rendering the full list, or
implement incremental loading via offset + "Load more" button); update the call
site that uses VoteEntityType.CROWDFUNDING_CAMPAIGN and includeVoters: true so
it requests enough items (or drives pagination state) and ensure the component
renders the paginated/fully fetched response.voters rather than assuming the
initial 20 is complete.
- Around line 81-92: The realtime handlers (onVoteUpdated, onVoteCreated,
onVoteDeleted) replace local voteCounts with data.voteCounts and can
accidentally wipe the current viewer’s userVote when the incoming payload omits
it; change each handler to merge the incoming aggregate into the existing
voteCounts by calling setVoteCounts(prev => ({ ...prev, ...data.voteCounts,
userVote: data.voteCounts.userVote !== undefined ? data.voteCounts.userVote :
prev.userVote })), and for onVoteDeleted do not force userVote to null—use the
same merge pattern so a deletion by another user won’t clear the viewer’s
selection; keep setVoters(data.voters) as-is.
---
Nitpick comments:
In `@app/`(landing)/projects/[slug]/components/backer-card.tsx:
- Around line 28-48: The formatRelative function in backer-card.tsx is
duplicated in voter-row.tsx; extract it into a shared module (e.g.,
date-utils.ts or utils.ts) and export it, then import and replace the local
implementations in both backer-card.tsx and voter-row.tsx to call the shared
formatRelative function; ensure the exported function signature and behavior
match the existing formatRelative(iso: string) and update imports where used.
In `@app/`(landing)/projects/[slug]/components/details-tab.tsx:
- Around line 313-334: The "Follow Project" and "Share" Button components in
details-tab.tsx currently only call window.scrollTo and should be updated:
replace the placeholder onClick handlers for the Button that renders the "Follow
Project" label and the Button that renders the "Share" label with real behavior
(e.g., call a followProject(projectId) async handler and a shareProject()
handler) or, if the features aren't ready, disable the buttons (set disabled
prop) and add a tooltip explaining "Coming soon" plus a TODO comment referencing
these Buttons so they can be implemented later; ensure you update the two Button
elements (the one with the Bell icon and the one with the Share2 icon) and keep
styling intact while removing the window.scrollTo placeholder.
In `@app/`(landing)/projects/[slug]/components/project-actions.tsx:
- Around line 69-73: Change the named function declarations to typed const arrow
functions: convert ProjectActions to a const React component with an explicit
type annotation (e.g., const ProjectActions: React.FC<ProjectActionsProps> =
(...) => { ... }), and likewise convert initialVoteCounts and
renderPrimaryAction into const arrow functions with explicit parameter and
return types; update any exported names to match the new consts and ensure
usages remain unchanged (also apply the same refactor for the functions in the
314-342 region).
In `@app/`(landing)/projects/[slug]/page.tsx:
- Around line 262-276: ProjectPage's useEffect currently calls params.then(...)
without handling rejection, so add defensive error handling in that effect:
update the effect that uses params (inside ProjectPage) to handle promise
rejection (e.g., params.then(resolved => setSlug(resolved.slug)).catch(err => {
setSlug(null); /* or set an error state */ })) or switch to an async IIFE with
try/catch to set an error state; then render a fallback (ProjectPageSkeleton or
an error UI) when the promise fails instead of leaving the component stuck.
Ensure you modify the useEffect containing params, setSlug, and the conditional
rendering to account for the error state so ProjectContent is only rendered when
slug is valid.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 58fd1988-37f7-4b62-b52e-109920da0571
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (37)
app/(landing)/campaigns/[slug]/page.tsxapp/(landing)/projects/[slug]/components/backer-card.tsxapp/(landing)/projects/[slug]/components/backers-tab.tsxapp/(landing)/projects/[slug]/components/details-tab.tsxapp/(landing)/projects/[slug]/components/hero-section.tsxapp/(landing)/projects/[slug]/components/milestones-tab.tsxapp/(landing)/projects/[slug]/components/project-actions.tsxapp/(landing)/projects/[slug]/components/project-banner.tsxapp/(landing)/projects/[slug]/components/project-details-card.tsxapp/(landing)/projects/[slug]/components/project-tabs.tsxapp/(landing)/projects/[slug]/components/share-popup.tsxapp/(landing)/projects/[slug]/components/skeletons.tsxapp/(landing)/projects/[slug]/components/team-member-card.tsxapp/(landing)/projects/[slug]/components/team-tab.tsxapp/(landing)/projects/[slug]/components/utils.tsapp/(landing)/projects/[slug]/components/voter-row.tsxapp/(landing)/projects/[slug]/components/voters-tab.tsxapp/(landing)/projects/[slug]/page.tsxcomponents/project-details/CampaignEndBanner.tsxcomponents/project-details/ProjectFundEscrow.tsxcomponents/project-details/project-about.tsxcomponents/project-details/project-backers/Empty.tsxcomponents/project-details/project-backers/index.tsxcomponents/project-details/project-details.tsxcomponents/project-details/project-layout.tsxcomponents/project-details/project-loading.tsxcomponents/project-details/project-sidebar/ProjectSidebarActions.tsxcomponents/project-details/project-sidebar/ProjectSidebarCreator.tsxcomponents/project-details/project-sidebar/ProjectSidebarHeader.tsxcomponents/project-details/project-sidebar/ProjectSidebarLinks.tsxcomponents/project-details/project-sidebar/ProjectSidebarProgress.tsxcomponents/project-details/project-sidebar/index.tsxcomponents/project-details/project-sidebar/types.tscomponents/project-details/project-sidebar/utils.tscomponents/project-details/project-team.tsxcomponents/project-details/project-voters/Empty.tsxcomponents/project-details/project-voters/index.tsx
💤 Files with no reviewable changes (19)
- components/project-details/project-sidebar/ProjectSidebarCreator.tsx
- components/project-details/project-loading.tsx
- components/project-details/project-backers/index.tsx
- components/project-details/project-voters/Empty.tsx
- components/project-details/project-sidebar/ProjectSidebarProgress.tsx
- components/project-details/project-team.tsx
- components/project-details/CampaignEndBanner.tsx
- components/project-details/project-backers/Empty.tsx
- components/project-details/project-about.tsx
- components/project-details/project-sidebar/index.tsx
- components/project-details/project-sidebar/utils.ts
- components/project-details/project-sidebar/types.ts
- components/project-details/project-sidebar/ProjectSidebarLinks.tsx
- components/project-details/ProjectFundEscrow.tsx
- components/project-details/project-details.tsx
- components/project-details/project-sidebar/ProjectSidebarActions.tsx
- components/project-details/project-sidebar/ProjectSidebarHeader.tsx
- components/project-details/project-voters/index.tsx
- components/project-details/project-layout.tsx
| <div | ||
| onClick={handleCardClick} | ||
| role={hasProfile ? 'button' : undefined} | ||
| tabIndex={hasProfile ? 0 : undefined} | ||
| onKeyDown={ | ||
| hasProfile | ||
| ? e => { | ||
| if (e.key === 'Enter' || e.key === ' ') { | ||
| e.preventDefault(); | ||
| onProfileClick?.(member); | ||
| } | ||
| } | ||
| : undefined | ||
| } |
There was a problem hiding this comment.
The card-level keyboard handler steals activation from the inner links.
Because the wrapper itself behaves like a button, Enter/Space on the nested profile/email links bubbles up to the card’s onKeyDown, runs preventDefault(), and triggers onProfileClick instead of the focused link action. Keep the outer container non-interactive around nested links, or at minimum ignore keyboard events that originate from interactive descendants.
Minimal guard if you keep the current structure
onKeyDown={
hasProfile
? e => {
+ if ((e.target as HTMLElement).closest('a, button')) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onProfileClick?.(member);
}
}Also applies to: 77-99
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(landing)/projects/[slug]/components/team-member-card.tsx around lines
34 - 47, The card-level keyboard handler is intercepting Enter/Space from inner
interactive elements; update the onKeyDown handler in team-member-card.tsx (the
handler attached alongside handleCardClick and role/tabIndex) to ignore events
originating from interactive descendants by returning early when event.target
!== event.currentTarget (i.e., only handle keys when the card itself is
focused), then proceed to call onProfileClick(member) and preventDefault only in
that case; apply the same guard to the other card-level keyboard handler block
around lines 77-99 so nested links are not stolen.
- Distinguish API 404 from transport/5xx errors in both project/campaign fetch flows so backend incidents surface as "Failed to fetch" instead of being masked as "Project not found" (new isApiNotFound helper). - Reset activeTab when the tab set changes so the selection never points at a tab that no longer exists. - Handle promise rejection from the params prop with notFound() instead of leaving the page stuck on the skeleton. - Backers tab: gate the funding CTA on getProjectStatus(vm) so a fully funded campaign with a stale CAMPAIGNING backend status no longer shows an actionable Back CTA while the hero shows "Funded". - Milestones tab: map resubmission_required (and variants) to in-progress so resubmittable rows are styled and counted correctly; harden formatDate with an explicit Number.isFinite check. - Project actions: subscribe useVoteRealtime + getVoteCounts to the derived entityType so submission votes don't get routed through the campaign channel; merge realtime vote payloads so a teammate's update cannot wipe the viewer's userVote selection; split the cancel flow into separate on-chain cancel and off-chain delete steps with distinct toasts and a reportError on the delete failure. - Voters tab: implement Load More pagination with totalItems tracking and id-based dedupe so the list is no longer silently capped at 20; merge realtime vote payloads so onVoteDeleted no longer force-clears userVote. - Team member card: only react to Enter/Space when the card itself is focused so nested profile/email links are not stolen. - Team tab: drop the invalid member.email vs creator.username comparison in matchesCreator (VMCreator has no email field). - Deduplicate formatRelative into utils.ts (shared between backer-card and voter-row) and add a Number.isFinite guard. - Delete the now-dead components/project-details/project-milestone/index.tsx (no consumers after the project layout retirement). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
app/(landing)/projects/[slug]/page.tsx (1)
211-221: Unreachable fallback branch.Given that
activeTabis typed asProjectTabValue(a union of'details' | 'team' | 'milestones' | 'voters' | 'backers' | 'comments') and all six values are explicitly handled in the preceding conditionals, this fallback branch can never execute. Consider removing it or, if it's defensive against future tab additions, add a comment explaining the intent.♻️ Option 1: Remove unreachable code
{activeTab === 'comments' && <ProjectComments projectId={vm.id} />} - {activeTab !== 'details' && - activeTab !== 'team' && - activeTab !== 'milestones' && - activeTab !== 'voters' && - activeTab !== 'backers' && - activeTab !== 'comments' && ( - <div className='border-stepper-border bg-background-card flex min-h-[200px] items-center justify-center rounded-2xl border p-8 text-sm text-gray-500'> - {tabs.find(t => t.value === activeTab)?.label} content coming - soon - </div> - )}♻️ Option 2: Add exhaustiveness check comment
+ {/* Defensive fallback for future tab additions — currently unreachable */} {activeTab !== 'details' &&🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/projects/[slug]/page.tsx around lines 211 - 221, The conditional rendering branch with the fallback div is unreachable because activeTab is typed as ProjectTabValue ('details'|'team'|'milestones'|'voters'|'backers'|'comments') and all those values are already handled; remove the fallback JSX (the div that shows "{tabs.find(t => t.value === activeTab)?.label} content coming soon") to eliminate dead code, or if you want a defensive guard for future tab values keep the branch but add a clear comment referencing activeTab and ProjectTabValue explaining it is intentional as an exhaustiveness fallback (or replace it with an explicit exhaustiveness check using a never/throw pattern) so reviewers understand the intent.app/(landing)/campaigns/[slug]/page.tsx (1)
145-209: Consider extracting shared page components to reduce duplication.
ProjectPageContent,ProjectPageSkeleton, andfetchAsSubmissionare nearly identical betweencampaigns/[slug]/page.tsxandprojects/[slug]/page.tsx. Consider extracting these into shared components/utilities underapp/(landing)/projects/[slug]/components/to maintain consistency and reduce maintenance burden.The campaigns page could then import and reuse:
import { ProjectPageContent, ProjectPageSkeleton } from '../projects/[slug]/components/page-content'; import { fetchAsSubmission } from '../projects/[slug]/lib/fetch-submission';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/campaigns/[slug]/page.tsx around lines 145 - 209, ProjectPageContent, ProjectPageSkeleton and the fetchAsSubmission logic are duplicated between campaigns/[slug]/page.tsx and projects/[slug]/page.tsx; extract the shared UI and data utilities into a single reusable module (e.g., app/(landing)/projects/[slug]/components/page-content and lib/fetch-submission) and update campaigns/[slug]/page.tsx to import ProjectPageContent, ProjectPageSkeleton and fetchAsSubmission instead of redefining them. Concretely: move the ProjectPageContent and ProjectPageSkeleton components plus any helper hooks they rely on (e.g., buildProjectTabs, ProjectTabs usage) into a shared file and export them; move the fetchAsSubmission implementation into a shared lib and export; then replace the duplicated definitions in campaigns/[slug]/page.tsx with imports and ensure props/types (ProjectViewModel, ProjectTabValue) are exported or re-exported so existing usages (vm, isSubmission, onRefresh, ProjectComments, HeroSection) compile.app/(landing)/projects/[slug]/components/utils.ts (1)
49-70: Minor inconsistency in time unit formatting.The function uses
${diffSec}s ago(abbreviated) for seconds butminute${min === 1 ? '' : 's'} ago(full word) for all other units. Consider using consistent formatting.♻️ Suggested fix for consistent formatting
- if (diffSec < 60) return `${diffSec}s ago`; + if (diffSec < 60) return `${diffSec} second${diffSec === 1 ? '' : 's'} ago`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/projects/[slug]/components/utils.ts around lines 49 - 70, The formatRelative function mixes abbreviated "s" for seconds with full words for other units; update the seconds branch to use the same full-word pluralization as the others by replacing `${diffSec}s ago` with `${diffSec} second${diffSec === 1 ? '' : 's'} ago` so seconds follow the consistent "N second(s) ago" pattern while leaving minute/hour/day branches unchanged.app/(landing)/projects/[slug]/components/backers-tab.tsx (1)
147-150: Minor grammar issue in CTA text.When
totalBackers === 1, the text reads "Join 1 other and back..." which is slightly awkward since the user would be the second backer, not joining "1 other". Consider adjusting the copy.♻️ Suggested fix
<h3 className='text-xl font-bold text-white sm:text-2xl'> - Join {totalBackers} {totalBackers === 1 ? 'other' : 'others'} and back{' '} + Join {totalBackers} {totalBackers === 1 ? 'backer' : 'backers'} supporting{' '} {vm.title} </h3>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/projects/[slug]/components/backers-tab.tsx around lines 147 - 150, The CTA copy reads awkwardly when totalBackers === 1; update the h3 in backers-tab.tsx so when totalBackers === 1 it uses a clearer phrase like "Be the second to back {vm.title}" and for all other counts keep the existing "Join {totalBackers} others and back {vm.title}" logic; modify the conditional around totalBackers in the h3 (reference: totalBackers, vm.title, the h3 element) to render the alternate string for the singular case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(landing)/projects/[slug]/components/milestones-tab.tsx:
- Around line 571-603: The button in FollowProgressCard only calls
handleScrollToTop and doesn't perform any follow action; import and use the
existing useFollow hook inside FollowProgressCard (e.g., const { follow,
unfollow, isFollowing, loading } = useFollow(projectIdOrSlug)) and replace
handleScrollToTop with a composed handler (e.g., handleFollowClick) that calls
follow (or toggles follow/unfollow) and then scrolls to top; update the Button
to call handleFollowClick, show loading/disabled state while loading, and update
the button label to reflect isFollowing (e.g., "Watching" vs "Watch Project") or
keep scroll behavior only if follow succeeds, and ensure you reference
FollowProgressCard, handleScrollToTop (rename/replace), Button, and useFollow
when making the changes.
In `@app/`(landing)/projects/[slug]/components/voters-tab.tsx:
- Around line 136-148: The initial fetch sets vote state with setVoteCounts but
currently forces userVote: null, dropping any userVote returned by the API;
update the assignment in the voters-tab component to use the backend value
(response.data.voteCounts.userVote) instead of null (optionally falling back to
null/undefined if absent) so the UI reflects the current user's vote on first
load when calling setVoteCounts.
---
Nitpick comments:
In `@app/`(landing)/campaigns/[slug]/page.tsx:
- Around line 145-209: ProjectPageContent, ProjectPageSkeleton and the
fetchAsSubmission logic are duplicated between campaigns/[slug]/page.tsx and
projects/[slug]/page.tsx; extract the shared UI and data utilities into a single
reusable module (e.g., app/(landing)/projects/[slug]/components/page-content and
lib/fetch-submission) and update campaigns/[slug]/page.tsx to import
ProjectPageContent, ProjectPageSkeleton and fetchAsSubmission instead of
redefining them. Concretely: move the ProjectPageContent and ProjectPageSkeleton
components plus any helper hooks they rely on (e.g., buildProjectTabs,
ProjectTabs usage) into a shared file and export them; move the
fetchAsSubmission implementation into a shared lib and export; then replace the
duplicated definitions in campaigns/[slug]/page.tsx with imports and ensure
props/types (ProjectViewModel, ProjectTabValue) are exported or re-exported so
existing usages (vm, isSubmission, onRefresh, ProjectComments, HeroSection)
compile.
In `@app/`(landing)/projects/[slug]/components/backers-tab.tsx:
- Around line 147-150: The CTA copy reads awkwardly when totalBackers === 1;
update the h3 in backers-tab.tsx so when totalBackers === 1 it uses a clearer
phrase like "Be the second to back {vm.title}" and for all other counts keep the
existing "Join {totalBackers} others and back {vm.title}" logic; modify the
conditional around totalBackers in the h3 (reference: totalBackers, vm.title,
the h3 element) to render the alternate string for the singular case.
In `@app/`(landing)/projects/[slug]/components/utils.ts:
- Around line 49-70: The formatRelative function mixes abbreviated "s" for
seconds with full words for other units; update the seconds branch to use the
same full-word pluralization as the others by replacing `${diffSec}s ago` with
`${diffSec} second${diffSec === 1 ? '' : 's'} ago` so seconds follow the
consistent "N second(s) ago" pattern while leaving minute/hour/day branches
unchanged.
In `@app/`(landing)/projects/[slug]/page.tsx:
- Around line 211-221: The conditional rendering branch with the fallback div is
unreachable because activeTab is typed as ProjectTabValue
('details'|'team'|'milestones'|'voters'|'backers'|'comments') and all those
values are already handled; remove the fallback JSX (the div that shows
"{tabs.find(t => t.value === activeTab)?.label} content coming soon") to
eliminate dead code, or if you want a defensive guard for future tab values keep
the branch but add a clear comment referencing activeTab and ProjectTabValue
explaining it is intentional as an exhaustiveness fallback (or replace it with
an explicit exhaustiveness check using a never/throw pattern) so reviewers
understand the intent.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 20e116b0-ccf1-4cee-a509-97d27c42029d
📒 Files selected for processing (12)
app/(landing)/campaigns/[slug]/page.tsxapp/(landing)/projects/[slug]/components/backer-card.tsxapp/(landing)/projects/[slug]/components/backers-tab.tsxapp/(landing)/projects/[slug]/components/milestones-tab.tsxapp/(landing)/projects/[slug]/components/project-actions.tsxapp/(landing)/projects/[slug]/components/team-member-card.tsxapp/(landing)/projects/[slug]/components/team-tab.tsxapp/(landing)/projects/[slug]/components/utils.tsapp/(landing)/projects/[slug]/components/voter-row.tsxapp/(landing)/projects/[slug]/components/voters-tab.tsxapp/(landing)/projects/[slug]/page.tsxcomponents/project-details/project-milestone/index.tsx
💤 Files with no reviewable changes (1)
- components/project-details/project-milestone/index.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- app/(landing)/projects/[slug]/components/team-member-card.tsx
- app/(landing)/projects/[slug]/components/voter-row.tsx
- app/(landing)/projects/[slug]/components/team-tab.tsx
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
app/(landing)/projects/[slug]/components/voters-tab.tsx (2)
196-205: Redundant response structure check in deduplication loop.The inner ternary
response.data && 'voters' in response.data ? response.data.voters : []is unnecessary since the enclosingifblock (line 188-192) already guaranteesresponse.data.votersexists. This adds cognitive noise.Suggested simplification
setVoters(prev => { const seen = new Set(prev.map(v => v.id)); const next = [...prev]; - for (const v of response.data && 'voters' in response.data - ? response.data.voters - : []) { + for (const v of response.data.voters) { if (!seen.has(v.id)) next.push(v); } return next; });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/projects/[slug]/components/voters-tab.tsx around lines 196 - 205, The deduplication inside the setVoters updater is using an unnecessary ternary guard (response.data && 'voters' in response.data ? response.data.voters : []) even though the outer if already ensures response.data.voters exists; simplify the loop in the setVoters callback to iterate directly over response.data.voters, keeping the seen Set and push logic intact (refer to setVoters and response.data.voters in voters-tab.tsx) to remove the redundant conditional and reduce cognitive noise.
155-160: Inconsistent nullish coalescing between fallback branches.Line 142 uses
?? null(nullish coalescing) while line 159 uses|| null(logical OR). For consistency and to avoid accidentally converting falsy values like0or''(though unlikely forVoteType), prefer nullish coalescing throughout.Suggested fix
setVoteCounts({ upvotes: counts.upvotes, downvotes: counts.downvotes, totalVotes: counts.totalVotes, - userVote: counts.userVote || null, + userVote: counts.userVote ?? null, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/projects/[slug]/components/voters-tab.tsx around lines 155 - 160, The assignment to state via setVoteCounts uses inconsistent fallback operators: change the userVote fallback from using logical OR to nullish coalescing so it becomes userVote: counts.userVote ?? null; update the setVoteCounts call (the object with upvotes, downvotes, totalVotes, userVote) to use ?? null for userVote to match the other usage and avoid converting falsy values accidentally.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(landing)/projects/[slug]/components/milestones-tab.tsx:
- Around line 389-397: The canSubmit check is too narrow: it only accepts
'resubmission_required' but mapStatus accepts multiple resubmission variants,
causing mismatch; update the canSubmit logic in the milestone row (see variables
reviewStatus and canSubmit) to treat all resubmission variants the same as
mapStatus does—either call the existing mapStatus/normalize function on
milestone.raw.reviewStatus and compare the normalized value, or expand the
conditional to include the other resubmission variants (e.g., hyphenated/space
forms) so the creator's actionable state matches the mapped status logic.
In `@app/`(landing)/projects/[slug]/components/voters-tab.tsx:
- Around line 101-112: The realtime handlers currently replace the entire voters
array (setVoters(data.voters)) which truncates any paginated/locally loaded
voters; change each handler (onVoteUpdated, onVoteCreated, onVoteDeleted) to
merge incoming voters into the existing state instead of replacing it by using a
merge helper (e.g., create or reuse a mergeVoters(prev, incoming) function) that
upserts incoming entries by id and preserves/concats existing pages not present
in data.voters, and keep the existing setVoteCounts(prev =>
mergeVoteCounts(prev, data.voteCounts)) logic.
---
Nitpick comments:
In `@app/`(landing)/projects/[slug]/components/voters-tab.tsx:
- Around line 196-205: The deduplication inside the setVoters updater is using
an unnecessary ternary guard (response.data && 'voters' in response.data ?
response.data.voters : []) even though the outer if already ensures
response.data.voters exists; simplify the loop in the setVoters callback to
iterate directly over response.data.voters, keeping the seen Set and push logic
intact (refer to setVoters and response.data.voters in voters-tab.tsx) to remove
the redundant conditional and reduce cognitive noise.
- Around line 155-160: The assignment to state via setVoteCounts uses
inconsistent fallback operators: change the userVote fallback from using logical
OR to nullish coalescing so it becomes userVote: counts.userVote ?? null; update
the setVoteCounts call (the object with upvotes, downvotes, totalVotes,
userVote) to use ?? null for userVote to match the other usage and avoid
converting falsy values accidentally.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e20587cd-13c4-458b-b5d0-abb7f0ecd5b1
📒 Files selected for processing (2)
app/(landing)/projects/[slug]/components/milestones-tab.tsxapp/(landing)/projects/[slug]/components/voters-tab.tsx
| const reviewStatus = (milestone.raw.reviewStatus || 'pending').toLowerCase(); | ||
| const canSubmit = | ||
| isCreator && | ||
| onChainId && | ||
| vm.campaign && | ||
| isFundedOrExecuting && | ||
| (reviewStatus === 'pending' || | ||
| reviewStatus === 'resubmission_required' || | ||
| reviewStatus === 'awaiting'); |
There was a problem hiding this comment.
Normalize resubmission variants in canSubmit to match mapStatus.
Line 396 only checks resubmission_required, but Line 124-128 accepts additional variants. For those variants, rows look actionable while the creator cannot submit.
Suggested fix
const canSubmit =
isCreator &&
onChainId &&
vm.campaign &&
isFundedOrExecuting &&
(reviewStatus === 'pending' ||
- reviewStatus === 'resubmission_required' ||
+ reviewStatus === 'resubmission_required' ||
+ reviewStatus === 'resubmission-required' ||
+ reviewStatus === 'resubmit_required' ||
+ reviewStatus === 'resubmit-required' ||
reviewStatus === 'awaiting');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(landing)/projects/[slug]/components/milestones-tab.tsx around lines 389
- 397, The canSubmit check is too narrow: it only accepts
'resubmission_required' but mapStatus accepts multiple resubmission variants,
causing mismatch; update the canSubmit logic in the milestone row (see variables
reviewStatus and canSubmit) to treat all resubmission variants the same as
mapStatus does—either call the existing mapStatus/normalize function on
milestone.raw.reviewStatus and compare the normalized value, or expand the
conditional to include the other resubmission variants (e.g., hyphenated/space
forms) so the creator's actionable state matches the mapped status logic.
| onVoteUpdated: data => { | ||
| setVoteCounts(prev => mergeVoteCounts(prev, data.voteCounts)); | ||
| setVoters(data.voters); | ||
| }, | ||
| onVoteCreated: data => { | ||
| setVoteCounts(prev => mergeVoteCounts(prev, data.voteCounts)); | ||
| setVoters(data.voters); | ||
| }, | ||
| onVoteDeleted: data => { | ||
| setVoteCounts(prev => mergeVoteCounts(prev, data.voteCounts)); | ||
| setVoters(data.voters); | ||
| }, |
There was a problem hiding this comment.
Realtime updates may truncate paginated voters list.
When the realtime hook fires, setVoters(data.voters) replaces the entire voters array. If the user has loaded multiple pages (e.g., 60 voters), a new vote event will replace that with only the voters included in the realtime payload (likely a smaller set), losing the extra loaded voters.
Consider merging instead of replacing:
Suggested approach
onVoteUpdated: data => {
setVoteCounts(prev => mergeVoteCounts(prev, data.voteCounts));
- setVoters(data.voters);
+ setVoters(prev => {
+ const incoming = new Map(data.voters.map(v => [v.id, v]));
+ // Update existing, keep any extras we've paginated in
+ return prev.map(v => incoming.get(v.id) ?? v)
+ .concat(data.voters.filter(v => !prev.some(p => p.id === v.id)));
+ });
},Apply similar logic to onVoteCreated and onVoteDeleted.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(landing)/projects/[slug]/components/voters-tab.tsx around lines 101 -
112, The realtime handlers currently replace the entire voters array
(setVoters(data.voters)) which truncates any paginated/locally loaded voters;
change each handler (onVoteUpdated, onVoteCreated, onVoteDeleted) to merge
incoming voters into the existing state instead of replacing it by using a merge
helper (e.g., create or reuse a mergeVoters(prev, incoming) function) that
upserts incoming entries by id and preserves/concats existing pages not present
in data.voters, and keep the existing setVoteCounts(prev =>
mergeVoteCounts(prev, data.voteCounts)) logic.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/(landing)/projects/[slug]/page.tsx (1)
67-70:⚠️ Potential issue | 🟠 MajorPersist the resolved entity type instead of reusing the query flag.
This flow can load
vmviafetchAsSubmission(slug)whileisSubmissionis stillfalse. After that,refreshDatakeeps re-fetching the project/crowdfunding path and downstream components still receive campaign mode, so fallback-loaded submissions can’t refresh or render consistently.Also applies to: 97-100, 123-129, 157-160
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/projects/[slug]/page.tsx around lines 67 - 70, The code is reusing the query flag isSubmission and not persisting the resolved entity type when fetchAsSubmission(slug) returns a VM, causing later refreshes to re-fetch the wrong path; change the flow to store the resolved entity mode (e.g., setResolvedEntityType or setIsSubmissionResolved) when you successfully load a submission via fetchAsSubmission(slug) and use that persisted flag in refreshData and rendering instead of the original query-derived isSubmission; update all similar branches that call fetchAsSubmission (the blocks around lines where fetchAsSubmission, setVm, and refreshData are used) to set the resolved flag immediately after result succeeds and read that flag for subsequent fetches and component rendering.
♻️ Duplicate comments (1)
app/(landing)/campaigns/[slug]/page.tsx (1)
132-134:⚠️ Potential issue | 🟠 MajorDon’t collapse non-404 fetch failures into
notFound().The fetch path now distinguishes misses from real backend failures, but this render branch still sends every error through the not-found boundary. A transient crowdfunding or hackathon outage will still look like a missing campaign.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/campaigns/[slug]/page.tsx around lines 132 - 134, The current branch calls notFound() for any error or missing vm, which hides real backend failures; change the condition in page.tsx so notFound() is only invoked for explicit 404/missing-campaign cases (e.g., check the fetch result's notFound flag or error.status === 404) and for other errors either rethrow the error (so the error boundary shows a proper failure) or render an error UI; update the check that currently reads "if (error || !vm) { notFound(); }" to discriminate 404s from transient/backend errors using the fetch response metadata or error.code.
🧹 Nitpick comments (1)
app/(landing)/projects/[slug]/components/details-tab.tsx (1)
339-355: Extract named handlers for these CTA buttons.The inline lambdas duplicate the same scroll-to-top behavior and miss the repo’s
handle*event-handler convention. Pulling this into a named callback also makes the empty state easier to extend later. As per coding guidelines, "Event handlers should start with 'handle' prefix (e.g., handleClick, handleSubmit)".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(landing)/projects/[slug]/components/details-tab.tsx around lines 339 - 355, Extract the duplicated inline onClick lambdas into named handler functions following the repo convention (e.g., handleScrollToTop and/or handleFollowProject) inside the details-tab.tsx component, replace both Button onClick props with those handler references, and ensure the handler performs the same safe check (typeof window !== 'undefined') and window.scrollTo({ top: 0, behavior: 'smooth' }); if you need a separate handleFollowProject, call handleScrollToTop from it so the Follow Project CTA can be extended later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(landing)/campaigns/[slug]/page.tsx:
- Around line 63-66: The route sometimes calls fetchAsSubmission(id) but does
not update the flag that indicates the fetched source, so downstream logic still
treats the data as a campaign; update the local state that tracks source
semantics before calling setVm and before refreshData so children see submission
semantics. Specifically, when you call fetchAsSubmission(id) (and similar calls
at the other locations), set the indicator derived from isSubmission (or a new
variable like fetchedIsSubmission) to true and pass that into setVm/refreshData
(or update the state property used by child components) so the component tree
receives submission semantics consistently.
In `@app/`(landing)/projects/[slug]/components/details-tab.tsx:
- Around line 47-56: The current early-return in DetailsTab (function
DetailsTab) hides DemoVideo and the in-tab support CTA when vm.description is
empty; change the render flow so DemoVideo and the support CTA are rendered
outside/above the description gate (i.e., render <DemoVideo vm={vm} /> and the
support CTA unconditionally or include them in the DetailsEmptyState) and only
gate the detailed description content with the hasDescription check; apply the
same change to the similar block at the other occurrence (lines 183-186) so demo
video and CTA are never skipped when description is missing.
- Around line 38-45: slugify currently produces empty strings for non-ASCII
titles and identical ids for repeated headings, causing duplicate DOM ids/React
keys and incorrect document.getElementById navigation; update the slug/id
generation used for outline headings (function slugify and the code path that
maps headings to ids) to (1) normalize/transliterate input (e.g., Unicode NFKD +
remove diacritics) and fallback to a deterministic hash (e.g., short SHA1 or
base64 of the title) when the slug becomes empty, and (2) ensure uniqueness by
appending a numeric suffix for duplicates using a local counter map keyed by the
base slug (e.g., if "intro" already exists produce "intro-1", "intro-2"), then
use these generated ids for element id attributes and React keys.
In `@app/`(landing)/projects/[slug]/page.tsx:
- Around line 152-154: The current code calls notFound() for any error or
missing vm; change it to only call notFound() for true 404s by checking
isApiNotFound(error) (or a dedicated missing flag returned from fetchData) and
otherwise surface the error (throw it or render an error state). Specifically,
in the branch that currently does "if (error || !vm) { notFound(); }", replace
with logic that calls notFound() only when isApiNotFound(error) || vm ===
null/undefined && fetchData indicated a missing resource, and for other error
cases re-throw the error or return an error component so non-404 failures hit
the route error boundary.
---
Outside diff comments:
In `@app/`(landing)/projects/[slug]/page.tsx:
- Around line 67-70: The code is reusing the query flag isSubmission and not
persisting the resolved entity type when fetchAsSubmission(slug) returns a VM,
causing later refreshes to re-fetch the wrong path; change the flow to store the
resolved entity mode (e.g., setResolvedEntityType or setIsSubmissionResolved)
when you successfully load a submission via fetchAsSubmission(slug) and use that
persisted flag in refreshData and rendering instead of the original
query-derived isSubmission; update all similar branches that call
fetchAsSubmission (the blocks around lines where fetchAsSubmission, setVm, and
refreshData are used) to set the resolved flag immediately after result succeeds
and read that flag for subsequent fetches and component rendering.
---
Duplicate comments:
In `@app/`(landing)/campaigns/[slug]/page.tsx:
- Around line 132-134: The current branch calls notFound() for any error or
missing vm, which hides real backend failures; change the condition in page.tsx
so notFound() is only invoked for explicit 404/missing-campaign cases (e.g.,
check the fetch result's notFound flag or error.status === 404) and for other
errors either rethrow the error (so the error boundary shows a proper failure)
or render an error UI; update the check that currently reads "if (error || !vm)
{ notFound(); }" to discriminate 404s from transient/backend errors using the
fetch response metadata or error.code.
---
Nitpick comments:
In `@app/`(landing)/projects/[slug]/components/details-tab.tsx:
- Around line 339-355: Extract the duplicated inline onClick lambdas into named
handler functions following the repo convention (e.g., handleScrollToTop and/or
handleFollowProject) inside the details-tab.tsx component, replace both Button
onClick props with those handler references, and ensure the handler performs the
same safe check (typeof window !== 'undefined') and window.scrollTo({ top: 0,
behavior: 'smooth' }); if you need a separate handleFollowProject, call
handleScrollToTop from it so the Follow Project CTA can be extended later.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f91ff786-dfd7-4e76-a0da-606a2ef6b411
📒 Files selected for processing (3)
app/(landing)/campaigns/[slug]/page.tsxapp/(landing)/projects/[slug]/components/details-tab.tsxapp/(landing)/projects/[slug]/page.tsx
| // ── Hackathon submission path ── | ||
| if (isSubmission) { | ||
| const result = await fetchSubmission(id); | ||
| const result = await fetchAsSubmission(id); | ||
| if (!cancelled) setVm(result); |
There was a problem hiding this comment.
Track the fetched source before passing isSubmission downstream.
This route can also fall back to fetchAsSubmission(id) while isSubmission stays false. When that happens, refreshData keeps querying crowdfunding and the child components still receive campaign semantics instead of submission semantics.
Also applies to: 84-87, 110-119, 137-140
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(landing)/campaigns/[slug]/page.tsx around lines 63 - 66, The route
sometimes calls fetchAsSubmission(id) but does not update the flag that
indicates the fetched source, so downstream logic still treats the data as a
campaign; update the local state that tracks source semantics before calling
setVm and before refreshData so children see submission semantics. Specifically,
when you call fetchAsSubmission(id) (and similar calls at the other locations),
set the indicator derived from isSubmission (or a new variable like
fetchedIsSubmission) to true and pass that into setVm/refreshData (or update the
state property used by child components) so the component tree receives
submission semantics consistently.
| function slugify(text: string) { | ||
| return text | ||
| .toLowerCase() | ||
| .trim() | ||
| .replace(/[^a-z0-9\s-]/g, '') | ||
| .replace(/\s+/g, '-') | ||
| .replace(/-+/g, '-'); | ||
| } |
There was a problem hiding this comment.
Generate unique, non-empty ids for outline headings.
slugify() can return the same value for repeated headings and '' for non-ASCII-only titles. That gives you duplicate DOM ids / React keys, and document.getElementById(id) will jump to the wrong section.
Suggested change
- const items: OutlineItem[] = headings.map(h => {
- const label = h.textContent ?? '';
- const id = h.id || slugify(label);
+ const usedIds = new Map<string, number>();
+ const items: OutlineItem[] = headings.map(h => {
+ const label = h.textContent?.trim() || 'Section';
+ const baseId = h.id || slugify(label) || 'section';
+ const next = (usedIds.get(baseId) ?? 0) + 1;
+ usedIds.set(baseId, next);
+ const id = next === 1 ? baseId : `${baseId}-${next}`;
h.id = id;
h.style.scrollMarginTop = '96px';
return { id, label };
});Also applies to: 79-84
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(landing)/projects/[slug]/components/details-tab.tsx around lines 38 -
45, slugify currently produces empty strings for non-ASCII titles and identical
ids for repeated headings, causing duplicate DOM ids/React keys and incorrect
document.getElementById navigation; update the slug/id generation used for
outline headings (function slugify and the code path that maps headings to ids)
to (1) normalize/transliterate input (e.g., Unicode NFKD + remove diacritics)
and fallback to a deterministic hash (e.g., short SHA1 or base64 of the title)
when the slug becomes empty, and (2) ensure uniqueness by appending a numeric
suffix for duplicates using a local counter map keyed by the base slug (e.g., if
"intro" already exists produce "intro-1", "intro-2"), then use these generated
ids for element id attributes and React keys.
| export function DetailsTab({ vm, isSubmission, onRefresh }: DetailsTabProps) { | ||
| const hasDescription = !!vm.description?.trim(); | ||
|
|
||
| if (!hasDescription) { | ||
| return <DetailsEmptyState vm={vm} />; | ||
| } | ||
|
|
||
| return ( | ||
| <DetailsContent vm={vm} isSubmission={isSubmission} onRefresh={onRefresh} /> | ||
| ); |
There was a problem hiding this comment.
Keep the non-markdown sections outside the empty-description gate.
This early return skips DemoVideo and the in-tab support CTA entirely. If a project has vm.demoVideo but no description, the details tab now renders only the empty state and hides valid content.
Suggested change
export function DetailsTab({ vm, isSubmission, onRefresh }: DetailsTabProps) {
- const hasDescription = !!vm.description?.trim();
-
- if (!hasDescription) {
- return <DetailsEmptyState vm={vm} />;
- }
-
return (
<DetailsContent vm={vm} isSubmission={isSubmission} onRefresh={onRefresh} />
);
}
@@
- {loading ? (
+ {!vm.description?.trim() ? (
+ <DetailsEmptyState vm={vm} />
+ ) : loading ? (Also applies to: 183-186
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(landing)/projects/[slug]/components/details-tab.tsx around lines 47 -
56, The current early-return in DetailsTab (function DetailsTab) hides DemoVideo
and the in-tab support CTA when vm.description is empty; change the render flow
so DemoVideo and the support CTA are rendered outside/above the description gate
(i.e., render <DemoVideo vm={vm} /> and the support CTA unconditionally or
include them in the DetailsEmptyState) and only gate the detailed description
content with the hasDescription check; apply the same change to the similar
block at the other occurrence (lines 183-186) so demo video and CTA are never
skipped when description is missing.
| if (error || !vm) { | ||
| notFound(); | ||
| } |
There was a problem hiding this comment.
Separate “not found” from “fetch failed”.
This branch still routes every error to notFound(), including the non-404 cases that isApiNotFound() preserved above. Keep a dedicated missing/not-missing discriminator from fetchData and only call notFound() for confirmed misses; otherwise surface an error state or throw to the route error boundary.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(landing)/projects/[slug]/page.tsx around lines 152 - 154, The current
code calls notFound() for any error or missing vm; change it to only call
notFound() for true 404s by checking isApiNotFound(error) (or a dedicated
missing flag returned from fetchData) and otherwise surface the error (throw it
or render an error state). Specifically, in the branch that currently does "if
(error || !vm) { notFound(); }", replace with logic that calls notFound() only
when isApiNotFound(error) || vm === null/undefined && fetchData indicated a
missing resource, and for other error cases re-throw the error or return an
error component so non-404 failures hit the route error boundary.
Summary by CodeRabbit
New Features
Removed Features