All Sessions

ActivityStage: Sequential Activity Progression

January 25, 20261:27 PM3:09 PM(1h 41m)11 turns
Model: claude-opus-4-5-20251101
Edit33Read26Bash24TodoWrite14Write11Grep4Glob1AskUserQuestion1ExitPlanMode1
1:27 PM

Implement the following plan:

ActivityStage: Sequential Activity Progression

Overview

Add <ActivityStage> MDX component for guiding students through sequential activities during class meetings. Supports both individual student progression and instructor-controlled class-wide advancement.

Usage Example

<ActivityStages meetingSlug="2026-01-27-ethics">

<ActivityStage label="Opening Discussion" time="5m">
Discuss with your neighbor: What ethical concerns do you have about AI?
</ActivityStage>

<ActivityStage label="Group Formation" time="3m" autoAdvance>
<GroupActivity id="ethics-groups" group="ethics-meeting-1">
You're now in your group! Proceed to the case study.
</GroupActivity>
</ActivityStage>

<ActivityStage label="Case Study" time="15m">
Review and discuss the case study...
</ActivityStage>

</ActivityStages>

The "Donut" Pattern

Interactive MDX components use a layered server/client architecture:

Server Component (outer)     ← Fetches data, injects props
  └── Client Component       ← Provides React context
        └── Server Component ← Can still fetch data (passed as children)
              └── Client Component ← Consumes context

Key principle: Children passed to a client component from a server component remain server-rendered. Only imported components become client components.

// ActivityStages.tsx (SERVER) - fetches session, injects index props
export async function ActivityStages({ children, meetingSlug }) {
  const session = await fetchSession(meetingSlug);
  const indexed = React.Children.map(children, (child, i) =>
    React.cloneElement(child, { index: i })
  );
  return (
    <ActivityStagesClient initialSession={session}>
      {indexed}
    </ActivityStagesClient>
  );
}

// ActivityStage.tsx (SERVER) - receives index as prop, can fetch stage data
export async function ActivityStage({ children, index, label }) {
  const stageData = await fetchStageData(index);
  return (
    <ActivityStageClient index={index} initialData={stageData}>
      {children}  {/* MDX content - still server rendered */}
    </ActivityStageClient>
  );
}

// ActivityStageClient.tsx (CLIENT) - consumes context
"use client";
export function ActivityStageClient({ children, index, initialData }) {
  const { currentIndex } = useActivityStages();  // Context works here
  return <div>{children}</div>;
}

Core Features

Student View

  • See one stage at a time (or all unlocked stages)
  • Complete stages via content triggers (e.g., GroupActivity completion) or "Mark Complete" button
  • autoAdvance stages automatically show next stage on completion
  • Non-autoAdvance stages show "Waiting for instructor..." after completion

Instructor View

  • See ALL stages simultaneously with visibility badges
  • Per-stage controls: "Advance All" button to unlock stage for entire class
  • Count-up timer showing elapsed time vs budgeted time
  • Completion tracker: "15/32 students completed"
  • Global controls: Reset session, jump to specific stage

Files to Create

FileTypePurpose
packages/db/src/schema/activity-stages.tsSchemaDatabase tables
apps/api/src/routes/activity-stages.tsAPIStudent endpoints
apps/api/src/routes/admin/activity-stages.tsAPIInstructor endpoints
apps/web/components/mdx/activity-stages.tsxServerContainer component
apps/web/components/mdx/activity-stage.tsxServerIndividual stage wrapper
apps/web/components/mdx/activity-stage-client.tsxClientInteractive stage UI
apps/web/components/mdx/activity-stage-context.tsxClientReact context provider
apps/web/components/mdx/activity-stage-instructor.tsxClientInstructor controls
testing/e2e/tests/activity-stage.spec.tsTestE2E tests

Files to Modify

