Skip to content
ilinxa/pro-ui

Team Challenge

alphav0.2.1

Cooperative team challenge card — one shared goal, collective progress, a whole-team reward, and penalty-free opt-in.

Category: GamificationUpdated: 2026-08-11Created: 2026-07-01Author: ilinxa

Context

The Relatedness surface of the gamification pack (E3). Renders one Challenge for one Team — label + member stack, a collective progress meter (current / target, never per-member), a whole-team reward chip (system D-08), and a team-level opt-in control where opted-out is a first-class, neutral, joinable state (the CRITICAL never-forced rule made literal: joining is a prominent invite, leaving is one click with no confirm/guilt, and a visible no-penalty hint states refusing is cost-free). On done, a lightweight inline earned acknowledgement (a 'Completed together' pill, non-blocking, no modal) — the heavy celebration overlay belongs to team-feedback-loop (E6), composed alongside; neither triggers the other. Opt-in is controlled-only; omitting onOptInChange collapses to a read-only card. Ships as a light shadcn-style compound — headless TeamChallengeRoot (controlled echo + telemetry chokepoint) + flat parts (Header, Progress, Reward, OptIn) + context-free primitives (ChallengeProgressMeter, ChallengeRewardChip, OptInToggle, TeamMemberStack, Skeleton) + the TeamChallenge assembly — so a card-body-only or opt-in-control-only subset falls out. Emits challenge.opened (first mount, double-emit-guarded) + challenge.opt-in via onEvent. Portable: zero next/*, own types.ts slice, no other registry import; SSR-safe. Third component of the gamification-system.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/team-challenge

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/team-challenge-fixtures

Preview

Interactive — join / leave (penalty-free)

Controlled opt-in: joining is a prominent invite, leaving is one click with no guilt. Emits challenge.opened + challenge.opt-in.

Optional

All 5 members commit a task this morning

Team Aurora

2 / 5
The team earns: A shared afternoon off

Optional — no penalty for sitting this one out

Joinable (opted-out)

A neutral, first-class invitation — the goal + reward shown as available if you join, never greyed-as-failure.

Optional

All 5 members commit a task this morning

Team Aurora

2 / 5
The team earns: A shared afternoon off

Optional — no penalty for sitting this one out

Active + Completed

Opted-in and in motion → whole-team earned treatment on done (lightweight inline ack, no modal).

Active

Close every stale review by Friday

Team Aurora

3 / 5
The team earns: Team lunch on the house

Optional — no penalty for sitting this one out

Completed together

Ship the onboarding flow together

Team Aurora

5 / 5
The team earned: Team lunch on the house

Optional — no penalty for sitting this one out

Read-only (omit onOptInChange)

Capability-gating — with no handler the opt-in control hides; a pure progress + reward card falls out for free.

Active

Close every stale review by Friday

Team Aurora

3 / 5
The team earns: Team lunch on the house

Composed / lighter (bare parts, no card chrome)

Just the penalty-free toggle + the avatar pile — the compound tree-shakes to a subset (no Root, no card).

Optional — no penalty for sitting this one out

Team sizes

Single-member (no +N) ↔ overflowing stack (+N chip).

Active

Close every stale review by Friday

Team Nimbus

3 / 5
The team earns: Team lunch on the house

Optional — no penalty for sitting this one out

Optional

All 5 members commit a task this morning

Team Meridian

2 / 5
The team earns: A shared afternoon off

Optional — no penalty for sitting this one out

Edge cases

No reward (chip hides) · target 0 (0 / 0, never NaN) · long label (truncates) · loading skeleton.

Active

Pair on one tricky bug this week

Team Aurora

1 / 4

Optional — no penalty for sitting this one out

Optional

Warm-up challenge (no target set yet)

Team Aurora

0 / 0

Optional — no penalty for sitting this one out

Optional

Every member records at least one asynchronous stand-up update before noon so nobody is blocked over the long weekend

Team Meridian

4 / 7
The team earns: The team picks the next sprint's focus together

Optional — no penalty for sitting this one out

Demo source

demo.tsxtsx

Usage

When to use

Reach for TeamChallenge when a team app wants a safe-by-design cooperative challenge — one shared goal, collective progress, and a whole-team reward, with a penalty-free opt-in. It is the Relatedness surface of the gamification pack. The single rule it exists to protect: never-forced — opting in is a deliberate team choice, refusing is cost-free and neutral, and the reward belongs to the whole team equally, never an individual.

Basic example

import { TeamChallenge } from "@/components/team-challenge"

export function TeamChallengeZone({ challenge, team }) {
  return (
    <TeamChallenge
      challenge={challenge}   // { label, optedIn, progress, reward, done }
      team={team}             // { name, members }
      onOptInChange={(optedIn) => persistOptIn(team.id, challenge.id, optedIn)}
      onEvent={(e) => analytics.track(e.type, e)} // host adds the envelope
    />
  )
}

Controlled opt-in (never forced)

challenge.optedIn is the value — the host owns it. Joining is a prominent invite; leaving is one click, no confirm, no guilt. Omit onOptInChange and the control hides entirely — a read-only progress + reward card falls out for free.

Compose a lighter version

It ships as a light shadcn-style compound. Drop the card chrome and keep only the penalty-free toggle (or the bare avatar pile):

import { OptInToggle, TeamMemberStack } from "@/components/team-challenge"

// A context-free toggle, droppable on any surface:
<OptInToggle optedIn={challenge.optedIn} onOptInChange={join} />

// …or just the identity pile:
<TeamMemberStack members={team.members} />

Notes

  • Collective progress only. One bar + current / target — never a per-member breakdown or ranking. The avatar stack is identity, not a progress display.
  • Whole-team reward. The reward reads as the team earns / earned — never first-person, never per-member.
  • Done is a lightweight inline ack. A Completed together pill (non-blocking, no modal). The heavy celebration overlay is team-feedback-loop (E6) — compose it alongside; neither triggers the other.
  • Telemetry. onEvent emits challenge.opened once on first mount (StrictMode-safe) and challenge.opt-in on toggle. No SDK, no next/*.
  • Portable. Own types.ts slice, no other registry import — only the shadcn progress, avatar, button, badge, and skeleton primitives.

Features

  • One shared team goal — label + member stack, collective progress (current / target), whole-team reward
  • Never-forced opt-in: opted-out is a neutral, first-class, cost-free invitation — never greyed-as-failure
  • Penalty-free leave: one click, no confirm dialog, no guilt copy; a visible no-penalty hint in both states
  • Collective progress only — never a per-member breakdown or ranking; the avatar stack is identity, not progress
  • Whole-team reward framing (the team earns / earned) — never first-person, never per-member
  • Done = a lightweight inline 'Completed together' ack (non-blocking, no modal); heavy celebration deferred to E6
  • challenge.opened + challenge.opt-in telemetry via onEvent — no SDK, no next/*
  • Light compound: headless Root + flat parts + context-free primitives; drop to a card-body-only or toggle-only subset

Tags

team-challengegamificationchallengerelatednessopt-inteamcooperativetelemetry

Dependencies

shadcn primitives: progress, avatar, button, badge, skeleton
npm peer deps: lucide-react@^1.11.0