[feat]: homepage upcoming course rounds#1770
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds a new UpcomingRounds homepage section: a React component that queries a new TRPC endpoint to retrieve, partition, and render upcoming course rounds (intensive vs part-time), plus Storybook stories and server-side aggregation logic in the course-rounds router. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser as Browser (Homepage)
participant UI as UpcomingRounds Component
participant TRPC as trpc.courseRounds.getAllUpcomingRounds
participant DB as Database (Prisma)
Note over UI,TRPC: User loads homepage → component mounts
Browser->>UI: render
UI->>TRPC: query getAllUpcomingRounds()
TRPC->>DB: fetch active courses & rounds (apply filters)
DB-->>TRPC: course & round records
TRPC->>TRPC: compute date ranges, enrich metadata, sort, group
TRPC-->>UI: return { intense, partTime }
UI->>Browser: render grouped round cards / links
Note right of Browser: Storybook MSW intercepts the TRPC call in stories
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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: 1
🧹 Nitpick comments (4)
apps/website/src/server/routers/course-rounds.ts (2)
182-226: Consider extracting shared helper functions to reduce duplication.The
formatDateandformatDateRangefunctions are duplicated verbatim fromgetRoundsForCourse(lines 52-100). Additionally, the sorting logic (lines 261-276) and grouping logic (lines 279-282) are also duplicated.Extract these into shared helper functions defined once at the module level or in a utility file.
+// Move to top of file, before router definition +const formatDate = (isoDate: string) => { + const date = new Date(isoDate); + const day = String(date.getUTCDate()).padStart(2, '0'); + const month = date.toLocaleDateString('en-US', { month: 'short', timeZone: 'UTC' }); + return `${day} ${month}`; +}; + +const formatDateRange = ( + firstDate: string | null, + lastDate: string | null, + numberOfUnits: number | null, + intensity: string | null, + separator = ' - ', +) => { + if (!firstDate) return null; + const first = new Date(firstDate); + let computedLast: Date | null = null; + if (numberOfUnits && numberOfUnits > 0) { + const isPartTime = intensity?.toLowerCase() === 'part-time'; + const daysToAdd = isPartTime ? (numberOfUnits * 7) - 1 : numberOfUnits - 1; + computedLast = new Date(first.getTime() + daysToAdd * 24 * 60 * 60 * 1000); + } else if (lastDate) { + computedLast = new Date(lastDate); + } else { + return null; + } + const firstDay = String(first.getUTCDate()).padStart(2, '0'); + const lastDay = String(computedLast.getUTCDate()).padStart(2, '0'); + const firstMonth = first.toLocaleDateString('en-US', { month: 'short', timeZone: 'UTC' }); + const lastMonth = computedLast.toLocaleDateString('en-US', { month: 'short', timeZone: 'UTC' }); + return `${firstDay} ${firstMonth}${separator}${lastDay} ${lastMonth}`; +};Then call with
formatDateRange(..., ' - ')for course pages andformatDateRange(..., ' – ')for homepage (en-dash).
162-165: Hardcoded slug exclusion is fragile.The
'future-of-ai'slug is a magic string. If this course slug changes in the CMS/database, this filter will silently break.Consider defining this as a named constant or adding a database field (e.g.,
excludeFromHomepage) to make the exclusion explicit and maintainable.+const EXCLUDED_COURSE_SLUGS = ['future-of-ai'] as const; + // Filter out self-paced courses (cadence = 'self-paced' or slug = 'future-of-ai') const courses = allCourses.filter( - (course) => course.cadence?.toLowerCase() !== 'self-paced' && course.slug !== 'future-of-ai', + (course) => course.cadence?.toLowerCase() !== 'self-paced' && !EXCLUDED_COURSE_SLUGS.includes(course.slug as typeof EXCLUDED_COURSE_SLUGS[number]), );apps/website/src/components/homepage/UpcomingRounds.tsx (1)
188-196: Fallback toround.idfor courseId defeats diversity logic.If
round.courseIdis nullish, usinground.idas the fallback means every such round would be treated as unique, potentially allowing multiple rounds from the same course to be selected (if courseId is missing from all).Per the router code, rounds without valid courseId are already filtered out (line 234-236 in course-rounds.ts), so this fallback may be unreachable. Consider removing it or adding a comment explaining the defensive nature.
for (const round of rounds) { if (selectedRounds.length >= 3) break; - const courseId = round.courseId ?? round.id; + // courseId is guaranteed non-null from router filtering + const courseId = round.courseId; if (!seenCourses.has(courseId)) { selectedRounds.push(round); seenCourses.add(courseId);apps/website/src/components/homepage/UpcomingRounds.stories.tsx (1)
5-16: Consider reusing the inferredRoundtype from the router.The local
Roundtype duplicates the shape from the router. If the router output changes, this type may drift. You can import and reuse the inferred type for consistency.+import type { inferRouterOutputs } from '@trpc/server'; +import type { AppRouter } from '../../server/routers/_app'; + +type RouterOutput = inferRouterOutputs<AppRouter>; +type Round = RouterOutput['courseRounds']['getAllUpcomingRounds']['intense'][number]; -type Round = { - id: string; - courseId: string; - courseTitle: string; - courseSlug: string; - intensity: string; - applicationDeadline: string; - applicationDeadlineRaw: string; - firstDiscussionDateRaw: string; - dateRange: string; - numberOfUnits: number; -};
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/website/src/components/homepage/UpcomingRounds.stories.tsx(1 hunks)apps/website/src/components/homepage/UpcomingRounds.tsx(1 hunks)apps/website/src/pages/index.tsx(2 hunks)apps/website/src/server/routers/course-rounds.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1758
File: apps/website/src/pages/courses/index.tsx:416-418
Timestamp: 2025-12-05T22:16:53.272Z
Learning: In apps/website/src/pages/courses/index.tsx, the CourseCard component fetches rounds data via trpc.courseRounds.getRoundsForCourse.useQuery even though the same data is fetched in useSortedCourses. This is intentional because tRPC (which uses React Query) automatically deduplicates network requests with identical query keys, so only one network request is made and the result is shared across components via the query cache.
📚 Learning: 2025-06-19T08:37:04.853Z
Learnt from: tolgadur
Repo: bluedotimpact/bluedot PR: 1029
File: apps/website/src/components/homepage/CourseSection.test.tsx:37-81
Timestamp: 2025-06-19T08:37:04.853Z
Learning: In the BlueDot codebase, the `mockCourse` helper function in test files provides default values for all Course properties including `isFeatured: false` and `isNew: false`. When creating mock course objects, only properties that differ from the defaults need to be explicitly specified in the overrides object.
Applied to files:
apps/website/src/components/homepage/UpcomingRounds.stories.tsx
📚 Learning: 2025-12-05T22:16:53.272Z
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1758
File: apps/website/src/pages/courses/index.tsx:416-418
Timestamp: 2025-12-05T22:16:53.272Z
Learning: In apps/website/src/pages/courses/index.tsx, the CourseCard component fetches rounds data via trpc.courseRounds.getRoundsForCourse.useQuery even though the same data is fetched in useSortedCourses. This is intentional because tRPC (which uses React Query) automatically deduplicates network requests with identical query keys, so only one network request is made and the result is shared across components via the query cache.
Applied to files:
apps/website/src/components/homepage/UpcomingRounds.stories.tsxapps/website/src/components/homepage/UpcomingRounds.tsxapps/website/src/pages/index.tsxapps/website/src/server/routers/course-rounds.ts
📚 Learning: 2025-11-03T01:57:21.418Z
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1554
File: apps/website/src/components/homepage/OurCommunitySection.tsx:11-17
Timestamp: 2025-11-03T01:57:21.418Z
Learning: In apps/website/src/components/homepage/OurCommunitySection.tsx, the CommunityMember type is intentionally duplicated (also exists in apps/website/src/components/lander/components/CommunityMembersSubSection.tsx) to keep the component changeable for potential future refactoring.
Applied to files:
apps/website/src/components/homepage/UpcomingRounds.tsxapps/website/src/pages/index.tsx
🧬 Code graph analysis (4)
apps/website/src/components/homepage/UpcomingRounds.stories.tsx (2)
apps/website/src/components/homepage/UpcomingRounds.tsx (1)
UpcomingRounds(21-84)apps/website/src/__tests__/trpcMswSetup.browser.ts (1)
trpcStorybookMsw(10-16)
apps/website/src/components/homepage/UpcomingRounds.tsx (5)
apps/website/src/server/routers/_app.ts (1)
AppRouter(36-36)apps/website/src/components/lander/course-content/AgiStrategyContent.tsx (1)
AGI_STRATEGY_APPLICATION_URL(15-15)apps/website/src/components/lander/course-content/BioSecurityContent.tsx (1)
BIOSECURITY_APPLICATION_URL(15-15)apps/website/src/components/lander/course-content/TechnicalAiSafetyContent.tsx (1)
TECHNICAL_AI_SAFETY_APPLICATION_URL(16-16)apps/website/src/components/lander/course-content/AiGovernanceContent.tsx (1)
AI_GOVERNANCE_APPLICATION_URL(12-12)
apps/website/src/pages/index.tsx (1)
apps/website/src/components/homepage/UpcomingRounds.tsx (1)
UpcomingRounds(21-84)
apps/website/src/server/routers/course-rounds.ts (3)
apps/website/src/server/trpc.ts (1)
publicProcedure(126-126)libraries/db/src/index.ts (2)
courseTable(9-9)applicationsRoundTable(23-23)libraries/db/src/schema.ts (2)
courseTable(61-146)applicationsRoundTable(557-591)
🔇 Additional comments (5)
apps/website/src/server/routers/course-rounds.ts (1)
149-285: Approve the newgetAllUpcomingRoundsendpoint.The implementation correctly:
- Filters active courses and excludes self-paced cadence
- Applies the 12-hour deadline threshold for timezone tolerance
- Enriches rounds with course metadata and filters orphaned rounds
- Sorts by start date then duration
- Groups by intensity type
The logic aligns well with the PR requirements.
apps/website/src/components/homepage/UpcomingRounds.tsx (2)
21-84: Good implementation of the UpcomingRounds component.The component correctly:
- Displays a loading skeleton while fetching
- Returns null when no data or no rounds exist
- Uses the
selectDiverseRoundshelper to pick diverse courses- Renders separate groups for intensive and part-time rounds
- Provides accessible links with appropriate
aria-labelandtarget="_blank"attributes
14-19: Add fallback URL logic for unmapped courses in UpcomingRounds.The map
COURSE_APPLICATION_URLShas no fallback if a course slug is missing—it defaults to an empty string, breaking the apply link. Unlikeapps/website/src/pages/courses/index.tsx, which falls back tocourse.detailsUrl || course.path, this component offers no safety net. If a course is added to the database and appears in upcoming rounds without being added to the map, users will encounter broken apply links.Consider either:
- Adding a fallback to
course.detailsUrlorcourse.path(passed via the round object)- Adding a database constraint or validation to prevent courses without application URLs from appearing in rounds
apps/website/src/pages/index.tsx (1)
9-9: LGTM!The integration correctly imports and positions the
UpcomingRoundscomponent afterCourseSectionand before the divider, as specified in the PR objectives.Also applies to: 90-90
apps/website/src/components/homepage/UpcomingRounds.stories.tsx (1)
96-121: Story setup looks good.The Storybook configuration correctly:
- Uses fullscreen layout for realistic preview
- Provides descriptive component documentation
- Sets up MSW handlers to mock the tRPC endpoint
- Returns properly structured mock data matching the API contract
| const RoundItem = ({ round }: RoundItemProps) => { | ||
| const applicationUrl = COURSE_APPLICATION_URLS[round.courseSlug] || ''; | ||
| const separator = applicationUrl.includes('?') ? '&' : '?'; | ||
| const applyUrl = `${applicationUrl}${separator}prefill_%5B%3E%5D%20Round=${round.id}`; |
There was a problem hiding this comment.
Missing course slug results in broken apply links.
When round.courseSlug isn't in COURSE_APPLICATION_URLS, applicationUrl becomes an empty string, producing a malformed URL like ?prefill_%5B%3E%5D%20Round=.... Users clicking "Apply now" will navigate to an invalid page.
Consider filtering out rounds without valid application URLs, or falling back to a generic courses page.
const RoundItem = ({ round }: RoundItemProps) => {
const applicationUrl = COURSE_APPLICATION_URLS[round.courseSlug] || '';
+
+ // Skip rendering if no valid application URL exists
+ if (!applicationUrl) {
+ console.warn(`Missing application URL for course: ${round.courseSlug}`);
+ return null;
+ }
+
const separator = applicationUrl.includes('?') ? '&' : '?';
const applyUrl = `${applicationUrl}${separator}prefill_%5B%3E%5D%20Round=${round.id}`;Alternatively, filter in selectDiverseRounds or before rendering.
There was a problem hiding this comment.
Yep, this is temporary because we're about to sync the application URL from airtable (e.g #1775)
There was a problem hiding this comment.
@alyssia-x, thanks for clarifying! That makes sense—syncing the application URLs from Airtable in #1775 will handle this comprehensively. I appreciate the context about the upcoming change. 👍
🧠 Learnings used
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1758
File: apps/website/src/pages/courses/index.tsx:416-418
Timestamp: 2025-12-05T22:16:53.272Z
Learning: In apps/website/src/pages/courses/index.tsx, the CourseCard component fetches rounds data via trpc.courseRounds.getRoundsForCourse.useQuery even though the same data is fetched in useSortedCourses. This is intentional because tRPC (which uses React Query) automatically deduplicates network requests with identical query keys, so only one network request is made and the result is shared across components via the query cache.
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1758
File: apps/website/src/pages/courses/index.tsx:53-53
Timestamp: 2025-12-05T22:16:07.019Z
Learning: In apps/website/src/pages/courses/index.tsx, the `isSelfPacedCourse` function intentionally includes a hardcoded check for `course.slug === 'future-of-ai'` alongside the `course.cadence?.toLowerCase() === 'self-paced'` check. This hardcoded slug check should be retained.
Greptile OverviewGreptile SummaryImplements a new Key changes:
Issues found:
Confidence Score: 3/5
Important Files ChangedFile Analysis
Sequence DiagramsequenceDiagram
participant User
participant HomePage
participant UpcomingRounds
participant tRPC
participant CourseRoundsRouter
participant Database
User->>HomePage: Visit homepage
HomePage->>UpcomingRounds: Render component
UpcomingRounds->>tRPC: getAllUpcomingRounds.useQuery()
tRPC->>CourseRoundsRouter: getAllUpcomingRounds()
CourseRoundsRouter->>Database: SELECT active courses
Database-->>CourseRoundsRouter: Return courses (excl. self-paced)
CourseRoundsRouter->>Database: SELECT upcoming rounds (deadline > now - 12h)
Database-->>CourseRoundsRouter: Return all rounds
CourseRoundsRouter->>CourseRoundsRouter: Enrich rounds with course info
CourseRoundsRouter->>CourseRoundsRouter: Format dates with en-dash
CourseRoundsRouter->>CourseRoundsRouter: Sort by start date, then duration
CourseRoundsRouter->>CourseRoundsRouter: Group by intensity (intensive/part-time)
CourseRoundsRouter-->>tRPC: Return grouped rounds
tRPC-->>UpcomingRounds: Return data
UpcomingRounds->>UpcomingRounds: selectDiverseRounds() - pick up to 3 from different courses
UpcomingRounds->>User: Display rounds with apply links
|
| const COURSE_APPLICATION_URLS: Record<string, string> = { | ||
| 'agi-strategy': AGI_STRATEGY_APPLICATION_URL, | ||
| biosecurity: BIOSECURITY_APPLICATION_URL, | ||
| 'technical-ai-safety': TECHNICAL_AI_SAFETY_APPLICATION_URL, | ||
| governance: AI_GOVERNANCE_APPLICATION_URL, | ||
| }; |
There was a problem hiding this comment.
logic: missing governance key in the /courses page COURSE_APPLICATION_URLS map (only has 3 courses), but present here (has 4 courses). this creates inconsistency - governance rounds will have working apply links on homepage but broken ones on /courses page
suggest adding the missing entry to /courses/index.tsx or extracting this constant to a shared location
Context Used: Context from dashboard - Consider refactoring complex lines of code for better readability and to handle potential edge cases... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/website/src/components/homepage/UpcomingRounds.tsx
Line: 14:19
Comment:
**logic:** missing `governance` key in the `/courses` page `COURSE_APPLICATION_URLS` map (only has 3 courses), but present here (has 4 courses). this creates inconsistency - governance rounds will have working apply links on homepage but broken ones on `/courses` page
suggest adding the missing entry to `/courses/index.tsx` or extracting this constant to a shared location
**Context Used:** Context from `dashboard` - Consider refactoring complex lines of code for better readability and to handle potential edge cases... ([source](https://app.greptile.com/review/custom-context?memory=331d942d-90e3-4857-8dba-26213d1a47a6))
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Yeah governance dates / ability to apply was just added so a PR will be made for that too
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/website/src/components/homepage/UpcomingRounds.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1758
File: apps/website/src/pages/courses/index.tsx:416-418
Timestamp: 2025-12-05T22:16:53.272Z
Learning: In apps/website/src/pages/courses/index.tsx, the CourseCard component fetches rounds data via trpc.courseRounds.getRoundsForCourse.useQuery even though the same data is fetched in useSortedCourses. This is intentional because tRPC (which uses React Query) automatically deduplicates network requests with identical query keys, so only one network request is made and the result is shared across components via the query cache.
📚 Learning: 2025-12-05T22:16:53.272Z
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1758
File: apps/website/src/pages/courses/index.tsx:416-418
Timestamp: 2025-12-05T22:16:53.272Z
Learning: In apps/website/src/pages/courses/index.tsx, the CourseCard component fetches rounds data via trpc.courseRounds.getRoundsForCourse.useQuery even though the same data is fetched in useSortedCourses. This is intentional because tRPC (which uses React Query) automatically deduplicates network requests with identical query keys, so only one network request is made and the result is shared across components via the query cache.
Applied to files:
apps/website/src/components/homepage/UpcomingRounds.tsx
📚 Learning: 2025-11-03T01:57:21.418Z
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1554
File: apps/website/src/components/homepage/OurCommunitySection.tsx:11-17
Timestamp: 2025-11-03T01:57:21.418Z
Learning: In apps/website/src/components/homepage/OurCommunitySection.tsx, the CommunityMember type is intentionally duplicated (also exists in apps/website/src/components/lander/components/CommunityMembersSubSection.tsx) to keep the component changeable for potential future refactoring.
Applied to files:
apps/website/src/components/homepage/UpcomingRounds.tsx
📚 Learning: 2025-12-05T22:16:07.019Z
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1758
File: apps/website/src/pages/courses/index.tsx:53-53
Timestamp: 2025-12-05T22:16:07.019Z
Learning: In apps/website/src/pages/courses/index.tsx, the `isSelfPacedCourse` function intentionally includes a hardcoded check for `course.slug === 'future-of-ai'` alongside the `course.cadence?.toLowerCase() === 'self-paced'` check. This hardcoded slug check should be retained.
Applied to files:
apps/website/src/components/homepage/UpcomingRounds.tsx
📚 Learning: 2025-11-18T09:31:50.904Z
Learnt from: Will-Howard
Repo: bluedotimpact/bluedot PR: 1642
File: apps/website/src/components/courses/GroupDiscussionBanner.tsx:149-189
Timestamp: 2025-11-18T09:31:50.904Z
Learning: In GroupDiscussionBanner (apps/website/src/components/courses/GroupDiscussionBanner.tsx), Will-Howard prefers to keep buttons like "Open discussion doc" and "Message group" visible even when their underlying links (discussionDocLink, slackChannelLink) are empty. This design choice helps catch configuration errors early because users will report non-functional buttons, whereas hidden buttons might mask missing data without anyone noticing.
Applied to files:
apps/website/src/components/homepage/UpcomingRounds.tsx
📚 Learning: 2025-10-07T11:28:47.742Z
Learnt from: Will-Howard
Repo: bluedotimpact/bluedot PR: 1428
File: apps/website/src/components/courses/GroupSwitchModal.test.tsx:229-240
Timestamp: 2025-10-07T11:28:47.742Z
Learning: In GroupSwitchModal component (apps/website/src/components/courses/GroupSwitchModal.tsx), when switching groups for one unit (temporary switch), the `oldGroupId` is intentionally set to `undefined` in the request payload. The backend derives the `oldGroupId` from the `oldDiscussionId` instead. This design avoids potential mismatches between the group ID and discussion ID.
Applied to files:
apps/website/src/components/homepage/UpcomingRounds.tsx
🧬 Code graph analysis (1)
apps/website/src/components/homepage/UpcomingRounds.tsx (5)
apps/website/src/server/routers/_app.ts (1)
AppRouter(36-36)apps/website/src/components/lander/course-content/AgiStrategyContent.tsx (1)
AGI_STRATEGY_APPLICATION_URL(15-15)apps/website/src/components/lander/course-content/BioSecurityContent.tsx (1)
BIOSECURITY_APPLICATION_URL(15-15)apps/website/src/components/lander/course-content/TechnicalAiSafetyContent.tsx (1)
TECHNICAL_AI_SAFETY_APPLICATION_URL(16-16)apps/website/src/components/lander/course-content/AiGovernanceContent.tsx (1)
AI_GOVERNANCE_APPLICATION_URL(12-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: ci
🔇 Additional comments (5)
apps/website/src/components/homepage/UpcomingRounds.tsx (5)
1-12: Type-safe router output inference is well implemented.Using
inferRouterOutputs<AppRouter>to derive types from the tRPC router ensures type safety across the client-server boundary. The type definitions are clean and precise.
86-112: Clean component structure and correct responsive behavior.The
RoundGroupcomponent correctly implements the mobile-centered header requirement from the PR objectives and properly renders dividers only between items. The responsive breakpoint handling is consistent with the rest of the component.
137-177: Excellent accessibility and responsive design implementation.The component provides appropriate
aria-labelattributes that describe the action and new-tab behavior. The different interaction patterns for mobile (clickable link only) versus desktop (clickable card) offer good UX tailored to each context.
123-132: Interpunct formatting correctly implements the design spec.The use of the interpunct separator (
·) between course title and date range matches the homepage-specific formatting requirement from the PR objectives.
182-208: Diverse round selection logic correctly implements the requirement.The two-pass algorithm appropriately prioritizes course diversity (first loop) while ensuring up to 3 rounds are displayed (second loop as fallback). The deduplication check on line 201 using
round.idprevents duplicates from being added in the fallback phase.
| const { data: allRounds, isLoading } = trpc.courseRounds.getAllUpcomingRounds.useQuery(); | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <section className="bg-white pt-[16px] pb-[48px] px-5 min-[680px]:pt-0 min-[680px]:pb-[64px] min-[680px]:px-8 min-[1024px]:pb-[80px] lg:px-12 min-[1280px]:pb-[96px] min-[1440px]:pt-[24px] min-[1920px]:pt-0 xl:px-16 2xl:px-20"> | ||
| <div className="flex flex-col gap-4 max-w-screen-xl mx-auto"> | ||
| <div className="h-20 w-full animate-pulse rounded bg-gray-200" /> | ||
| <div className="h-20 w-full animate-pulse rounded bg-gray-200" /> | ||
| </div> | ||
| </section> | ||
| ); | ||
| } | ||
|
|
||
| if (!allRounds) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Consider adding error state handling for better UX.
The component only handles isLoading and missing data states. If the tRPC query fails (network error, server error, etc.), the component silently returns null, leaving users without feedback that something went wrong.
For a non-critical homepage section, this might be acceptable, but consider showing a lightweight error state or fallback message to improve user experience when the data fetch fails.
Example approach:
-const { data: allRounds, isLoading } = trpc.courseRounds.getAllUpcomingRounds.useQuery();
+const { data: allRounds, isLoading, isError } = trpc.courseRounds.getAllUpcomingRounds.useQuery();
if (isLoading) {
return (
<section className="...">
{/* skeleton */}
</section>
);
}
-if (!allRounds) {
+if (isError || !allRounds) {
return null;
}Or optionally display a minimal error message for better transparency.
🤖 Prompt for AI Agents
In apps/website/src/components/homepage/UpcomingRounds.tsx around lines 22 to
37, the component only handles loading and null data but does not handle query
errors; update the component to check
trpc.courseRounds.getAllUpcomingRounds.useQuery's isError (and optionally error)
state and render a lightweight error/fallback UI instead of returning null —
show a brief message like "Unable to load upcoming rounds" (optionally include
error.message), and provide a retry action by calling refetch from the hook so
users can recover from transient failures.
There was a problem hiding this comment.
A previous issue was made that follows under this area #1772
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Fetch details of issue #1772 to understand what it covers
gh issue view 1772 --repo bluedotimpact/bluedot --json title,body,url,state,labels
Length of output: 2697
@alyssia-x Thanks for pointing that out! I can see that issue #1772 covers error and empty state handling for data fetching components including course rounds on the homepage. Since this is already being tracked as part of a broader design system improvement, my concern here is adequately covered.
Looking forward to seeing the standardized error/empty state designs once they're implemented! 👍
🧠 Learnings used
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1758
File: apps/website/src/pages/courses/index.tsx:416-418
Timestamp: 2025-12-05T22:16:53.272Z
Learning: In apps/website/src/pages/courses/index.tsx, the CourseCard component fetches rounds data via trpc.courseRounds.getRoundsForCourse.useQuery even though the same data is fetched in useSortedCourses. This is intentional because tRPC (which uses React Query) automatically deduplicates network requests with identical query keys, so only one network request is made and the result is shared across components via the query cache.
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1577
File: apps/website/src/components/homepage/EventsSection.tsx:296-297
Timestamp: 2025-11-05T00:03:33.638Z
Learning: In the bluedotimpact/bluedot repository, when handling loading states for data fetching in components (such as the EventsSection component in apps/website/src/components/homepage/EventsSection.tsx), prefer using the ProgressDots component from the library to display a loading indicator rather than hiding the entire section or returning null during loading states. This provides a better user experience by keeping the layout consistent while data is being fetched.
Learnt from: alyssia-x
Repo: bluedotimpact/bluedot PR: 1554
File: apps/website/src/components/homepage/OurCommunitySection.tsx:11-17
Timestamp: 2025-11-03T01:57:21.418Z
Learning: In apps/website/src/components/homepage/OurCommunitySection.tsx, the CommunityMember type is intentionally duplicated (also exists in apps/website/src/components/lander/components/CommunityMembersSubSection.tsx) to keep the component changeable for potential future refactoring.
Description
New Upcoming Rounds
UpcomingRounds.tsxsection on the homepage, displaying upcoming intensive and part-time course rounds fetched from the database. When enough rounds are available, the component will display:Otherwise, round display follows the same sorting logic mentioned in #1758's PR description.
Some notes on design:
/coursespage.Other Notes/TODOs
I didn't want to focus on abstracting for this PR because the code for courses is going to change soon in a follow-up PR (e.g #1775). Since this is a P1, just want to get this out and will de-duplicate later :)
Issue
Closes #1730
Developer checklist
Screenshot
Desktop / Tablet
Mobile (320)