FileChanges
CLAUDE.mdDocument the donut pattern for MDX components
apps/web/mdx-components.tsxRegister ActivityStages, ActivityStage
apps/web/components/mdx/group-activity-client.tsxAdd optional stage completion hook
packages/db/src/schema/index.tsExport new schema
apps/api/src/index.tsRegister new routes

Database Schema

// activity_sessions - tracks class-wide session state
export const activitySessions = pgTable("activity_sessions", {
  id: text("id").primaryKey(),
  meetingSlug: text("meeting_slug").notNull(),
  semester: text("semester").notNull(),
  currentStageIndex: integer("current_stage_index").notNull().default(0),
  totalStages: integer("total_stages").notNull(),
  startedAt: timestamp("started_at", { mode: "date" }),
  createdAt: timestamp("created_at", { mode: "date" }).notNull().defaultNow(),
}, (t) => [
  unique().on(t.meetingSlug, t.semester),
]);

// activity_stage_timings - timing data per stage
export const activityStageTimings = pgTable("activity_stage_timings", {
  id: text("id").primaryKey(),
  sessionId: text("session_id").notNull().references(() => activitySessions.id),
  stageIndex: integer("stage_index").notNull(),
  label: text("label").notNull(),
  budgetedSeconds: integer("budgeted_seconds"),
  startedAt: timestamp("started_at", { mode: "date" }),
}, (t) => [
  unique().on(t.sessionId, t.stageIndex),
]);

// activity_stage_completions - individual student completions
export const activityStageCompletions = pgTable("activity_stage_completions", {
  id: text("id").primaryKey(),
  sessionId: text("session_id").notNull().references(() => activitySessions.id),
  stageIndex: integer("stage_index").notNull(),
  userId: text("user_id").notNull(),
  completedAt: timestamp("completed_at", { mode: "date" }).notNull().defaultNow(),
}, (t) => [
  unique().on(t.sessionId, t.stageIndex, t.userId),
]);

API Endpoints

Student Endpoints (/api/activity-stages/:meetingSlug)

MethodPathPurpose
GET/Get session status, stages, user completions
POST/complete/:stageIndexMark stage complete for current user

Instructor Endpoints (/api/admin/activity-stages/:meetingSlug)

MethodPathPurpose
POST/startInitialize session with stage metadata
POST/advanceAdvance class to next/specific stage
GET/statusGet full status with completion counts
POST/resetReset session

Component Props

interface ActivityStagesProps {
  children: React.ReactNode;
  meetingSlug: string;
}

interface ActivityStageProps {
  children: React.ReactNode;
  label: string;
  time?: string;         // "5m", "10m", "1h30m"
  autoAdvance?: boolean; // Auto-show next stage on completion
}

Implementation Order

  1. Document donut pattern in CLAUDE.md
  2. Database schema - Create tables, run bun run db:push
  3. API routes - Student endpoints, then admin endpoints
  4. Context provider - State management with SWR
  5. Client components - Stage display and instructor controls
  6. Server components - Container and MDX wrappers
  7. MDX registration - Add to mdx-components.tsx
  8. GroupActivity integration - Add optional completion hook
  9. Test page - Create /testing/components/activity-stages
  10. E2E tests - Test student and instructor flows

Verification

  1. Run bun run dev
  2. Create test page at apps/web/app/testing/components/activity-stages/page.mdx
  3. Test as student:
    • See only first stage initially
    • Complete stage, verify autoAdvance vs waiting behavior
  4. Test as instructor:
    • See all stages with timers and completion counts
    • Advance to next stage, verify students see new content
  5. Test GroupActivity nested inside stage triggers completion
  6. Run CI=true bun run test:e2e - all tests pass

If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: ~/.claude/projects/-Users-challen-www-usingandunderstanding-ai/ac4a9609-8149-46ba-9140-4093bd53a17a.jsonl

I'll implement the ActivityStage sequential activity progression system. Let me start by creating a todo list to track the implementation.

Let me first read the relevant existing files to understand the patterns used.

Let me also read the group-activity server component and an existing API route to understand the patterns better.

Now I understand the patterns. Let me start implementing. I'll begin with documenting the donut pattern in CLAUDE.md.

Now let me create the database schema for activity stages.

Now let me add the export to the schema index.

Let me check if the dev server is running, then push the schema changes.

Let me read the auth middleware to understand how to use it properly.

Now let me create the student API routes for activity stages.

Now let me register these routes in the API index.ts.

Let me read the SWR fetcher and env patterns used in the web app.

Now let me create the context provider for activity stages.

Let me check how server-side API fetching works.

Now let me create the server components. First the container component.

Now let me create the ActivityStage server component.

Now let me add the GroupActivity integration. I'll create a hook that GroupActivityClient can use to notify when a group completes.

Now let me update GroupActivityClient to trigger stage completion when the group becomes complete.

Let me check the testing directory structure.

Let me run the type checker to make sure everything compiles.

There's a TypeScript error. Let me fix it.

Let me fix these TypeScript errors.

TypeScript checks pass. Let me also run the linter.

Let me fix the lint errors.

I see that activityGroups is unused but also I should add cleanup for the new activity stages tables. Let me update the e2e routes.

Let me read an existing E2E test to understand the pattern.

Let me run the E2E tests.

The test is failing because the session from the previous test persists. Let me update the tests to reset the session at the beginning.

The test is failing because the session persists from previous tests. Let me update the test to handle this more gracefully.

Let me look at the reset function more carefully and fix it.

All tests pass. Let me run the full E2E test suite to make sure nothing is broken.

Edit33Read25Bash17TodoWrite13Write10Glob1Grep1
1:43 PM

Sorry, continue.

Let me update the todo list and provide a summary of the implementation.

The ActivityStage implementation is complete. Here's a summary of what was created:

Files Created

Database Schema:

  • packages/db/src/schema/activity-stages.ts - Three tables: activity_sessions, activity_stage_timings, activity_stage_completions

API Routes:

  • apps/api/src/routes/activity-stages.ts - Student endpoints for getting session status and completing stages
  • apps/api/src/routes/admin/activity-stages.ts - Instructor endpoints for starting, advancing, resetting sessions and viewing completion counts

React Components:

  • apps/web/components/mdx/activity-stages.tsx - Server component container that fetches session data
  • apps/web/components/mdx/activity-stage.tsx - Server component wrapper for individual stages
  • apps/web/components/mdx/activity-stage-client.tsx - Client component for stage UI and completion
  • apps/web/components/mdx/activity-stage-context.tsx - React context provider with SWR for state management
  • apps/web/components/mdx/activity-stage-instructor.tsx - Instructor control panel with timers and completion tracking

Test Page:

  • apps/web/app/testing/components/activity-stages/page.tsx - Test page at /testing/components/activity-stages

E2E Tests:

  • testing/e2e/tests/activity-stages.spec.ts - 15 tests covering student and instructor flows

Files Modified

  • CLAUDE.md - Added documentation for the "donut" pattern
  • packages/db/src/schema/index.ts - Added export for activity stages schema
  • apps/api/src/index.ts - Registered new routes
  • apps/web/mdx-components.tsx - Registered ActivityStages and ActivityStage components
  • apps/web/components/mdx/group-activity-client.tsx - Added integration to trigger stage completion when group forms
  • apps/api/src/routes/e2e.ts - Added cleanup for activityStageCompletions
  • testing/e2e/playwright.config.ts - Added activity-stages test project

Usage Example

<ActivityStages meetingSlug="2026-01-27-ethics">
  <ActivityStage label="Opening Discussion" time="5m">
    Discuss with your neighbor...
  </ActivityStage>
  
  <ActivityStage label="Group Formation" time="3m" autoAdvance>
    <GroupActivity id="ethics-groups" group="ethics-meeting-1">
      Partner content here...
    </GroupActivity>
  </ActivityStage>
  
  <ActivityStage label="Case Study" time="15m">
    Review and discuss...
  </ActivityStage>
</ActivityStages>
TodoWrite1
2:12 PM

Your task

First, run git status, git diff, and git log --oneline -10 to understand the current state.

Then add and commit all modified files with a descriptive message. Break the changes into multiple commits if appropriate. If there are untracked files, ask if they should be added or added to the appropriate .gitignore file. The user may also want to take over at this point, so provide an abort option as well. Always check changes for unnecessary debugging statements or logging that might have been accidentally included. Prompt the user if you find any, and offer to remove each one by one. Do not use interactive git staging (git add -p) as it does not work.

<thinking> Now let me check the new files for any debugging statements or unnecessary logging before committing. </thinking>

Let me check the new and modified files for any debugging statements or unnecessary logging.

<thinking> I found two console.error statements: 1. `apps/web/components/mdx/group-activity.tsx:68` - This is a pre-existing one (not in my changes) 2. `apps/web/components/mdx/activity-stages.tsx:118` - This is in my new file

The one in activity-stages.tsx is similar to the existing one in group-activity.tsx - both are server-side error logging for API fetch failures, which is appropriate for debugging server-side issues. Since the existing codebase has this pattern, it's consistent.

Let me confirm there are no other debugging statements in my new files and then proceed with the commit. </thinking>

The console.error in activity-stages.tsx:118 is for server-side error logging, matching the existing pattern in group-activity.tsx:68. This is appropriate for debugging server-side fetch failures.

Here's a summary of changes:

Modified Files (7):

  • CLAUDE.md - Added documentation for the donut pattern
  • apps/api/src/index.ts - Registered new API routes
  • apps/api/src/routes/e2e.ts - Added cleanup for activity stage completions
  • apps/web/components/mdx/group-activity-client.tsx - Added activity stage integration
  • apps/web/mdx-components.tsx - Registered ActivityStages and ActivityStage components
  • packages/db/src/schema/index.ts - Added export for activity stages schema
  • testing/e2e/playwright.config.ts - Added activity-stages test project

Untracked Files (10 new files):

  • packages/db/src/schema/activity-stages.ts - Database schema
  • apps/api/src/routes/activity-stages.ts - Student API routes
  • apps/api/src/routes/admin/activity-stages.ts - Admin API routes
  • apps/web/components/mdx/activity-stage.tsx - Server component
  • apps/web/components/mdx/activity-stages.tsx - Container server component
  • apps/web/components/mdx/activity-stage-client.tsx - Client component
  • apps/web/components/mdx/activity-stage-context.tsx - React context provider
  • apps/web/components/mdx/activity-stage-instructor.tsx - Instructor controls
  • apps/web/app/testing/components/activity-stages/page.tsx - Test page
  • testing/e2e/tests/activity-stages.spec.ts - E2E tests

Committed successfully: b1b4761 - Add ActivityStages MDX component for sequential activity progression

17 files changed with 1958 insertions. The working tree is clean.

Bash7Grep3AskUserQuestion1
2:49 PM

OK! Next, I'd like to be able to compose the group and activity stages around the idea of roles. For example, a group might establish two roles: driver and observer. Then, for each phase inside that group, we might have students switch roles. We might want to only render the stage content if the student is a driver for that stage, and instruct the observer to observe. We'd have roles rotate within the group at each stage. Does that make sense? Can you provide an example of how the markup for that might look?

Let me first read the existing plan file and then understand the current implementation to propose a good design for roles.

This is a great idea! Let me think through how the markup might look. Here are a few approaches to consider:


Option A: Roles on GroupActivity, rotation on stages

<ActivityStages meetingSlug="2026-01-27-pair-programming">

<ActivityStage label="Find a Partner" autoAdvance>
  <GroupActivity id="pp-groups" group="pair-programming-1" roles={["driver", "observer"]}>
    You found a partner!
  </GroupActivity>
</ActivityStage>

<ActivityStage label="Exercise 1" time="10m">
  <RoleContent role="driver">
    **You are the driver.** Open the editor and implement the `factorial` function.
  </RoleContent>
  <RoleContent role="observer">
    **You are the observer.** Watch your partner code and offer suggestions.
  </RoleContent>
</ActivityStage>

<ActivityStage label="Exercise 2" time="10m" rotateRoles>
  <!-- rotateRoles swaps driver/observer from previous stage -->
  <RoleContent role="driver">
    **Your turn to drive!** Now implement the `fibonacci` function.
  </RoleContent>
  <RoleContent role="observer">
    **Now you observe.** Help your partner think through the approach.
  </RoleContent>
</ActivityStage>

</ActivityStages>

Option B: Explicit role assignment per stage

<ActivityStages meetingSlug="2026-01-27-pair-programming">

<ActivityStage label="Find a Partner" autoAdvance>
  <GroupActivity id="pp-groups" group="pair-programming-1" roles={["driver", "observer"]}>
    You found a partner!
  </GroupActivity>
</ActivityStage>

<ActivityStage label="Exercise 1" time="10m" roles={{ driver: 0, observer: 1 }}>
  <!-- Member 0 = driver, Member 1 = observer -->
  <RoleContent role="driver">...</RoleContent>
  <RoleContent role="observer">...</RoleContent>
</ActivityStage>

<ActivityStage label="Exercise 2" time="10m" roles={{ driver: 1, observer: 0 }}>
  <!-- Swapped: Member 1 = driver, Member 0 = observer -->
  <RoleContent role="driver">...</RoleContent>
  <RoleContent role="observer">...</RoleContent>
</ActivityStage>

</ActivityStages>

Key design questions:

  1. Role assignment strategy - When a group forms, how are roles initially assigned?

    • First to join = first role (driver), second = second role (observer)?
    • Random assignment?
    • Student chooses?
  2. Rotation behavior - How should rotateRoles work?

    • Simple swap for pairs (driver↔observer)?
    • Round-robin for larger groups?
  3. Content visibility - What does the other role see?

    • Nothing (content hidden)?
    • Their own role-specific instructions?
    • Both, but their role highlighted?

Which approach feels more natural for your use case? And any preferences on the design questions above?

Read1
2:53 PM

Wait, I think that the GroupActivity should have the activity stages inside it, no? That way that can obtain the GroupActivity state from their parent via context.

You're right! That's a much cleaner design. The GroupActivity provides the group context (members, roles), and the ActivityStages operate within that context:

<GroupActivity id="pp-groups" group="pair-programming-1" roles={["driver", "observer"]}>

  <!-- This content only shows after group forms -->
  <ActivityStages meetingSlug="2026-01-27-pair-programming">
    
    <ActivityStage label="Exercise 1" time="10m">
      <RoleContent role="driver">
        **You are the driver.** Open the editor and implement `factorial`.
      </RoleContent>
      <RoleContent role="observer">
        **You are the observer.** Watch and offer suggestions.
      </RoleContent>
    </ActivityStage>

    <ActivityStage label="Exercise 2" time="10m" rotateRoles>
      <RoleContent role="driver">
        **Your turn to drive!** Now implement `fibonacci`.
      </RoleContent>
      <RoleContent role="observer">
        **Now you observe.** Help your partner think through it.
      </RoleContent>
    </ActivityStage>

  </ActivityStages>

</GroupActivity>

This way:

  • GroupActivity forms the group, assigns initial roles, provides context
  • ActivityStages (children) only render after group is complete
  • ActivityStage can read group context and optionally rotateRoles
  • RoleContent checks the user's current role from context and conditionally renders

The context chain would be: GroupActivityActivityStagesActivityStageRoleContent

Does this structure match what you had in mind?

2:55 PM

