Skip to content

Conversation

@zacjones93
Copy link
Contributor

@zacjones93 zacjones93 commented Oct 8, 2025

This uses course builder events to manage what is displayed on the workshop page which will allow us to use MDX and be more flexible with updating copy

Next step before going live and redirecting to this route is to set up stripe via course builder

Summary by CodeRabbit

  • New Features

    • Dynamic workshop pages powered by Course Builder events (with ISR, SEO metadata).
    • MDX-driven workshop content with custom components.
    • Featured workshop redirect and a Workshops layout.
    • “Workshop Not Found” page.
    • Event-based Active Sale with coupon-aware pricing and PPP support.
    • Timezone-aware date/time display.
  • Improvements

    • More flexible components (TestimonialBar, InstructorTerminal, Markdown).
    • Enhanced MDX serialization options.
  • Documentation

    • Workshop Event Database Migration proposal.
    • Stripe checkout integration plan and rollout guidance.
  • Chores

    • Updated dependencies; added date-fns-tz.

zacjones93 and others added 6 commits October 8, 2025 11:59
- Create get-event.ts for fetching events from Course Builder DB
- Create get-event-coupons.ts for SQL-based coupon queries
- Add Event schema with Zod validation
- Export getConnectionPool in get-course-builder-metadata.ts

These utilities fetch workshop events and related coupons directly from
the Course Builder database, eliminating the need for data transformation
layers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Upgrade next-mdx-remote from 4.4.1 to 5.0.0
- Add format: 'mdx' to serialize options for proper JSX parsing
- Update serialize-mdx.ts for compatibility

The newer version of next-mdx-remote resolves "_jsx is not a function"
errors when using React 19.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add /workshops/[slug] dynamic route for Course Builder events
- Create workshop-page-client.tsx with MDX rendering via next-mdx-remote
- Move shared workshop components to _components/ directory
- Create mdx-components.tsx registry for available MDX components
- Add not-prose classes to workshop components (Hero, Testimonial, etc.)
- Configure responsive typography with proper dark mode colors

Components now accept Event types from Course Builder instead of
transformed LiveWorkshop objects. MDX body content is rendered with
custom components available for use in Course Builder.

Typography matches production cursor workshop:
- h2: text-xl → sm:text-2xl → lg:text-3xl
- h3: text-lg → lg:text-xl
- Dark mode text colors for headings and paragraphs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Update cursor and claude-code workshop components to use Event types
- Migrate shared components from /workshop/shared to /app/workshops/[slug]/_components
- Update ActiveSale components to accept Event + EventCoupon[] instead of LiveWorkshop
- Update cursor/index.tsx to import from new _components directory
- Remove old shared component files (moved to app directory)

Components now fetch data directly from Course Builder database instead
of using transformation layer. This simplifies the data flow and makes
the components more maintainable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add workshop-event-database-migration-proposal.md documenting the
  migration from hardcoded workshop data to Course Builder events
- Add workshop-stripe-integration-plan.md for future Stripe checkout
  integration with Course Builder products

These documents provide context for the workshop infrastructure changes
and future enhancement plans.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add TypeScript types for MarkdownProps
- Support custom components via components prop
- Support className for styling
- Merge custom components with default components

This allows consumers to override default component rendering and
apply custom styling to markdown content.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@vercel
Copy link

vercel bot commented Oct 8, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
egghead-io-nextjs Error Error Oct 8, 2025 8:32pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 8, 2025

Walkthrough

Adds documentation for database migration and Stripe integration plans. Implements an event-driven workshops App Router: dynamic slug page with ISR, client renderer with MDX components, new event schemas, DB accessors (events, coupons), and a refactor of Hero/ActiveSale to use Event + coupons. Multiple import path consolidations, Markdown/MDX tweaks, and supportive utilities.

Changes

Cohort / File(s) Summary
Docs: Migration and Stripe plans
docs/workshop-event-database-migration-proposal.md, docs/workshop-stripe-integration-plan.md
Adds detailed proposals for migrating workshops to Course Builder DB and a Stripe checkout integration plan with proxy/webhook guidance.
Dependencies
package.json
Adds date-fns-tz; bumps date-fns to ^2.30.0 and next-mdx-remote to 5.0.0.
Event schemas and data layer
src/schemas/event.ts, src/lib/events/get-event.ts, src/lib/events/get-event-coupons.ts, src/lib/events/transform-event-to-workshop.ts, src/lib/get-course-builder-metadata.ts
Introduces Zod schemas for events; adds DB fetchers for events and coupons; transformer to workshop shape; exports connection pool helper.
App Router: Workshops pages
src/app/workshops/layout.tsx, src/app/workshops/page.tsx, src/app/workshops/[slug]/page.tsx, src/app/workshops/[slug]/workshop-page-client.tsx, src/app/workshops/[slug]/not-found.tsx
Adds layout, index redirect to featured workshop, dynamic slug page with data loading, MDX serialization, ISR (revalidate 3600), and client renderer; not-found page.
App Router: Workshop components
src/app/workshops/[slug]/_components/Hero.tsx, .../TestimonialBar.tsx, .../feature-list.tsx, .../instructor-terminal.tsx, .../mdx-components.tsx, .../testimonial.tsx, .../time-and-location.tsx, .../workshop-history.tsx
Refactors Hero to event-based props and timezones; props-ify TestimonialBar and InstructorTerminal; adds FeatureList and MDX component map; minor styling and import adjustments.
ActiveSale refactor (event-based)
src/components/workshop/claude-code/active-sale.tsx
Switches from workshop to event+coupons model; rewires coupon selection, pricing, PPP, and payment link composition; updates props across subcomponents.
Markdown/MDX utilities
src/components/markdown.tsx, src/components/markdown/serialize-mdx.ts
Types Markdown component with default list renderers; MDX serializer sets format to mdx and disables frontmatter in non-Shiki path.
Import path consolidation (workshop UI)
src/components/workshop/claude-code/SignUpForm.tsx, src/components/workshop/cursor/* (Hero.tsx, Features.tsx, Conclusion.tsx, SignUpForm.tsx, WorkshopStructure.tsx, active-sale.tsx, team/hero.tsx), src/pages/workshop/* (claude-code/index.tsx, cursor/index.tsx, cursor/team/index.tsx)
Updates animation and component import paths to new App Router component locations; no logic changes.
Invoices import paths
src/app/workshops/[slug]/_components/invoices/invoice-teaser.tsx, src/pages/workshop/*/invoice/[merchantChargeId].tsx
Adjusts type/component import paths for invoices to app-based locations.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Next.js App Router as App Router
  participant Page [slug] as /workshops/[slug]/page.tsx
  participant DB as Course Builder DB
  participant Coupons as get-event-coupons
  participant MDX as serialize-mdx
  participant Client as WorkshopPageClient

  User->>App Router: Request /workshops/{slug}
  App Router->>Page [slug]: render()
  Page [slug]->>DB: getEventBySlug(slug)
  alt Event not found or invalid
    Page [slug]->>App Router: notFound()
    App Router-->>User: 404 Workshop Not Found
  else Event found
    Page [slug]->>Coupons: getEventCoupons(event.id)
    Coupons->>DB: query coupons for event products
    DB-->>Coupons: coupons[]
    Coupons-->>Page [slug]: EventCoupon[]
    opt MDX body present
      Page [slug]->>MDX: serialize(event.fields.body, scope: {event, coupons})
      MDX-->>Page [slug]: serializedBody
    end
    Page [slug]->>Client: render with {event, coupons, serializedBody}
    Client-->>User: HTML (Hero/MDX/ActiveSale)
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant Index as /workshops/page.tsx
  participant Events as getPrimaryWorkshop

  User->>Index: Request /workshops
  Index->>Events: fetch featured/upcoming event
  alt No event
    Index-->>User: "No Workshops Available"
  else Found event
    Index-->>User: Redirect to /workshops/{slug}
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–75 minutes

Possibly related PRs

Suggested labels

NO MERGE

Suggested reviewers

  • johnlindquist
  • Creeland
  • nicollguarnizo

Poem

A rabbit taps keys in the twilight glow,
Routes bloom from a single slug’s flow.
Events now sing, coupons align,
MDX whispers in serialized rhyme.
I thump with joy—ISR in flight!
Hop, hop—deploy, then chew a byte. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly describes the main change of pulling workshop data from Course Builder events, reflecting the core intent of the PR, but the “WIP step 1” suffix adds unnecessary noise without adding value to the summary.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch zac/cb-workshop-events

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (2)
src/lib/get-course-builder-metadata.ts (1)

33-43: Refactor src/lib/db.ts to use the centralized getConnectionPool()
Direct calls to createPool in src/lib/db.ts instantiate a separate pool; import and call the newly exported getConnectionPool() instead to ensure a single shared pool.

src/components/workshop/claude-code/active-sale.tsx (1)

567-567: Typo: “pratcical” → “practical”

Minor copy fix.

-        Learn repeatable, pratcical strategies for shipping code with AI in a
+        Learn repeatable, practical strategies for shipping code with AI in a
🧹 Nitpick comments (31)
src/components/workshop/cursor/SignUpForm.tsx (2)

5-5: Consider moving shared animation code to a more appropriate location.

Importing fadeInUp from app/workshops/[slug]/_components/animations creates coupling between this reusable component (located in src/components/) and a route-specific implementation. Shared animation utilities should typically live in a common location like src/lib/animations or src/utils/animations rather than inside a dynamic route folder.

This coupling means any route structure changes will break this import, and the animation code isn't easily discoverable for reuse in other components.

Consider moving the animations file to a shared location:

// Create src/lib/animations.ts or src/utils/animations.ts
export const fadeInUp = {
  initial: { opacity: 0, y: 20 },
  animate: { opacity: 1, y: 0 },
  transition: { duration: 0.5 }
}

Then update the import:

-import {fadeInUp} from '../../../app/workshops/[slug]/_components/animations'
+import {fadeInUp} from '@/lib/animations'

5-5: Use the @/ path alias to simplify imports.

