-
Notifications
You must be signed in to change notification settings - Fork 194
Pull workshop data from Course builder workshop events WIP step 1 #1541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- 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]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 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.
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 tocreatePoolinsrc/lib/db.tsinstantiate a separate pool; import and call the newly exportedgetConnectionPool()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
fadeInUpfromapp/workshops/[slug]/_components/animationscreates coupling between this reusable component (located insrc/components/) and a route-specific implementation. Shared animation utilities should typically live in a common location likesrc/lib/animationsorsrc/utils/animationsrather 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/animationsis difficult to maintain. As per coding guidelines, prefer the@/path alias for imports from thesrcdirectory.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, andstaggerItemare correctly exported fromsrc/app/workshops/[slug]/_components/animations.ts. To avoid brittle deep relative imports acrosssrc/andapp/, 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:
- Missing language identifiers in fenced code blocks (line 182): Add language specifiers to all code blocks for proper syntax highlighting
- 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:
- Invalid link fragments (lines 23-29): Table of contents links may not resolve correctly
- 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
parseDateTimeWithOffsetandformatTimeInTimeZoneto a shared utility location likesrc/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-tzas a dependency, consider using its utilities likeformatInTimeZoneandzonedTimeToUtcfor 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 undersrc/. 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) usesexport const revalidate = 0(on-demand) while detail pages ([slug]/page.tsx) useexport const revalidate = 3600; add a comment explaining why they differ.
20-28: Remove redundant type check
getEventBySlugalready filters bytype = 'event'andEventSchemaenforces it, soevent.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 couponsAlso applies to: 92-94
37-45: Add typed RowDataPacket interfaces for stronger TS safetyExplicit 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 productionConsole 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/noiseparseDateTimeWithOffset and formatTimeInTimeZone aren’t used.
Delete these helpers or move to a shared util when needed.
189-195: Avoid showing "Error" strings in UI fallbackIf 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_ENVThese 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 displayRaw 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 navigationAvoid 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 timezoneCurrently 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 priceTighten 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 clarityIf 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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.tsxsrc/app/workshops/page.tsxsrc/components/workshop/cursor/SignUpForm.tsxsrc/app/workshops/[slug]/_components/testimonial.tsxsrc/app/workshops/[slug]/_components/mdx-components.tsxsrc/app/workshops/[slug]/_components/instructor-terminal.tsxsrc/pages/workshop/cursor/index.tsxsrc/lib/events/get-event-coupons.tssrc/components/workshop/cursor/active-sale.tsxsrc/pages/workshop/cursor/invoice/[merchantChargeId].tsxsrc/components/workshop/claude-code/SignUpForm.tsxsrc/app/workshops/layout.tsxsrc/app/workshops/[slug]/_components/feature-list.tsxsrc/lib/events/transform-event-to-workshop.tssrc/components/workshop/cursor/Hero.tsxsrc/app/workshops/[slug]/_components/workshop-history.tsxsrc/schemas/event.tssrc/components/workshop/cursor/team/hero.tsxsrc/lib/events/get-event.tssrc/components/workshop/cursor/WorkshopStructure.tsxsrc/pages/workshop/claude-code/invoice/[merchantChargeId].tsxsrc/components/markdown.tsxsrc/app/workshops/[slug]/page.tsxsrc/app/workshops/[slug]/_components/TestimonialBar.tsxsrc/lib/get-course-builder-metadata.tssrc/app/workshops/[slug]/not-found.tsxsrc/app/workshops/[slug]/workshop-page-client.tsxsrc/pages/workshop/cursor/team/index.tsxsrc/components/workshop/cursor/Features.tsxsrc/pages/workshop/claude-code/index.tsxsrc/app/workshops/[slug]/_components/time-and-location.tsxsrc/components/workshop/claude-code/active-sale.tsxsrc/app/workshops/[slug]/_components/invoices/invoice-teaser.tsxsrc/app/workshops/[slug]/_components/Hero.tsxsrc/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.tsxsrc/app/workshops/page.tsxsrc/components/workshop/cursor/SignUpForm.tsxsrc/app/workshops/[slug]/_components/testimonial.tsxsrc/app/workshops/[slug]/_components/mdx-components.tsxsrc/app/workshops/[slug]/_components/instructor-terminal.tsxsrc/pages/workshop/cursor/index.tsxsrc/lib/events/get-event-coupons.tssrc/components/workshop/cursor/active-sale.tsxsrc/pages/workshop/cursor/invoice/[merchantChargeId].tsxsrc/components/workshop/claude-code/SignUpForm.tsxsrc/app/workshops/layout.tsxsrc/app/workshops/[slug]/_components/feature-list.tsxsrc/lib/events/transform-event-to-workshop.tssrc/components/workshop/cursor/Hero.tsxsrc/app/workshops/[slug]/_components/workshop-history.tsxsrc/schemas/event.tssrc/components/workshop/cursor/team/hero.tsxsrc/lib/events/get-event.tssrc/components/workshop/cursor/WorkshopStructure.tsxsrc/pages/workshop/claude-code/invoice/[merchantChargeId].tsxsrc/components/markdown.tsxsrc/app/workshops/[slug]/page.tsxsrc/app/workshops/[slug]/_components/TestimonialBar.tsxsrc/lib/get-course-builder-metadata.tssrc/app/workshops/[slug]/not-found.tsxsrc/app/workshops/[slug]/workshop-page-client.tsxsrc/pages/workshop/cursor/team/index.tsxsrc/components/workshop/cursor/Features.tsxsrc/pages/workshop/claude-code/index.tsxsrc/app/workshops/[slug]/_components/time-and-location.tsxsrc/components/workshop/claude-code/active-sale.tsxsrc/app/workshops/[slug]/_components/invoices/invoice-teaser.tsxsrc/app/workshops/[slug]/_components/Hero.tsxsrc/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.tsxsrc/app/workshops/page.tsxsrc/components/workshop/cursor/SignUpForm.tsxsrc/app/workshops/[slug]/_components/testimonial.tsxsrc/app/workshops/[slug]/_components/mdx-components.tsxsrc/app/workshops/[slug]/_components/instructor-terminal.tsxsrc/pages/workshop/cursor/index.tsxsrc/components/workshop/cursor/active-sale.tsxsrc/pages/workshop/cursor/invoice/[merchantChargeId].tsxsrc/components/workshop/claude-code/SignUpForm.tsxsrc/app/workshops/layout.tsxsrc/app/workshops/[slug]/_components/feature-list.tsxsrc/components/workshop/cursor/Hero.tsxsrc/app/workshops/[slug]/_components/workshop-history.tsxsrc/components/workshop/cursor/team/hero.tsxsrc/components/workshop/cursor/WorkshopStructure.tsxsrc/pages/workshop/claude-code/invoice/[merchantChargeId].tsxsrc/components/markdown.tsxsrc/app/workshops/[slug]/page.tsxsrc/app/workshops/[slug]/_components/TestimonialBar.tsxsrc/app/workshops/[slug]/not-found.tsxsrc/app/workshops/[slug]/workshop-page-client.tsxsrc/pages/workshop/cursor/team/index.tsxsrc/components/workshop/cursor/Features.tsxsrc/pages/workshop/claude-code/index.tsxsrc/app/workshops/[slug]/_components/time-and-location.tsxsrc/components/workshop/claude-code/active-sale.tsxsrc/app/workshops/[slug]/_components/invoices/invoice-teaser.tsxsrc/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.tsxsrc/app/workshops/page.tsxsrc/components/workshop/cursor/SignUpForm.tsxsrc/app/workshops/[slug]/_components/testimonial.tsxsrc/app/workshops/[slug]/_components/mdx-components.tsxsrc/app/workshops/[slug]/_components/instructor-terminal.tsxsrc/pages/workshop/cursor/index.tsxsrc/lib/events/get-event-coupons.tssrc/components/workshop/cursor/active-sale.tsxsrc/pages/workshop/cursor/invoice/[merchantChargeId].tsxsrc/components/workshop/claude-code/SignUpForm.tsxsrc/app/workshops/layout.tsxsrc/app/workshops/[slug]/_components/feature-list.tsxsrc/lib/events/transform-event-to-workshop.tssrc/components/workshop/cursor/Hero.tsxsrc/app/workshops/[slug]/_components/workshop-history.tsxsrc/schemas/event.tssrc/components/workshop/cursor/team/hero.tsxsrc/lib/events/get-event.tssrc/components/workshop/cursor/WorkshopStructure.tsxsrc/pages/workshop/claude-code/invoice/[merchantChargeId].tsxsrc/components/markdown.tsxsrc/app/workshops/[slug]/page.tsxsrc/app/workshops/[slug]/_components/TestimonialBar.tsxsrc/lib/get-course-builder-metadata.tssrc/app/workshops/[slug]/not-found.tsxsrc/app/workshops/[slug]/workshop-page-client.tsxsrc/pages/workshop/cursor/team/index.tsxsrc/components/workshop/cursor/Features.tsxsrc/pages/workshop/claude-code/index.tsxsrc/app/workshops/[slug]/_components/time-and-location.tsxsrc/components/workshop/claude-code/active-sale.tsxsrc/app/workshops/[slug]/_components/invoices/invoice-teaser.tsxsrc/app/workshops/[slug]/_components/Hero.tsxsrc/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.tsxsrc/components/workshop/cursor/SignUpForm.tsxsrc/components/workshop/cursor/active-sale.tsxsrc/components/workshop/claude-code/SignUpForm.tsxsrc/components/workshop/cursor/Hero.tsxsrc/components/workshop/cursor/team/hero.tsxsrc/components/workshop/cursor/WorkshopStructure.tsxsrc/components/markdown.tsxsrc/components/workshop/cursor/Features.tsxsrc/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.tsxsrc/app/workshops/page.tsxsrc/components/workshop/cursor/SignUpForm.tsxsrc/app/workshops/[slug]/_components/testimonial.tsxsrc/app/workshops/[slug]/_components/mdx-components.tsxsrc/app/workshops/[slug]/_components/instructor-terminal.tsxsrc/pages/workshop/cursor/index.tsxsrc/components/workshop/cursor/active-sale.tsxsrc/pages/workshop/cursor/invoice/[merchantChargeId].tsxsrc/components/workshop/claude-code/SignUpForm.tsxsrc/app/workshops/layout.tsxsrc/app/workshops/[slug]/_components/feature-list.tsxsrc/components/workshop/cursor/Hero.tsxsrc/app/workshops/[slug]/_components/workshop-history.tsxsrc/components/workshop/cursor/team/hero.tsxsrc/components/workshop/cursor/WorkshopStructure.tsxsrc/pages/workshop/claude-code/invoice/[merchantChargeId].tsxsrc/components/markdown.tsxsrc/app/workshops/[slug]/page.tsxsrc/app/workshops/[slug]/_components/TestimonialBar.tsxsrc/app/workshops/[slug]/not-found.tsxsrc/app/workshops/[slug]/workshop-page-client.tsxsrc/pages/workshop/cursor/team/index.tsxsrc/components/workshop/cursor/Features.tsxsrc/pages/workshop/claude-code/index.tsxsrc/app/workshops/[slug]/_components/time-and-location.tsxsrc/components/workshop/claude-code/active-sale.tsxsrc/app/workshops/[slug]/_components/invoices/invoice-teaser.tsxsrc/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.tsxsrc/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.tsxsrc/pages/workshop/cursor/invoice/[merchantChargeId].tsxsrc/pages/workshop/claude-code/invoice/[merchantChargeId].tsxsrc/pages/workshop/cursor/team/index.tsxsrc/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.tssrc/lib/events/transform-event-to-workshop.tssrc/lib/events/get-event.tssrc/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 abenchmarkscript topackage.jsonthat runs the benchmark file withpnpm benchmark.
Install thetinybenchlibrary 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 withwrangler kv:bulk putorwrangler 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 withwrangler vectorize insert, ensure the input file is a JSON file formatted as an array of objects withid,values, and optionalmetadatafields.
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.mddocs/workshop-event-database-migration-proposal.md
docs/**
📄 CodeRabbit inference engine (.cursor/rules/docs-sync.mdc)
docs/**: Review and update all project documentation within thedocs/directory to ensure it accurately reflects the latest project state, recent code changes, and completed tasks.
Read all files located within thedocs/directory and its subdirectories, paying close attention to READMEs, guides, architectural documents, and usage instructions.
Apply edits directly to the relevant files within thedocs/directory to correct outdated, inaccurate, or incomplete documentation.
docs/**: Review and update all project documentation within thedocs/directory to ensure it accurately reflects the latest project state, recent code changes, and completed tasks.
Read all files located within thedocs/directory and its subdirectories, paying close attention to READMEs, guides, architectural documents, and usage instructions.
Apply edits directly to the relevant files within thedocs/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 indocs/*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 documentationThoroughly 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.mddocs/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.mddocs/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
Purchasetype 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/Instructorto@/app/workshops/[slug]/_components/Instructoris 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! Thenot-proseclass correctly prevents prose styles from affecting custom terminal styling.Adding
not-proseto 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! Thenot-proseclass appropriately preserves custom testimonial styling.Adding
not-proseprevents 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:
- @mdx-js/[email protected], @mdx-js/[email protected], [email protected], [email protected], @code-hike/mdx@^0.9.0, load-egghead-notes-mdx-url@^0.1.1
- rehype-raw@^6.1.1, rehype-highlight@^7.0.0, rehype-shiki@^0.0.9
- remark-slug@^6.1.0, remark-gfm@^3.0.1, remark-footnotes@^2.0.0, remark-code-titles@^0.1.2
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
paramsprop is correctly awaited on line 80, which aligns with Next.js 15 requirements whereparamsis a Promise. Good implementation.
34-46: Confirm MDX context scope usage.
- Static components consume
eventbut nocouponsreferences detected.- Verify that your dynamic MDX content actually uses
coupons, or remove it from thescopepassed toserializeMDX.src/app/workshops/[slug]/_components/TestimonialBar.tsx (3)
11-30: LGTM!Renaming to
defaultTestimonialsand 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-proseprevents 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
useRefanduseMemofrom React, following best practices for tree-shaking.src/components/markdown.tsx (3)
4-10: LGTM!The typed
Componentsimport andMarkdownPropsinterface provide proper TypeScript safety while maintaining flexibility for component overrides.
18-26: LGTM!The
defaultComponentsprovide sensible styling defaults while remaining overridable through thecustomComponentsprop. The component spread pattern on line 31 correctly prioritizes custom components.
34-36: LGTM!Correctly wraps
childrenwithinReactMarkdownelement 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
parseEventFieldshelper safely handles both string and parsed JSON fields, preventing parse errors.
19-53: LGTM!Connection is properly acquired and released in the
finallyblock, 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 unitsIs 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 multiplyingIf 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.
| import {format, formatInTimeZone} from 'date-fns-tz' | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| const mdxComponentsWithContext = useMemo( | ||
| () => ({ | ||
| ...workshopMDXComponents, | ||
| // Pass context props to Hero component | ||
| Hero: (props: any) => ( | ||
| <Hero | ||
| {...props} | ||
| formRef={formRef} | ||
| saleisActive={saleIsActive} | ||
| event={event} | ||
| /> | ||
| ), | ||
| }), | ||
| [formRef, saleIsActive, event], | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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').
| const paymentLink = `${event.fields.stripePaymentLink || ''}${ | ||
| couponToApply.queryParam | ||
| }` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| import { | ||
| fadeInUp, | ||
| staggerContainer, | ||
| staggerItem, | ||
| } from '../../../app/workshops/[slug]/_components/animations' |
There was a problem hiding this comment.
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.tsto 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
_componentsfolder 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.
| 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() | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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`, | ||
| ) |
There was a problem hiding this comment.
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:
- Document why these filters are commented out (e.g., "TODO: Enable after migration")
- Add them to a configuration/feature flag
- 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.tsLength 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.tsLength 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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -C3Length 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.
| 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 ?? '', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
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
Improvements
Documentation
Chores