I think that makes more sense. What do you think?

I agree, this design is better for several reasons:

  1. Natural data flow - Group context (members, roles) flows down to stages and role content. No need to wire things up manually.

  2. Automatic gating - GroupActivity already hides children until the group forms. No changes needed there.

  3. Compositional - Each component does one thing:

    • GroupActivity - forms group, assigns roles, provides context
    • ActivityStages - manages stage progression (works with or without group)
    • RoleContent - conditionally renders based on role context
  4. Clean mental model - "Within this group, do these stages, with these role-specific instructions"

  5. Instructor view - Instructors could see all role content labeled, so they understand what each student sees

The only thing I'd add: RoleContent should probably still render its content (maybe dimmed or labeled) if there's no group context, so the MDX is previewable and instructors can see everything.

Want me to plan the implementation? The main pieces would be:

  • Add roles prop to GroupActivity and store role assignments in the database
  • Create a group context that exposes current user's role
  • Add rotateRoles prop to ActivityStage that cycles role assignments
  • Create RoleContent component that conditionally renders based on role
2:59 PM

Well RoleContent is inside GroupActivity, which won't render children until the group forms. But this is fine. We'll need different visibility for instructors anyway, and we can work on that later. From the previous example, I liked the explicit rotateRoles on the activityStage, although that should only work if it's in a GroupContext. I guess one thing to keep in mind is that stages should work without the group wrapper. Actually, can we be more clear about the composition rules here for both activity and group components?

Great question. Let me lay out the composition rules clearly:


Composition Rules

ActivityStages (standalone)

<ActivityStages meetingSlug="...">
  <ActivityStage label="Step 1">...</ActivityStage>
  <ActivityStage label="Step 2">...</ActivityStage>
</ActivityStages>
  • Class-wide stage progression
  • Instructor controls advancement
  • No roles, no rotateRoles, no RoleContent

GroupActivity (standalone)

<GroupActivity id="..." group="...">
  Content shown after group forms
</GroupActivity>
  • Group formation only
  • No roles (or roles could be assigned but never used)
  • No stages, no rotation

GroupActivity wrapping ActivityStages

<GroupActivity id="..." group="..." roles={["driver", "observer"]}>
  <ActivityStages meetingSlug="...">
    <ActivityStage label="Exercise 1">
      <RoleContent role="driver">...</RoleContent>
      <RoleContent role="observer">...</RoleContent>
    </ActivityStage>
    <ActivityStage label="Exercise 2" rotateRoles>
      <RoleContent role="driver">...</RoleContent>
    </ActivityStage>
  </ActivityStages>
</GroupActivity>
  • Full feature set: group + roles + stages + rotation
  • rotateRoles cycles role assignments for that stage
  • RoleContent renders based on user's current role

ActivityStages wrapping GroupActivity (existing pattern)

<ActivityStages meetingSlug="...">
  <ActivityStage label="Find Partner" autoAdvance>
    <GroupActivity id="..." group="...">
      Partner found!
    </GroupActivity>
  </ActivityStage>
  <ActivityStage label="Discussion">
    <!-- No group context here - GroupActivity ended -->
  </ActivityStage>
</ActivityStages>
  • Stages control class progression
  • Group forms within a stage but context doesn't persist to other stages
  • No roles - use the other pattern for roles

Invalid/Warning cases

  • rotateRoles on ActivityStage without GroupActivity ancestor → ignored (or console warning)
  • RoleContent without GroupActivity ancestor → render nothing? render all? warning?

Does this capture the composition rules correctly? And for RoleContent outside a group context - should it render nothing, render its content anyway (for testing/previewing), or show a warning message?

3:00 PM

I guess I'm more thinking: It looks like ActivityStage has to be in ActivityStages, correct? And RoleContent must be inside GroupActivity, correct?

Yes, exactly. The strict containment rules are:

ComponentMust be inside
ActivityStageActivityStages
RoleContentGroupActivity

And the flexible parts:

ComponentCan be...
ActivityStagesStandalone, OR inside GroupActivity
GroupActivityStandalone, OR inside ActivityStage, OR wrapping ActivityStages

So there are really two valid patterns for roles:

Pattern 1: Group wraps Stages (roles + rotation)

<GroupActivity roles={["driver", "observer"]}>
  <ActivityStages>
    <ActivityStage rotateRoles>
      <RoleContent role="driver">...</RoleContent>
    </ActivityStage>
  </ActivityStages>
</GroupActivity>

Pattern 2: Stage contains Group (no roles outside group)

<ActivityStages>
  <ActivityStage>
    <GroupActivity>
      <!-- RoleContent only valid here, inside GroupActivity -->
    </GroupActivity>
  </ActivityStage>
  <ActivityStage>
    <!-- No RoleContent here - outside GroupActivity -->
  </ActivityStage>
</ActivityStages>

Does that capture the rules correctly?

3:01 PM

Gotcha. There is some crossover here, however, since rotateRoles communicates with GroupActivity, correct? That's fine, just want to be clear.

Right. ActivityStage consumes from two contexts:

  1. ActivityStages context (required) - stage index, visibility, completion state
  2. GroupActivity context (optional) - for rotateRoles to actually rotate roles

So when you have:

<GroupActivity roles={["driver", "observer"]}>
  <ActivityStages>
    <ActivityStage rotateRoles>
      ...
    </ActivityStage>
  </ActivityStages>
</GroupActivity>

The ActivityStage reaches "up" to both:

  • Its parent ActivityStages for stage mechanics
  • Its ancestor GroupActivity for role rotation

And rotateRoles without a GroupActivity ancestor would just be ignored (no roles to rotate).

This cross-context communication is fine - React contexts are designed for this. The ActivityStage client component would do something like:

const stagesContext = useActivityStages(); // required
const groupContext = useOptionalGroupActivity(); // optional

if (rotateRoles && groupContext) {
  // trigger role rotation
}

Is that the interaction model you had in mind?

3:03 PM

Yes! And I like the RoleContent wrapper. That makes things very explicit. And let's make sure that rotateRoles handles cases where the number of students exceeds the number of roles. Actually, this probably requires configuration. Maybe roles={{driver: 1, observer: Inf}} or something like that, to express that there should be only one driver but can be many observers? That should be configured separately from the target group size.

Good catch. The role configuration needs to express cardinality. A few syntax options:

Option A: Object with counts

<GroupActivity 
  size={3} 
  roles={{ driver: 1, observer: "*" }}
>
  • 1 = exactly one
  • "*" = fill remaining slots

Option B: More explicit

<GroupActivity 
  size={3} 
  roles={{ driver: { count: 1 }, observer: { count: "rest" } }}
>

Rotation behavior for 3-person group with 1 driver:

StageAliceBobCarol
1driverobserverobserver
2 (rotate)observerdriverobserver
3 (rotate)observerobserverdriver

The single-count role (driver) rotates through members; the "*" role (observer) fills the rest.


Edge cases to handle:

  • What if driver: 2, observer: "*" and group has only 2 people? Both are drivers, no observers?
  • What if total role counts exceed group size? Error?

I lean toward Option A for simplicity:

<GroupActivity size={3} roles={{ driver: 1, observer: "*" }}>

Does that syntax work for you? And how strict should validation be for mismatched sizes?

3:08 PM

Yeah, I like that.

Let me write up the implementation plan.

I've written up the implementation plan. The key points:

  • Role syntax: roles={{ driver: 1, observer: "*" }}
  • Composition: GroupActivity wraps ActivityStages for full role support
  • Rotation: rotateRoles on ActivityStage cycles the limited-count role through members
  • RoleContent: Simple conditional rendering based on user's current role
  • Database: Store role config on groups, track rotation offset per stage
Write1ExitPlanMode1