The relative path ../../../app/workshops/[slug]/_components/animations is difficult to maintain. As per coding guidelines, prefer the @/ path alias for imports from the src directory.

Apply this change to use the path alias:

-import {fadeInUp} from '../../../app/workshops/[slug]/_components/animations'
+import {fadeInUp} from '@/app/workshops/[slug]/_components/animations'

As per coding guidelines.

src/components/workshop/cursor/WorkshopStructure.tsx (1)

3-7: Import path and exports validated; consider using a shared alias for animations
fadeInUp, staggerContainer, and staggerItem are correctly exported from src/app/workshops/[slug]/_components/animations.ts. To avoid brittle deep relative imports across src/ and app/, centralize these into a shared module and import via a path alias (e.g. @/animations).

docs/workshop-stripe-integration-plan.md (1)

1-1192: Comprehensive Stripe integration plan with minor formatting suggestions.

This documentation provides an excellent, detailed plan for integrating Course Builder's Stripe checkout with workshop pages. The phased approach with clear rollback strategies demonstrates thoughtful planning.

Optional: Address markdown linting issues for better consistency

The static analysis tool flagged a few formatting issues:

  1. Missing language identifiers in fenced code blocks (line 182): Add language specifiers to all code blocks for proper syntax highlighting
  2. Emphasis used instead of headings (lines 569, 576, 607, 617): Consider converting emphasized text to proper headings where appropriate

Example fix for code block:

-```
+```mermaid
 ┌─────────────┐
 │ Egghead User│

These are purely cosmetic improvements and don't affect the plan's quality.

docs/workshop-event-database-migration-proposal.md (1)

1-1787: Excellent migration proposal with clear phasing and rollback plans.

This document provides a thorough analysis of migrating from Vercel KV to Course Builder database for workshop events. The dynamic App Router architecture recommendation (Section 4) is particularly valuable, proposing a future-proof solution that eliminates hard-coded workshop routes.

Key strengths:

  • Detailed comparison of migration strategies
  • Phased implementation with clear success criteria
  • Comprehensive rollback plans at each phase
  • Reference implementations and code examples

Optional: Fix markdown formatting for consistency

Static analysis flagged several minor formatting issues:

  1. Invalid link fragments (lines 23-29): Table of contents links may not resolve correctly
  2. Missing language specifications (lines 225, 343, 361, 503, 1297): Add language identifiers to fenced code blocks

Example fix for link fragments:

-4. [**NEW: Dynamic Workshop Architecture**](#4-dynamic-workshop-architecture-app-router)
+4. [**NEW: Dynamic Workshop Architecture**](#4-dynamic-workshop-architecture-app-router)

These are cosmetic improvements that don't impact the technical content.

src/app/workshops/[slug]/_components/time-and-location.tsx (1)

3-6: Import path updated correctly, but consider extracting shared utilities.

The import path correctly maintains access to the time formatting utilities after the component reorganization.

For improved maintainability, consider extracting parseDateTimeWithOffset and formatTimeInTimeZone to a shared utility location like src/lib/utils/time.ts, rather than importing from a component file. This would:

  • Make these utilities more discoverable
  • Avoid coupling between components
  • Follow the single responsibility principle

Example structure:

// src/lib/utils/time.ts
export function parseDateTimeWithOffset(date: string, time: string, offset: number) {
  // implementation
}

export function formatTimeInTimeZone(date: Date, timezone: string) {
  // implementation
}
src/components/workshop/claude-code/SignUpForm.tsx (1)

5-5: Consider using the @/ path alias for consistency.

The import path '../../../app/workshops/[slug]/_components/animations' could be simplified using the @/ alias (e.g., '@/app/workshops/[slug]/_components/animations') for better maintainability and to align with the project's coding guidelines that prefer @/ for imports from the src directory.

Apply this diff:

-import {fadeInUp} from '../../../app/workshops/[slug]/_components/animations'
+import {fadeInUp} from '@/app/workshops/[slug]/_components/animations'
src/components/workshop/cursor/Conclusion.tsx (1)

3-3: Consider using the @/ path alias for consistency.

Similar to other files in this PR, the import path could be simplified using the @/ alias for better maintainability.

Apply this diff:

-import {fadeInUp} from '../../../app/workshops/[slug]/_components/animations'
+import {fadeInUp} from '@/app/workshops/[slug]/_components/animations'
src/components/workshop/cursor/active-sale.tsx (1)

3-6: Standardize import path style for consistency.

The import paths show inconsistency: Line 3 uses a relative path while Line 6 uses the @/ alias. For consistency and maintainability, prefer the @/ alias for both imports.

Apply this diff:

-import TimeAndLocation from '../../../app/workshops/[slug]/_components/time-and-location'
+import TimeAndLocation from '@/app/workshops/[slug]/_components/time-and-location'
 import {useCommerceMachine} from '@/hooks/use-commerce-machine'
 import {get, isEmpty} from 'lodash'
 import WorkshopParityCouponMessage from '@/app/workshops/[slug]/_components/parity-coupon-message'
src/components/workshop/cursor/team/hero.tsx (1)

4-7: Consider using the @/ path alias for consistency.

The import path could be simplified using the @/ alias for better maintainability and consistency with other imports in the file.

Apply this diff:

 import {
   fadeInUp,
   scaleIn,
-} from '../../../../app/workshops/[slug]/_components/animations'
+} from '@/app/workshops/[slug]/_components/animations'
src/components/workshop/cursor/Hero.tsx (3)

4-4: Remove commented code or document why it's kept.

The commented import // import '../shared/styles.css' should either be removed if no longer needed or uncommented if still required. Commented code can lead to confusion about intentional removal vs. temporary commenting.

If the styles are no longer needed, remove the line:

-// import '../shared/styles.css'

7-7: Consider using the @/ path alias for consistency.

For consistency with the project's import conventions, use the @/ alias instead of relative paths.

Apply this diff:

-import TimeAndLocation from '../../../app/workshops/[slug]/_components/time-and-location'
+import TimeAndLocation from '@/app/workshops/[slug]/_components/time-and-location'

20-106: Consider leveraging date-fns-tz for timezone conversions.

The manual date/time parsing and timezone offset handling (lines 20-106) is complex and error-prone. Since the PR adds date-fns-tz as a dependency, consider using its utilities like formatInTimeZone and zonedTimeToUtc for more robust and maintainable timezone handling.

Example refactor using date-fns-tz:

import { formatInTimeZone } from 'date-fns-tz'

// Instead of parseDateTimeWithOffset and formatTimeInTimeZone:
const workshopDateStr = `${workshop.date} ${workshop.startTime}`
const londonTime = formatInTimeZone(
  new Date(workshopDateStr), 
  'Europe/London', 
  'h:mm a'
)
const parisBerlinTime = formatInTimeZone(
  new Date(workshopDateStr), 
  'Europe/Paris', 
  'h:mm a'
)
src/app/workshops/layout.tsx (1)

7-7: Prefer @/ alias for src imports.

Our convention is to use the @/ alias for anything under src/. Please switch this relative import to the alias for consistency.

-import {Providers} from '../providers'
+import {Providers} from '@/app/providers'
src/app/workshops/[slug]/page.tsx (3)

62-68: Consider filtering workshops in generateStaticParams.

The commented-out filters in getAllWorkshops (state='published', visibility='public') suggest you may want to filter which workshops are statically generated. Currently, all events are included regardless of their state or visibility.

If you intend to only generate static pages for published/public workshops, apply this pattern:

 export async function generateStaticParams() {
   const workshops = await getAllWorkshops()
 
-  return workshops.map((workshop) => ({
+  return workshops
+    .filter(w => w.fields.state === 'published' && w.fields.visibility === 'public')
+    .map((workshop) => ({
     slug: workshop.fields.slug,
   }))
 }

74-74: Document differing revalidate values
The list page (src/app/workshops/page.tsx) uses export const revalidate = 0 (on-demand) while detail pages ([slug]/page.tsx) use export const revalidate = 3600; add a comment explaining why they differ.


20-28: Remove redundant type check
getEventBySlug already filters by type = 'event' and EventSchema enforces it, so event.type !== 'event' can never be true. Simplify to:

if (!event) {
  notFound()
}
src/app/workshops/[slug]/workshop-page-client.tsx (2)

75-86: Remove or uncomment the Hero section.

This large commented block (lines 75-86) shows an alternative Hero rendering approach. If this is temporary during development, consider adding a comment explaining why it's commented out. Otherwise, remove it to reduce clutter.


105-105: TODO: Implement user context for discount eligibility.

Line 105 has a hardcoded hasYearlyProDiscount={false} with a TODO comment. Ensure this is tracked for implementation before the feature goes live.

Do you want me to open a new issue to track fetching user context for yearly pro discount eligibility?

src/lib/events/get-event.ts (1)

147-163: SQL injection risk with dynamic slug condition.

Lines 147-149 construct dynamic SQL by string concatenation (${slugCondition}), though the actual parameter is safely passed via the params array. However, this pattern could be risky if modified. Consider using a safer approach.

Refactor to avoid string concatenation in SQL:

-    const slugCondition = slugPattern
-      ? `AND JSON_UNQUOTE(JSON_EXTRACT(fields, '$.slug')) LIKE ?`
-      : ''
-
-    const params = slugPattern ? [slugPattern] : []
-
     const [featuredRows] = await conn.execute<RowDataPacket[]>(
       `SELECT * FROM egghead_ContentResource
        WHERE type = 'event'
-       -- AND JSON_UNQUOTE(JSON_EXTRACT(fields, '$.state')) = 'published'
-       -- AND JSON_UNQUOTE(JSON_EXTRACT(fields, '$.visibility')) = 'public'
        AND JSON_UNQUOTE(JSON_EXTRACT(fields, '$.featured')) = 'true'
-       ${slugCondition}
+       ${slugPattern ? `AND JSON_UNQUOTE(JSON_EXTRACT(fields, '$.slug')) LIKE ?` : ''}
        ORDER BY JSON_EXTRACT(fields, '$.startsAt') DESC
        LIMIT 1`,
-      params,
+      slugPattern ? [slugPattern] : [],
     )

Or better yet, always include the condition and use '%' as a wildcard when no pattern is specified.

src/lib/events/get-event-coupons.ts (3)

74-90: Harden fields parsing and filter expired/exhausted coupons before returning

  • JSON.parse on fields can throw and currently aborts the whole fetch.
  • Returned list includes expired or fully used coupons.

Refactor to safely parse per row and filter after mapping.

Apply this diff:

-    const coupons: EventCoupon[] = couponRows.map((row) => {
-      // Parse fields if they are JSON strings
-      const fields =
-        typeof row.fields === 'string' ? JSON.parse(row.fields) : row.fields
-
-      return {
+    let coupons: EventCoupon[] = couponRows.map((row) => {
+      // Parse fields if they are JSON strings (tolerant)
+      let parsedFields: Record<string, any> = {}
+      try {
+        parsedFields =
+          typeof row.fields === 'string' ? JSON.parse(row.fields) : row.fields
+      } catch (e) {
+        console.warn('Invalid coupon.fields JSON', {couponId: row.id})
+      }
+
+      return {
         id: row.id,
         code: row.code,
         percentageDiscount: Number(row.percentageDiscount),
         expires: row.expires ? new Date(row.expires) : null,
         maxUses: row.maxUses,
         usedCount: row.usedCount,
         restrictedToProductId: row.restrictedToProductId,
-        fields: fields || {},
-        couponType: determineCouponType(fields),
+        fields: parsedFields || {},
+        couponType: determineCouponType(parsedFields),
       }
     })
-
-    console.log(`Found ${coupons.length} coupons for event: ${eventId}`)
+    // Filter out expired or exhausted coupons
+    const now = Date.now()
+    coupons = coupons.filter(
+      (c) =>
+        (!c.expires || c.expires.getTime() > now) &&
+        (c.maxUses === 0 || c.usedCount < c.maxUses),
+    )
+
+    console.log(`Found ${coupons.length} coupons for event: ${eventId}`)
     return coupons

Also applies to: 92-94


37-45: Add typed RowDataPacket interfaces for stronger TS safety

Explicit row typings avoid magic property access on RowDataPacket.

Example:

interface ProductRow extends RowDataPacket { productId: string }
interface CouponRow extends RowDataPacket {
  id: string
  code: string | null
  percentageDiscount: number | string
  expires: string | Date | null
  maxUses: number
  usedCount: number
  restrictedToProductId: string | null
  fields: string | Record<string, any> | null
}
const [productRows] = await conn.execute<ProductRow[]>(`...`, [eventId])
const [couponRows] = await conn.execute<CouponRow[]>(`...`, productIds)

Also applies to: 51-56


23-28: Prefer structured logger over console, or gate logs in production

Console noise in server logs can get chatty. Consider a shared logger with levels and env gating.

As per coding guidelines

Also applies to: 92-96

src/app/workshops/[slug]/_components/Hero.tsx (2)

26-116: Remove unused date helpers to reduce bundle size/noise

parseDateTimeWithOffset and formatTimeInTimeZone aren’t used.

Delete these helpers or move to a shared util when needed.


189-195: Avoid showing "Error" strings in UI fallback

If you later enable the EU-friendly block, prefer empty string or omit the line when date is missing.

src/components/workshop/claude-code/active-sale.tsx (3)

96-103: Remove noisy console logs or gate by NODE_ENV

These logs will spam the console on every state change.

-  console.log(
-    'Effect running: Updating couponToApply based on new baseCoupon',
-    baseCoupon,
-  )
+  // dev-only debug:
+  // if (process.env.NODE_ENV !== 'production') console.debug('Updating coupon', baseCoupon)

359-366: Normalize currency display

Raw numbers can show long fractions. Format to 2 decimals.

Apply this diff:

-            ${nonMemberPrice}
+            ${nonMemberPrice.toFixed(2)}
@@
-                ${basePrice}
+                ${basePrice.toFixed(2)}
@@
-              <div className="text-xs font-black">${memberDiscount}</div>
+              <div className="text-xs font-black">${memberDiscount.toFixed(2)}</div>
@@
-              ${memberPrice}
+              ${memberPrice.toFixed(2)}
@@
-                ${nonMemberPrice}
+                ${nonMemberPrice.toFixed(2)}

Alternatively, use Intl.NumberFormat for full locale-aware currency.

Also applies to: 393-394, 415-423


435-441: Prefer Next.js Link for internal navigation

Avoid full page reload for /pricing.

Replace with .

Also applies to: 443-449

src/schemas/event.ts (3)

66-69: Validate datetimes with Zod’s datetime and constrain timezone

Currently plain strings; add datatype checks.

-  startsAt: z.string(), // ISO datetime: "2025-02-20T17:00:00.000Z"
-  endsAt: z.string().optional(),
-  timezone: z.string(), // "America/Los_Angeles"
+  startsAt: z.string().datetime(), // ISO datetime: "2025-02-20T17:00:00.000Z"
+  endsAt: z.string().datetime().optional(),
+  timezone: z
+    .string()
+    .refine(
+      (tz) => Intl.supportedValuesOf?.('timeZone')?.includes?.(tz) ?? true,
+      'Invalid IANA timezone',
+    ),

Note: supportedValuesOf is Node 20+. If not available, keep string or add a custom allowlist.


75-76: Use url() for URL fields and numeric type for price

Tighten types for early failure.

-  image: z.string().optional(),
+  image: z.string().url().optional(),
@@
-  heroImage: z.string().optional(),
-  heroBackgroundImage: z.string().optional(),
+  heroImage: z.string().url().optional(),
+  heroBackgroundImage: z.string().url().optional(),
@@
-  workshopPrice: z.string().optional(),
-  stripeProductId: z.string().optional(),
-  stripePaymentLink: z.string().optional(),
+  workshopPrice: z.coerce.number().positive().optional(),
+  stripeProductId: z.string().optional(),
+  stripePaymentLink: z.string().url().optional(),
@@
-  ogImage: z.string(),
+  ogImage: z.string().url(),
@@
-  url: z.string(),
+  url: z.string().url(),

Also applies to: 86-87, 121-123, 41-44, 49-51


20-24: Consider numeric discounts for clarity

If WorkshopDiscounts represent percentages, prefer numbers with a 0–1 or 0–100 constraint.

-export const WorkshopDiscountsSchema = z.object({
-  earlyBirdMember: z.string().optional(),
-  member: z.string().optional(),
-  earlyBird: z.string().optional(),
-})
+export const WorkshopDiscountsSchema = z.object({
+  earlyBirdMember: z.number().min(0).optional(),
+  member: z.number().min(0).optional(),
+  earlyBird: z.number().min(0).optional(),
+})

Coordinate with consumers before changing this.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae583de and c792937.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (38)
  • docs/workshop-event-database-migration-proposal.md (1 hunks)
  • docs/workshop-stripe-integration-plan.md (1 hunks)
  • package.json (2 hunks)
  • src/app/workshops/[slug]/_components/Hero.tsx (3 hunks)
  • src/app/workshops/[slug]/_components/TestimonialBar.tsx (2 hunks)
  • src/app/workshops/[slug]/_components/feature-list.tsx (1 hunks)
  • src/app/workshops/[slug]/_components/instructor-terminal.tsx (3 hunks)
  • src/app/workshops/[slug]/_components/invoices/invoice-teaser.tsx (1 hunks)
  • src/app/workshops/[slug]/_components/mdx-components.tsx (1 hunks)
  • src/app/workshops/[slug]/_components/testimonial.tsx (1 hunks)
  • src/app/workshops/[slug]/_components/time-and-location.tsx (1 hunks)
  • src/app/workshops/[slug]/_components/workshop-history.tsx (1 hunks)
  • src/app/workshops/[slug]/not-found.tsx (1 hunks)
  • src/app/workshops/[slug]/page.tsx (1 hunks)
  • src/app/workshops/[slug]/workshop-page-client.tsx (1 hunks)
  • src/app/workshops/layout.tsx (1 hunks)
  • src/app/workshops/page.tsx (1 hunks)
  • src/components/markdown.tsx (1 hunks)
  • src/components/markdown/serialize-mdx.ts (2 hunks)
  • src/components/workshop/claude-code/SignUpForm.tsx (1 hunks)
  • src/components/workshop/claude-code/active-sale.tsx (12 hunks)
  • src/components/workshop/cursor/Conclusion.tsx (1 hunks)
  • src/components/workshop/cursor/Features.tsx (1 hunks)
  • src/components/workshop/cursor/Hero.tsx (1 hunks)
  • src/components/workshop/cursor/SignUpForm.tsx (1 hunks)
  • src/components/workshop/cursor/WorkshopStructure.tsx (1 hunks)
  • src/components/workshop/cursor/active-sale.tsx (1 hunks)
  • src/components/workshop/cursor/team/hero.tsx (1 hunks)
  • src/lib/events/get-event-coupons.ts (1 hunks)
  • src/lib/events/get-event.ts (1 hunks)
  • src/lib/events/transform-event-to-workshop.ts (1 hunks)
  • src/lib/get-course-builder-metadata.ts (1 hunks)
  • src/pages/workshop/claude-code/index.tsx (1 hunks)
  • src/pages/workshop/claude-code/invoice/[merchantChargeId].tsx (1 hunks)
  • src/pages/workshop/cursor/index.tsx (2 hunks)
  • src/pages/workshop/cursor/invoice/[merchantChargeId].tsx (1 hunks)
  • src/pages/workshop/cursor/team/index.tsx (1 hunks)
  • src/schemas/event.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (17)
**/*.{js,mjs,cjs,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/logging-session.mdc)

**/*.{js,mjs,cjs,ts,tsx}: On application startup, generate a timestamp YYYY-MM-DD-HH-MM-SS and create logs/.log; append all log output to this file
Each application run must create a new log file, preserving previous logs
Use a logger (e.g., pino or winston) or file writes to implement logging
Record start/end of major operations, successful paths, handled errors, and unexpected failures
Include environment info (e.g., CLI arguments and Node version) in logs
Provide a toggle to disable file logging: LOG_LEVEL=off or --no-log; when disabled, do not create the log file and suppress log output
Prefer structured log lines (JSON or clearly delimited) with level, timestamp, and message in each entry
Logs must capture both happy paths and errors while maintaining readability and sufficient detail for troubleshooting

Files:

  • src/components/workshop/cursor/Conclusion.tsx
  • src/app/workshops/page.tsx
  • src/components/workshop/cursor/SignUpForm.tsx
  • src/app/workshops/[slug]/_components/testimonial.tsx
  • src/app/workshops/[slug]/_components/mdx-components.tsx
  • src/app/workshops/[slug]/_components/instructor-terminal.tsx
  • src/pages/workshop/cursor/index.tsx
  • src/lib/events/get-event-coupons.ts
  • src/components/workshop/cursor/active-sale.tsx
  • src/pages/workshop/cursor/invoice/[merchantChargeId].tsx
  • src/components/workshop/claude-code/SignUpForm.tsx
  • src/app/workshops/layout.tsx
  • src/app/workshops/[slug]/_components/feature-list.tsx
  • src/lib/events/transform-event-to-workshop.ts
  • src/components/workshop/cursor/Hero.tsx
  • src/app/workshops/[slug]/_components/workshop-history.tsx
  • src/schemas/event.ts
  • src/components/workshop/cursor/team/hero.tsx
  • src/lib/events/get-event.ts
  • src/components/workshop/cursor/WorkshopStructure.tsx
  • src/pages/workshop/claude-code/invoice/[merchantChargeId].tsx
  • src/components/markdown.tsx
  • src/app/workshops/[slug]/page.tsx
  • src/app/workshops/[slug]/_components/TestimonialBar.tsx
  • src/lib/get-course-builder-metadata.ts
  • src/app/workshops/[slug]/not-found.tsx
  • src/app/workshops/[slug]/workshop-page-client.tsx
  • src/pages/workshop/cursor/team/index.tsx
  • src/components/workshop/cursor/Features.tsx
  • src/pages/workshop/claude-code/index.tsx
  • src/app/workshops/[slug]/_components/time-and-location.tsx
  • src/components/workshop/claude-code/active-sale.tsx
  • src/app/workshops/[slug]/_components/invoices/invoice-teaser.tsx
  • src/app/workshops/[slug]/_components/Hero.tsx
  • src/components/markdown/serialize-mdx.ts
{src,app,lib,packages}/**

📄 CodeRabbit inference engine (.cursor/rules/project-update-user-rules.mdc)

Examine main source directories (e.g., src/, app/, lib/, packages/) to understand module organization and architectural patterns

Files:

  • src/components/workshop/cursor/Conclusion.tsx
  • src/app/workshops/page.tsx
  • src/components/workshop/cursor/SignUpForm.tsx
  • src/app/workshops/[slug]/_components/testimonial.tsx
  • src/app/workshops/[slug]/_components/mdx-components.tsx
  • src/app/workshops/[slug]/_components/instructor-terminal.tsx
  • src/pages/workshop/cursor/index.tsx
  • src/lib/events/get-event-coupons.ts
  • src/components/workshop/cursor/active-sale.tsx
  • src/pages/workshop/cursor/invoice/[merchantChargeId].tsx
  • src/components/workshop/claude-code/SignUpForm.tsx
  • src/app/workshops/layout.tsx
  • src/app/workshops/[slug]/_components/feature-list.tsx
  • src/lib/events/transform-event-to-workshop.ts
  • src/components/workshop/cursor/Hero.tsx
  • src/app/workshops/[slug]/_components/workshop-history.tsx
  • src/schemas/event.ts
  • src/components/workshop/cursor/team/hero.tsx
  • src/lib/events/get-event.ts
  • src/components/workshop/cursor/WorkshopStructure.tsx
  • src/pages/workshop/claude-code/invoice/[merchantChargeId].tsx
  • src/components/markdown.tsx
  • src/app/workshops/[slug]/page.tsx
  • src/app/workshops/[slug]/_components/TestimonialBar.tsx
  • src/lib/get-course-builder-metadata.ts
  • src/app/workshops/[slug]/not-found.tsx
  • src/app/workshops/[slug]/workshop-page-client.tsx
  • src/pages/workshop/cursor/team/index.tsx
  • src/components/workshop/cursor/Features.tsx
  • src/pages/workshop/claude-code/index.tsx
  • src/app/workshops/[slug]/_components/time-and-location.tsx
  • src/components/workshop/claude-code/active-sale.tsx
  • src/app/workshops/[slug]/_components/invoices/invoice-teaser.tsx
  • src/app/workshops/[slug]/_components/Hero.tsx
  • src/components/markdown/serialize-mdx.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/react-rules.mdc)

**/*.{tsx,jsx}: Do not include non-memoized functions in React useEffect dependency arrays to avoid infinite re-renders
For effects that should only run on mount/unmount, use an empty dependency array [] in useEffect
If a function must be a dependency in useEffect, memoize it with useCallback so its identity is stable

Files:

  • src/components/workshop/cursor/Conclusion.tsx
  • src/app/workshops/page.tsx
  • src/components/workshop/cursor/SignUpForm.tsx
  • src/app/workshops/[slug]/_components/testimonial.tsx
  • src/app/workshops/[slug]/_components/mdx-components.tsx
  • src/app/workshops/[slug]/_components/instructor-terminal.tsx
  • src/pages/workshop/cursor/index.tsx
  • src/components/workshop/cursor/active-sale.tsx
  • src/pages/workshop/cursor/invoice/[merchantChargeId].tsx
  • src/components/workshop/claude-code/SignUpForm.tsx
  • src/app/workshops/layout.tsx
  • src/app/workshops/[slug]/_components/feature-list.tsx
  • src/components/workshop/cursor/Hero.tsx
  • src/app/workshops/[slug]/_components/workshop-history.tsx
  • src/components/workshop/cursor/team/hero.tsx
  • src/components/workshop/cursor/WorkshopStructure.tsx
  • src/pages/workshop/claude-code/invoice/[merchantChargeId].tsx
  • src/components/markdown.tsx
  • src/app/workshops/[slug]/page.tsx
  • src/app/workshops/[slug]/_components/TestimonialBar.tsx
  • src/app/workshops/[slug]/not-found.tsx
  • src/app/workshops/[slug]/workshop-page-client.tsx
  • src/pages/workshop/cursor/team/index.tsx
  • src/components/workshop/cursor/Features.tsx
  • src/pages/workshop/claude-code/index.tsx
  • src/app/workshops/[slug]/_components/time-and-location.tsx
  • src/components/workshop/claude-code/active-sale.tsx
  • src/app/workshops/[slug]/_components/invoices/invoice-teaser.tsx
  • src/app/workshops/[slug]/_components/Hero.tsx
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use the @/ path alias for imports from the src directory

Files:

  • src/components/workshop/cursor/Conclusion.tsx
  • src/app/workshops/page.tsx
  • src/components/workshop/cursor/SignUpForm.tsx
  • src/app/workshops/[slug]/_components/testimonial.tsx
  • src/app/workshops/[slug]/_components/mdx-components.tsx
  • src/app/workshops/[slug]/_components/instructor-terminal.tsx
  • src/pages/workshop/cursor/index.tsx
  • src/lib/events/get-event-coupons.ts
  • src/components/workshop/cursor/active-sale.tsx
  • src/pages/workshop/cursor/invoice/[merchantChargeId].tsx
  • src/components/workshop/claude-code/SignUpForm.tsx
  • src/app/workshops/layout.tsx
  • src/app/workshops/[slug]/_components/feature-list.tsx
  • src/lib/events/transform-event-to-workshop.ts
  • src/components/workshop/cursor/Hero.tsx
  • src/app/workshops/[slug]/_components/workshop-history.tsx
  • src/schemas/event.ts
  • src/components/workshop/cursor/team/hero.tsx
  • src/lib/events/get-event.ts
  • src/components/workshop/cursor/WorkshopStructure.tsx
  • src/pages/workshop/claude-code/invoice/[merchantChargeId].tsx
  • src/components/markdown.tsx
  • src/app/workshops/[slug]/page.tsx
  • src/app/workshops/[slug]/_components/TestimonialBar.tsx
  • src/lib/get-course-builder-metadata.ts
  • src/app/workshops/[slug]/not-found.tsx
  • src/app/workshops/[slug]/workshop-page-client.tsx
  • src/pages/workshop/cursor/team/index.tsx
  • src/components/workshop/cursor/Features.tsx
  • src/pages/workshop/claude-code/index.tsx
  • src/app/workshops/[slug]/_components/time-and-location.tsx
  • src/components/workshop/claude-code/active-sale.tsx
  • src/app/workshops/[slug]/_components/invoices/invoice-teaser.tsx
  • src/app/workshops/[slug]/_components/Hero.tsx
  • src/components/markdown/serialize-mdx.ts
src/components/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Organize UI components in feature-based folders under src/components and keep components single-responsibility with typed props

Files:

  • src/components/workshop/cursor/Conclusion.tsx
  • src/components/workshop/cursor/SignUpForm.tsx
  • src/components/workshop/cursor/active-sale.tsx
  • src/components/workshop/claude-code/SignUpForm.tsx
  • src/components/workshop/cursor/Hero.tsx
  • src/components/workshop/cursor/team/hero.tsx
  • src/components/workshop/cursor/WorkshopStructure.tsx
  • src/components/markdown.tsx
  • src/components/workshop/cursor/Features.tsx
  • src/components/workshop/claude-code/active-sale.tsx
src/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.tsx: Prefer Tailwind utility classes for styling in components; use CSS Modules only for complex styles
Use trpc.useQuery() for client-side data fetching

Files:

  • src/components/workshop/cursor/Conclusion.tsx
  • src/app/workshops/page.tsx
  • src/components/workshop/cursor/SignUpForm.tsx
  • src/app/workshops/[slug]/_components/testimonial.tsx
  • src/app/workshops/[slug]/_components/mdx-components.tsx
  • src/app/workshops/[slug]/_components/instructor-terminal.tsx
  • src/pages/workshop/cursor/index.tsx
  • src/components/workshop/cursor/active-sale.tsx
  • src/pages/workshop/cursor/invoice/[merchantChargeId].tsx
  • src/components/workshop/claude-code/SignUpForm.tsx
  • src/app/workshops/layout.tsx
  • src/app/workshops/[slug]/_components/feature-list.tsx
  • src/components/workshop/cursor/Hero.tsx
  • src/app/workshops/[slug]/_components/workshop-history.tsx
  • src/components/workshop/cursor/team/hero.tsx
  • src/components/workshop/cursor/WorkshopStructure.tsx
  • src/pages/workshop/claude-code/invoice/[merchantChargeId].tsx
  • src/components/markdown.tsx
  • src/app/workshops/[slug]/page.tsx
  • src/app/workshops/[slug]/_components/TestimonialBar.tsx
  • src/app/workshops/[slug]/not-found.tsx
  • src/app/workshops/[slug]/workshop-page-client.tsx
  • src/pages/workshop/cursor/team/index.tsx
  • src/components/workshop/cursor/Features.tsx
  • src/pages/workshop/claude-code/index.tsx
  • src/app/workshops/[slug]/_components/time-and-location.tsx
  • src/components/workshop/claude-code/active-sale.tsx
  • src/app/workshops/[slug]/_components/invoices/invoice-teaser.tsx
  • src/app/workshops/[slug]/_components/Hero.tsx
src/app/**/page.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Keep app route page files focused on orchestration/layout/SEO and under ~200–300 lines, importing extracted modules

Files:

  • src/app/workshops/page.tsx
  • src/app/workshops/[slug]/page.tsx
src/pages/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

In legacy pages, keep files focused on orchestration/layout and import extracted modules during migration

Files:

  • src/pages/workshop/cursor/index.tsx
  • src/pages/workshop/cursor/invoice/[merchantChargeId].tsx
  • src/pages/workshop/claude-code/invoice/[merchantChargeId].tsx
  • src/pages/workshop/cursor/team/index.tsx
  • src/pages/workshop/claude-code/index.tsx
src/lib/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Place database queries and data-fetching logic in src/lib/[feature] and mark server-only files with 'use server'

Files:

  • src/lib/events/get-event-coupons.ts
  • src/lib/events/transform-event-to-workshop.ts
  • src/lib/events/get-event.ts
  • src/lib/get-course-builder-metadata.ts
src/schemas/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

src/schemas/**/*.ts: Extract all Zod schemas and TypeScript types into src/schemas
Export inferred types from Zod schemas (e.g., export type Post = z.infer)

Files:

  • src/schemas/event.ts
package.json

📄 CodeRabbit inference engine (.cursor/rules/benchmarks-create.mdc)

package.json: Add a benchmark script to package.json that runs the benchmark file with pnpm benchmark.
Install the tinybench library if not already present.

package.json: Before modifying build-script settings, verify that package.json exists (e.g., check repository root) before attempting changes
Allow build scripts for specific trusted dependencies via package.json by setting pnpm.onlyBuiltDependencies (preferred for ongoing configurations)

Files:

  • package.json
**/*.json

📄 CodeRabbit inference engine (.cursor/rules/cli-wrangler.mdc)

**/*.json: When performing bulk KV operations with wrangler kv:bulk put or wrangler kv:bulk delete, ensure the input file is a JSON file formatted as specified (for put: [{"key":"foo", "value":"bar"},...]; for delete: ["foo", "bar", ...]).
When inserting vectors with wrangler vectorize insert, ensure the input file is a JSON file formatted as an array of objects with id, values, and optional metadata fields.

Files:

  • package.json
{package.json,pnpm-lock.yaml,yarn.lock,pyproject.toml,poetry.lock,go.mod,pom.xml,build.gradle,Gemfile,composer.json}

📄 CodeRabbit inference engine (.cursor/rules/project-update-user-rules.mdc)

Analyze project root and read configuration files for details on dependencies, scripts, language versions, tool configurations, and settings (e.g., package.json, pnpm-lock.yaml, yarn.lock, pyproject.toml, poetry.lock, go.mod, pom.xml, build.gradle, Gemfile, composer.json)

Files:

  • package.json
src/lib/**/*course-builder*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

src/lib/**/*course-builder*.ts: Always use the existing getConnectionPool() when accessing the Course Builder database
Ensure Course Builder DB access runs server-side only: guard with typeof window === 'undefined', use dynamic import wrappers, and never import mysql2 directly in client-bundled files
Use flexible slug matching in SQL: id = ? OR JSON_UNQUOTE(JSON_EXTRACT(fields, '$.slug')) = ?
Parse JSON fields safely: typeof row.fields === 'string' ? JSON.parse(row.fields) : row.fields
Identify content types as specified (courses: type='post' with fields.postType='course'; videos: type='videoResource') and model relationships via egghead_ContentResourceResource
Always release DB connections in a finally block, return null for missing data, and log helpful debug messages

Files:

  • src/lib/get-course-builder-metadata.ts
docs/*

📄 CodeRabbit inference engine (.cursor/rules/docs-structure.mdc)

Review all files within the docs/ directory (e.g., README.md, CONTRIBUTING.md, architecture documents) for existing descriptions, guidelines, or discussions related to the project's intended file structure, module organization, or component layout.

Files:

  • docs/workshop-stripe-integration-plan.md
  • docs/workshop-event-database-migration-proposal.md
docs/**

📄 CodeRabbit inference engine (.cursor/rules/docs-sync.mdc)

docs/**: Review and update all project documentation within the docs/ directory to ensure it accurately reflects the latest project state, recent code changes, and completed tasks.
Read all files located within the docs/ directory and its subdirectories, paying close attention to READMEs, guides, architectural documents, and usage instructions.
Apply edits directly to the relevant files within the docs/ directory to correct outdated, inaccurate, or incomplete documentation.

docs/**: Review and update all project documentation within the docs/ directory to ensure it accurately reflects the latest project state, recent code changes, and completed tasks.
Read all files located within the docs/ directory and its subdirectories, paying close attention to READMEs, guides, architectural documents, and usage instructions.
Apply edits directly to the relevant files within the docs/ directory to ensure documentation is consistent, accurate, and fully synchronized with the project's latest developments.
Identify and update any sections, instructions, diagrams, or examples in docs/* that are out-of-date, inaccurate, or incomplete to reflect the current state of the project.

Mandatory context review: read all relevant files in the project's docs/ directory before writing any part of the plan

docs/**: Scan all files within the docs/ directory and its subdirectories for occurrences of the keyword 'TODO'
Search for 'TODO' in a case-insensitive manner when supported by the tool
For each identified TODO in documentation, extract the full text of the TODO comment
Record the file path and line number for each TODO found in documentation

Thoroughly read all files within the docs/ directory and its subdirectories, prioritizing README.md, CONTRIBUTING.md, and any technical specifications

Files:

  • docs/workshop-stripe-integration-plan.md
  • docs/workshop-event-database-migration-proposal.md
{README.md,docs/**}

📄 CodeRabbit inference engine (.cursor/rules/task-next.mdc)

Review relevant project documentation, specifically files in docs/ and README.md, to understand context

Files:

  • docs/workshop-stripe-integration-plan.md
  • docs/workshop-event-database-migration-proposal.md
🧠 Learnings (1)
📚 Learning: 2025-09-02T17:59:16.787Z
Learnt from: CR
PR: skillrecordings/egghead-next#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-02T17:59:16.787Z
Learning: Applies to src/lib/**/*course-builder*.ts : Always use the existing getConnectionPool() when accessing the Course Builder database

Applied to files:

  • src/lib/get-course-builder-metadata.ts
🧬 Code graph analysis (10)
src/app/workshops/page.tsx (2)
src/lib/events/get-event.ts (1)
  • getPrimaryWorkshop (139-211)
src/app/workshops/[slug]/page.tsx (1)
  • revalidate (74-74)
src/lib/events/get-event-coupons.ts (1)
src/lib/get-course-builder-metadata.ts (1)
  • getConnectionPool (33-43)
src/app/workshops/layout.tsx (1)
src/app/providers.tsx (1)
  • Providers (8-18)
src/lib/events/transform-event-to-workshop.ts (2)
src/schemas/event.ts (1)
  • Event (144-144)
src/types.ts (1)
  • LiveWorkshop (24-24)
src/lib/events/get-event.ts (2)
src/schemas/event.ts (2)
  • Event (144-144)
  • EventSchema (128-138)
src/lib/db.ts (1)
  • getPool (5-22)
src/app/workshops/[slug]/page.tsx (4)
src/lib/events/get-event.ts (2)
  • getEventBySlug (59-96)
  • getAllWorkshops (101-133)
src/lib/events/get-event-coupons.ts (1)
  • getEventCoupons (22-102)
src/app/workshops/[slug]/workshop-page-client.tsx (1)
  • WorkshopPageClient (26-118)
src/app/workshops/page.tsx (1)
  • revalidate (36-36)
src/app/workshops/[slug]/_components/TestimonialBar.tsx (1)
src/app/workshops/[slug]/_components/testimonial.tsx (1)
  • Testimonial (3-48)
src/app/workshops/[slug]/workshop-page-client.tsx (3)
src/lib/events/get-event-coupons.ts (1)
  • EventCoupon (4-15)
src/app/workshops/[slug]/_components/Hero.tsx (2)
  • SignUpFormRef (14-16)
  • Hero (157-334)
src/app/workshops/[slug]/_components/mdx-components.tsx (1)
  • workshopMDXComponents (17-25)
src/components/workshop/claude-code/active-sale.tsx (2)
src/lib/events/get-event-coupons.ts (1)
  • EventCoupon (4-15)
src/hooks/use-commerce-machine.ts (1)
  • useCommerceMachine (35-100)
src/app/workshops/[slug]/_components/Hero.tsx (2)
src/app/workshops/[slug]/_components/animations.ts (2)
  • scaleIn (23-28)
  • fadeInUp (1-6)
src/app/workshops/[slug]/_components/claude-hero-logo.tsx (1)
  • Logo (1-59)
🪛 markdownlint-cli2 (0.18.1)
docs/workshop-stripe-integration-plan.md

182-182: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


569-569: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


576-576: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


607-607: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


617-617: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

docs/workshop-event-database-migration-proposal.md

23-23: Link fragments should be valid

(MD051, link-fragments)


24-24: Link fragments should be valid

(MD051, link-fragments)


25-25: Link fragments should be valid

(MD051, link-fragments)


26-26: Link fragments should be valid

(MD051, link-fragments)


27-27: Link fragments should be valid

(MD051, link-fragments)


28-28: Link fragments should be valid

(MD051, link-fragments)


29-29: Link fragments should be valid

(MD051, link-fragments)


225-225: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


343-343: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


361-361: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


503-503: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


1297-1297: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (28)
src/app/workshops/[slug]/_components/invoices/invoice-teaser.tsx (1)

8-8: Type import path updated correctly.

The Purchase type import has been consolidated to the new app-based path, consistent with the broader refactoring effort.

src/pages/workshop/cursor/invoice/[merchantChargeId].tsx (1)

8-9: Import paths updated consistently.

The import path changes mirror those in the claude-code invoice page, confirming this is a consistent refactoring pattern across all workshop invoice pages.

src/pages/workshop/claude-code/invoice/[merchantChargeId].tsx (1)

8-9: Import paths verified and correct. Both InvoiceCustomText and InvoicePrintButton resolve to existing files and exports, and no residual old import paths remain.

src/pages/workshop/cursor/team/index.tsx (1)

8-8: LGTM! Import path consolidation aligns with the component reorganization.

The import path update from @/components/workshop/shared/Instructor to @/app/workshops/[slug]/_components/Instructor is part of the broader refactor to centralize workshop components under the new app router structure.

src/components/markdown/serialize-mdx.ts (2)

88-88: Explicit MDX format specification improves clarity.

Adding format: 'mdx' explicitly declares the content format, which is a good practice for maintainability and aligns with the MDX-driven workshop rendering approach.


104-104: Approve disabling frontmatter parsing No frontmatter blocks detected in .md/.mdx files; disabling parsing is safe.

src/pages/workshop/claude-code/index.tsx (1)

3-18: LGTM! Import path consolidation aligns with the new workshop architecture.

All component imports have been updated to reference the new centralized location at @/app/workshops/[slug]/_components/. This consolidation is consistent with the broader refactor to establish a single dynamic workshop route with shared components.

src/app/workshops/[slug]/_components/workshop-history.tsx (1)

20-20: LGTM! The not-prose class correctly prevents prose styles from affecting custom terminal styling.

Adding not-prose to this section is appropriate because the component uses custom terminal-themed styling (command prompts, colored text, etc.) that would conflict with default prose styles. This change is consistent with similar styling updates across other workshop components.

src/app/workshops/[slug]/_components/testimonial.tsx (1)

15-15: LGTM! The not-prose class appropriately preserves custom testimonial styling.

Adding not-prose prevents Tailwind's prose typography styles from interfering with the carefully crafted testimonial design (custom gradients, spacing, and layout). This is consistent with similar styling adjustments across other workshop components in this refactor.

src/app/workshops/[slug]/not-found.tsx (1)

1-22: LGTM! Clean not-found page implementation.

The not-found page provides a clear, user-friendly 404 experience for missing workshop routes. The implementation correctly uses Next.js App Router conventions, includes proper dark mode support, and offers clear navigation back to the workshops list.

package.json (2)

128-129: Date-fns-tz compatibility verified date-fns-tz@^2.0.0 requires date-fns>=2.0.0, which is satisfied by date-fns@^2.30.0.


172-172: Verify MDX plugin compatibility with MDX v3
List of MDX-related packages in package.json:

Ensure each plugin is MDX v3-compatible, include syntax-highlighting assets (e.g. shiki) in builds, and validate serialization and runtime rendering (ISR/revalidation).

src/app/workshops/[slug]/page.tsx (3)

1-8: LGTM!

The imports are well-organized and use the correct path aliases. The combination of server-side data loading functions and client component wrapper follows Next.js App Router best practices.


79-106: Handle async params correctly in generateMetadata.

The params prop is correctly awaited on line 80, which aligns with Next.js 15 requirements where params is a Promise. Good implementation.


34-46: Confirm MDX context scope usage.

  • Static components consume event but no coupons references detected.
  • Verify that your dynamic MDX content actually uses coupons, or remove it from the scope passed to serializeMDX.
src/app/workshops/[slug]/_components/TestimonialBar.tsx (3)

11-30: LGTM!

Renaming to defaultTestimonials and extracting it as a constant improves clarity and enables prop-driven testimonials while maintaining backward compatibility.


32-38: LGTM!

The optional testimonials prop with default value pattern is clean and follows React best practices. TypeScript typing is correct.


44-44: LGTM!

Adding not-prose prevents Tailwind prose styles from affecting the testimonial bar layout, which is correct for a custom-styled component within MDX content.

src/app/workshops/[slug]/workshop-page-client.tsx (1)

3-3: LGTM!

Correctly imports only useRef and useMemo from React, following best practices for tree-shaking.

src/components/markdown.tsx (3)

4-10: LGTM!

The typed Components import and MarkdownProps interface provide proper TypeScript safety while maintaining flexibility for component overrides.


18-26: LGTM!

The defaultComponents provide sensible styling defaults while remaining overridable through the customComponents prop. The component spread pattern on line 31 correctly prioritizes custom components.


34-36: LGTM!

Correctly wraps children within ReactMarkdown element instead of using self-closing tag. This is the proper ReactMarkdown API usage.

src/lib/events/get-event.ts (4)

1-1: LGTM!

The 'use server' directive correctly marks this module as server-only, preventing client-side bundling of database code.


10-13: LGTM!

The parseEventFields helper safely handles both string and parsed JSON fields, preventing parse errors.


19-53: LGTM!

Connection is properly acquired and released in the finally block, preventing connection leaks. The parameterized query prevents SQL injection.


64-69: SQL injection safety verified Parameterized ? placeholders safely escape inputs in JSON_UNQUOTE(JSON_EXTRACT(...)) comparisons.

src/lib/events/get-event-coupons.ts (1)

82-83: Confirm percentageDiscount units

Is percentageDiscount stored as 0.15 (fraction) or 15 (percent)? Downstream pricing multiplies basePrice by this value. If DB stores whole percentages, divide by 100 when mapping.

Would you confirm the DB convention? If needed, I can generate a follow-up diff to normalize here to a fraction.

src/components/workshop/claude-code/active-sale.tsx (1)

283-309: Verify discount unit (fraction vs percent) before multiplying

If percentageDiscount is stored as 15 (percent), current math over-discounts 15×.

Would you confirm the storage convention? If it’s 15, change to discount = basePrice * (pct / 100). I can provide the exact diff once confirmed.

Comment on lines +12 to 13
import {format, formatInTimeZone} from 'date-fns-tz'

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect import: format should come from date-fns, not date-fns-tz

This will fail at build/runtime.

Apply this diff:

-import {format, formatInTimeZone} from 'date-fns-tz'
+import {formatInTimeZone} from 'date-fns-tz'
+import {format} from 'date-fns'
🤖 Prompt for AI Agents
In src/app/workshops/[slug]/_components/Hero.tsx around lines 12-13, the current
line imports format from date-fns-tz which is incorrect; replace the import so
that format is imported from 'date-fns' and formatInTimeZone remains imported
from 'date-fns-tz' (i.e. import { format } from 'date-fns' and import {
formatInTimeZone } from 'date-fns-tz') to avoid build/runtime failure.

Comment on lines +54 to +68
const mdxComponentsWithContext = useMemo(
() => ({
...workshopMDXComponents,
// Pass context props to Hero component
Hero: (props: any) => (
<Hero
{...props}
formRef={formRef}
saleisActive={saleIsActive}
event={event}
/>
),
}),
[formRef, saleIsActive, event],
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix useMemo dependency array.

The dependency array on line 67 includes formRef, but refs should not be dependencies as their .current property changes don't trigger re-renders. The formRef object identity is stable across renders, making it unnecessary in the dependency array.

Apply this diff:

   const mdxComponentsWithContext = useMemo(
     () => ({
       ...workshopMDXComponents,
       // Pass context props to Hero component
       Hero: (props: any) => (
         <Hero
           {...props}
           formRef={formRef}
           saleisActive={saleIsActive}
           event={event}
         />
       ),
     }),
-    [formRef, saleIsActive, event],
+    [saleIsActive, event],
   )
📝 Committable suggestion

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

Suggested change
const mdxComponentsWithContext = useMemo(
() => ({
...workshopMDXComponents,
// Pass context props to Hero component
Hero: (props: any) => (
<Hero
{...props}
formRef={formRef}
saleisActive={saleIsActive}
event={event}
/>
),
}),
[formRef, saleIsActive, event],
)
const mdxComponentsWithContext = useMemo(
() => ({
...workshopMDXComponents,
// Pass context props to Hero component
Hero: (props: any) => (
<Hero
{...props}
formRef={formRef}
saleisActive={saleIsActive}
event={event}
/>
),
}),
[saleIsActive, event],
)
🤖 Prompt for AI Agents
In src/app/workshops/[slug]/workshop-page-client.tsx around lines 54 to 68, the
useMemo dependency array incorrectly includes formRef (a stable ref whose
identity doesn't change); remove formRef from the dependency array and keep
[saleIsActive, event] only so the memoized mdxComponentsWithContext updates
correctly when relevant values change while avoiding unnecessary dependencies.

Comment on lines 1 to 4
import {AsteriskIcon} from 'lucide-react'
import Link from 'next/link'
import TimeAndLocation from '../shared/time-and-location'
import TimeAndLocation from '../../../app/workshops/[slug]/_components/time-and-location'
import {useCommerceMachine} from '@/hooks/use-commerce-machine'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

This component uses hooks but lacks 'use client'

ActiveSale (and nested components here) use React hooks; without 'use client', Next.js App Router will error.

Apply this diff:

+ 'use client'
+
 import {AsteriskIcon} from 'lucide-react'
 import Link from 'next/link'
-import TimeAndLocation from '../../../app/workshops/[slug]/_components/time-and-location'
+import TimeAndLocation from '@/app/workshops/[slug]/_components/time-and-location'

As per coding guidelines

📝 Committable suggestion

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

Suggested change
import {AsteriskIcon} from 'lucide-react'
import Link from 'next/link'
import TimeAndLocation from '../shared/time-and-location'
import TimeAndLocation from '../../../app/workshops/[slug]/_components/time-and-location'
import {useCommerceMachine} from '@/hooks/use-commerce-machine'
'use client'
import {AsteriskIcon} from 'lucide-react'
import Link from 'next/link'
import TimeAndLocation from '@/app/workshops/[slug]/_components/time-and-location'
import {useCommerceMachine} from '@/hooks/use-commerce-machine'
// …rest of the file…
🤖 Prompt for AI Agents
In src/components/workshop/claude-code/active-sale.tsx around lines 1 to 4, the
component (and nested components) use React hooks but the file lacks the
required "use client" directive; add a top-of-file "use client" directive as the
very first line so Next.js treats this module as a client component, and ensure
any child components that also use hooks are either client components or
imported from client modules.

import type {Event} from '@/schemas/event'
import type {EventCoupon} from '@/lib/events/get-event-coupons'
import Spinner from '@/components/spinner'
import {format, formatInTimeZone} from 'date-fns-tz'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect import: format should come from date-fns, not date-fns-tz

This import will break usage of format.

Apply this diff:

-import {format, formatInTimeZone} from 'date-fns-tz'
+import {formatInTimeZone} from 'date-fns-tz'
+import {format} from 'date-fns'
📝 Committable suggestion

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

Suggested change
import {format, formatInTimeZone} from 'date-fns-tz'
import {formatInTimeZone} from 'date-fns-tz'
import {format} from 'date-fns'
🤖 Prompt for AI Agents
In src/components/workshop/claude-code/active-sale.tsx around line 13, the
import currently brings format from date-fns-tz which is incorrect and will
break usage; update the import so that format is imported from 'date-fns' and
formatInTimeZone remains imported from 'date-fns-tz' (i.e., replace the single
import line with two named imports: format from 'date-fns' and formatInTimeZone
from 'date-fns-tz').

Comment on lines +183 to +185
const paymentLink = `${event.fields.stripePaymentLink || ''}${
couponToApply.queryParam
}`
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Build payment link using URLSearchParams; concatenation is brittle

If stripePaymentLink already contains a query string, simple concatenation creates invalid URLs.

Apply this diff:

-  const paymentLink = `${event.fields.stripePaymentLink || ''}${
-    couponToApply.queryParam
-  }`
+  const paymentLink = (() => {
+    const base = event.fields.stripePaymentLink || ''
+    if (!base) return ''
+    try {
+      const url = new URL(base)
+      const code = couponToApply.queryParam.replace(
+        '?prefilled_promo_code=',
+        '',
+      )
+      if (code) url.searchParams.set('prefilled_promo_code', code)
+      return url.toString()
+    } catch {
+      // Fallback to original behavior if base isn't an absolute URL
+      return `${base}${couponToApply.queryParam}`
+    }
+  })()
📝 Committable suggestion

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

Suggested change
const paymentLink = `${event.fields.stripePaymentLink || ''}${
couponToApply.queryParam
}`
const paymentLink = (() => {
const base = event.fields.stripePaymentLink || ''
if (!base) return ''
try {
const url = new URL(base)
const code = couponToApply.queryParam.replace(
'?prefilled_promo_code=',
'',
)
if (code) url.searchParams.set('prefilled_promo_code', code)
return url.toString()
} catch {
// Fallback to original behavior if base isn't an absolute URL
return `${base}${couponToApply.queryParam}`
}
})()
🤖 Prompt for AI Agents
In src/components/workshop/claude-code/active-sale.tsx around lines 183-185, the
paymentLink is built via string concatenation which breaks when
stripePaymentLink already has a query string; instead parse stripePaymentLink
into a URL (fall back to a base URL if it's a relative path), use
URLSearchParams (or the URL.searchParams API) to append/merge
couponToApply.queryParam key/value into the existing query string, then
serialize the URL back to a string; ensure you handle empty or undefined
stripePaymentLink by starting from '' or a known base and preserve existing
params without creating malformed URLs.

Comment on lines +4 to +8
import {
fadeInUp,
staggerContainer,
staggerItem,
} from '../../../app/workshops/[slug]/_components/animations'
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Reconsider the animation import path.

Importing from app/workshops/[slug]/_components/animations creates a dependency from a reusable component (src/components/workshop/cursor/) to a route-specific internal folder. This breaks modularity and makes the component tightly coupled to a specific route structure.

If these animations are shared across multiple workshop components, consider moving them to a shared location such as src/lib/animations or src/components/animations. Additionally, the coding guidelines recommend using the @/ path alias for imports from the src directory.

Run the following script to check where these animations are used and whether they're truly route-specific or shared:


🏁 Script executed:

#!/bin/bash
# Check usage of these animation utilities across the codebase
rg -n --type=tsx --type=ts -C3 '\b(fadeInUp|staggerContainer|staggerItem)\b' \
  -g '!node_modules' -g '!dist' -g '!build'

Length of output: 27


🌐 Web query:

Are Next.js App Router _components folders intended for internal route-specific components or can they be imported from outside the route?

💡 Result:

Short answer: _components (private folders) are intended as implementation‑detail, route‑specific colocated files but they are regular modules and can be imported from outside the route if you reference them by path. They’re opted out of the routing system (not routable) but not magically private to imports. [1][2]

Sources:

  • Next.js App Router — Colocation & Private folders (explain _folders opt out of routing). [1]
  • App Router routing overview (folders define routes; colocated files are allowed; only page/route files are public). [2]

🏁 Script executed:

#!/bin/bash
# Re-run search without type filters to locate animation utilities usage
rg -n -C3 '(fadeInUp|staggerContainer|staggerItem)' \
  -g '*.tsx' -g '*.ts' -g '*.js' \
  -g '!node_modules' -g '!dist' -g '!build'

Length of output: 16467


Extract animations into a shared module

  • Move src/app/workshops/[slug]/_components/animations.ts to a shared location (e.g. src/lib/animations.ts).
  • Update all imports in src/components/workshop/** to:
    import { fadeInUp, staggerContainer, staggerItem } from '@/lib/animations'
  • Drop imports from the route’s _components folder and use the @/ alias for consistency.
🤖 Prompt for AI Agents
In src/components/workshop/cursor/Features.tsx around lines 4 to 8, the file
currently imports animations from the route-specific _components folder; move
src/app/workshops/[slug]/_components/animations.ts to a shared module at
src/lib/animations.ts (or another agreed shared path) and update this file’s
import to: import { fadeInUp, staggerContainer, staggerItem } from
'@/lib/animations'; also update every other file under
src/components/workshop/** to use the same '@/lib/animations' alias and remove
references to the route’s _components folder so all animation imports are
consistent.

Comment on lines +18 to +54
export async function getEventById(id: string): Promise<Event | null> {
const pool = getPool()
const conn = await pool.getConnection()

try {
const [rows] = await conn.execute<RowDataPacket[]>(
`SELECT * FROM egghead_ContentResource
WHERE type = 'event' AND id = ?
LIMIT 1`,
[id],
)

if (!rows[0]) {
console.log('No event found for ID:', id)
return null
}

const row = rows[0]
const eventData = {
...row,
fields: parseEventFields(row),
}

const result = EventSchema.safeParse(eventData)
if (!result.success) {
console.error('Event validation failed:', result.error)
throw new Error(`Invalid event data: ${result.error.message}`)
}

return result.data
} catch (error) {
console.error('Error in getEventById:', error)
throw error
} finally {
conn.release()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent error handling between functions.

getEventById throws on validation failure (line 44), while other functions like getEventBySlug (line 86) return null. This inconsistency could lead to unexpected behavior.

Consider making error handling consistent. For data access functions, returning null for not-found/invalid is typically preferred:

     const result = EventSchema.safeParse(eventData)
     if (!result.success) {
       console.error('Event validation failed:', result.error)
-      throw new Error(`Invalid event data: ${result.error.message}`)
+      return null
     }
 
     return result.data
   } catch (error) {
     console.error('Error in getEventById:', error)
-    throw error
+    return null
   } finally {
📝 Committable suggestion

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

Suggested change
export async function getEventById(id: string): Promise<Event | null> {
const pool = getPool()
const conn = await pool.getConnection()
try {
const [rows] = await conn.execute<RowDataPacket[]>(
`SELECT * FROM egghead_ContentResource
WHERE type = 'event' AND id = ?
LIMIT 1`,
[id],
)
if (!rows[0]) {
console.log('No event found for ID:', id)
return null
}
const row = rows[0]
const eventData = {
...row,
fields: parseEventFields(row),
}
const result = EventSchema.safeParse(eventData)
if (!result.success) {
console.error('Event validation failed:', result.error)
throw new Error(`Invalid event data: ${result.error.message}`)
}
return result.data
} catch (error) {
console.error('Error in getEventById:', error)
throw error
} finally {
conn.release()
}
}
export async function getEventById(id: string): Promise<Event | null> {
const pool = getPool()
const conn = await pool.getConnection()
try {
const [rows] = await conn.execute<RowDataPacket[]>(
`SELECT * FROM egghead_ContentResource
WHERE type = 'event' AND id = ?
LIMIT 1`,
[id],
)
if (!rows[0]) {
console.log('No event found for ID:', id)
return null
}
const row = rows[0]
const eventData = {
...row,
fields: parseEventFields(row),
}
const result = EventSchema.safeParse(eventData)
if (!result.success) {
console.error('Event validation failed:', result.error)
return null
}
return result.data
} catch (error) {
console.error('Error in getEventById:', error)
return null
} finally {
conn.release()
}
}
🤖 Prompt for AI Agents
In src/lib/events/get-event.ts around lines 18 to 54, the function currently
throws an error when EventSchema validation fails which is inconsistent with
getEventBySlug that returns null for invalid/not-found events; update
getEventById to return null on validation failure instead of throwing: after
validating with EventSchema.safeParse, if result.success is false log the
validation error (console.error) and return null; keep the existing not-found
handling, error logging in the catch block, and the finally conn.release()
unchanged.

Comment on lines +106 to +112
const [rows] = await conn.execute<RowDataPacket[]>(
`SELECT * FROM egghead_ContentResource
WHERE type = 'event'
-- AND JSON_UNQUOTE(JSON_EXTRACT(fields, '$.state')) = 'published'
-- AND JSON_UNQUOTE(JSON_EXTRACT(fields, '$.visibility')) = 'public'
ORDER BY JSON_EXTRACT(fields, '$.startsAt') DESC`,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Document or remove commented-out filters.

Lines 109-110 have commented filters for state='published' and visibility='public'. This pattern appears in multiple functions throughout the file. These filters seem important for production but are currently disabled.

Either:

  1. Document why these filters are commented out (e.g., "TODO: Enable after migration")
  2. Add them to a configuration/feature flag
  3. Remove if genuinely not needed

Run this script to find all occurrences:


🏁 Script executed:

#!/bin/bash
# Find all commented state/visibility filters
rg -nP "-- AND JSON_UNQUOTE.*\.(state|visibility)" src/lib/events/get-event.ts

Length of output: 443


🏁 Script executed:

#!/bin/bash
# Find commented JSON_UNQUOTE filters for state or visibility in get-event.ts
rg -nP -- '^\s*--.*JSON_UNQUOTE.*\.(state|visibility)' src/lib/events/get-event.ts

Length of output: 631


Document or remove commented‐out state/visibility filters: in get-event.ts at lines 109–110, 156–157, 180–181, and 228–229 the JSON_UNQUOTE filters for state='published' and visibility='public' are disabled; either restore them behind a feature flag, add a TODO explaining why they’re commented out, or remove them if no longer needed.

🤖 Prompt for AI Agents
In src/lib/events/get-event.ts around lines 106–112 (and similarly at lines
156–157, 180–181, 228–229) the JSON_UNQUOTE(JSON_EXTRACT(...)) filters for
state='published' and visibility='public' are commented out; either restore them
behind a boolean feature flag or add a clear TODO explaining why they are
disabled. Implement one of two fixes: (A) restore filters by wrapping the WHERE
clause additions with an environment/feature-flag check (e.g., if
ENABLE_PUBLISHED_VISIBILITY_FILTER then include the two JSON_UNQUOTE conditions
in the SQL) and document the flag in README/env, or (B) if you intentionally
disabled them permanently, remove the commented lines and add a single-line TODO
above the query that states why they were removed and when/if they should be
reinstated. Ensure the change is applied consistently at all four locations and
include a short code comment that references the feature flag or the rationale.

WHERE type = 'event'
-- AND JSON_UNQUOTE(JSON_EXTRACT(fields, '$.state')) = 'published'
-- AND JSON_UNQUOTE(JSON_EXTRACT(fields, '$.visibility')) = 'public'
AND STR_TO_DATE(JSON_UNQUOTE(JSON_EXTRACT(fields, '$.startsAt')), '%Y-%m-%dT%H:%i:%s') >= NOW()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify STR_TO_DATE format matches startsAt serialization.

Line 182 uses STR_TO_DATE with format '%Y-%m-%dT%H:%i:%s' to parse ISO 8601 timestamps. Ensure that the startsAt field stored in the database matches this exact format.

Verify the startsAt format in the database:


🏁 Script executed:

#!/bin/bash
# Check EventFieldsSchema for startsAt field definition
ast-grep --pattern $'startsAt: z.$_'
rg -nP "startsAt" src/schemas/event.ts -C3

Length of output: 247


Update STR_TO_DATE format to match stored ISO datetime
The startsAt values include a .000Z suffix, so '%Y-%m-%dT%H:%i:%s' won’t parse them. Update to e.g. '%Y-%m-%dT%H:%i:%s.%fZ' or strip the trailing Z before parsing.

🤖 Prompt for AI Agents
In src/lib/events/get-event.ts around line 182, the STR_TO_DATE format used to
parse JSON_EXTRACT(fields, '$.startsAt') is '%Y-%m-%dT%H:%i:%s' which fails for
ISO datetimes containing the '.000Z' suffix; update the parsing to accept
fractional seconds and the trailing Z (for example use '%Y-%m-%dT%H:%i:%s.%fZ')
or alternatively strip the trailing 'Z' (and ensure fractional seconds exist)
before calling STR_TO_DATE so the datetime comparison against NOW() works
correctly.

Comment on lines +69 to +88
const workshop: LiveWorkshop = {
date: format(startDate, 'yyyy-MM-dd'),
startTime: formatInTimeZone(startDate, fields.timezone, 'h:mm a'),
endTime: endDate
? formatInTimeZone(endDate, fields.timezone, 'h:mm a')
: '',
timeZone: getTimeZoneName(fields.timezone),
utcOffset: getUtcOffset(fields.timezone, startDate),
isSaleLive: fields.isSaleLive ?? fields.state === 'published',
isEuFriendly: isEuFriendlyTime(startDate, fields.timezone),
isEarlyBird: fields.isEarlyBird ?? false,
productId: fields.stripeProductId ?? '',
workshopPrice: fields.workshopPrice ?? '',
stripePaymentLink: fields.stripePaymentLink ?? '',
stripeEarlyBirdMemberCouponCode: fields.coupons?.earlyBirdMember ?? '',
stripeMemberCouponCode: fields.coupons?.member ?? '',
stripeEarlyBirdCouponCode: fields.coupons?.earlyBird ?? '',
stripeEarlyBirdMemberDiscount: fields.discounts?.earlyBirdMember ?? '',
stripeMemberDiscount: fields.discounts?.member ?? '',
stripeEarlyBirdNonMemberDiscount: fields.discounts?.earlyBird ?? '',
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix timezone-aware workshop date.

date is formatted with the server’s local timezone, so events that start near midnight in their own timezone can render on the wrong calendar day (e.g., Tokyo events show the previous date when the server runs in UTC). Format the date in the event’s timezone to avoid off-by-one-day errors.

-      date: format(startDate, 'yyyy-MM-dd'),
+      date: formatInTimeZone(startDate, fields.timezone, 'yyyy-MM-dd'),
📝 Committable suggestion

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

Suggested change
const workshop: LiveWorkshop = {
date: format(startDate, 'yyyy-MM-dd'),
startTime: formatInTimeZone(startDate, fields.timezone, 'h:mm a'),
endTime: endDate
? formatInTimeZone(endDate, fields.timezone, 'h:mm a')
: '',
timeZone: getTimeZoneName(fields.timezone),
utcOffset: getUtcOffset(fields.timezone, startDate),
isSaleLive: fields.isSaleLive ?? fields.state === 'published',
isEuFriendly: isEuFriendlyTime(startDate, fields.timezone),
isEarlyBird: fields.isEarlyBird ?? false,
productId: fields.stripeProductId ?? '',
workshopPrice: fields.workshopPrice ?? '',
stripePaymentLink: fields.stripePaymentLink ?? '',
stripeEarlyBirdMemberCouponCode: fields.coupons?.earlyBirdMember ?? '',
stripeMemberCouponCode: fields.coupons?.member ?? '',
stripeEarlyBirdCouponCode: fields.coupons?.earlyBird ?? '',
stripeEarlyBirdMemberDiscount: fields.discounts?.earlyBirdMember ?? '',
stripeMemberDiscount: fields.discounts?.member ?? '',
stripeEarlyBirdNonMemberDiscount: fields.discounts?.earlyBird ?? '',
const workshop: LiveWorkshop = {
date: formatInTimeZone(startDate, fields.timezone, 'yyyy-MM-dd'),
startTime: formatInTimeZone(startDate, fields.timezone, 'h:mm a'),
endTime: endDate
? formatInTimeZone(endDate, fields.timezone, 'h:mm a')
: '',
timeZone: getTimeZoneName(fields.timezone),
utcOffset: getUtcOffset(fields.timezone, startDate),
isSaleLive: fields.isSaleLive ?? fields.state === 'published',
isEuFriendly: isEuFriendlyTime(startDate, fields.timezone),
isEarlyBird: fields.isEarlyBird ?? false,
productId: fields.stripeProductId ?? '',
workshopPrice: fields.workshopPrice ?? '',
stripePaymentLink: fields.stripePaymentLink ?? '',
stripeEarlyBirdMemberCouponCode: fields.coupons?.earlyBirdMember ?? '',
stripeMemberCouponCode: fields.coupons?.member ?? '',
stripeEarlyBirdCouponCode: fields.coupons?.earlyBird ?? '',
stripeEarlyBirdMemberDiscount: fields.discounts?.earlyBirdMember ?? '',
stripeMemberDiscount: fields.discounts?.member ?? '',
stripeEarlyBirdNonMemberDiscount: fields.discounts?.earlyBird ?? '',
};
🤖 Prompt for AI Agents
In src/lib/events/transform-event-to-workshop.ts around lines 69 to 88, the
workshop.date is being formatted with the server’s local timezone
(format(startDate, 'yyyy-MM-dd')) causing off-by-one-day errors for events near
midnight; replace that call with the timezone-aware formatter used elsewhere
(formatInTimeZone(startDate, fields.timezone, 'yyyy-MM-dd')) so the date is
computed in the event’s timezone; ensure you keep using the same startDate
variable and timezone value (fields.timezone) as used for startTime/endTime and
utcOffset.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants