Registration Card
alphav0.2.0Event registration status card — capacity progress, spots-left counter, status-aware call to action, and a share slot.
Context
Use for event registration sidebars, course / webinar signup cards, training enrollment, conference tickets, beta / waitlist signups, fundraising goal cards. Architecturally independent of event timing — host owns date logic and passes `closed: true` when registration window has ended. Migration origin: kasder kas-social-front-v0 events/[id]/page.tsx Kayıt Durumu sidebar card. Composes naturally with event-card helpers + share-bar in the actions slot.
Installation
pnpm dlx shadcn@latest init"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}pnpm dlx shadcn@latest add @ilinxa/registration-cardAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/registration-card-fixturesPreview
Kayıt Durumu
Demo source
Usage
When to use
Reach for RegistrationCard for any signup / RSVP / ticket / fundraising widget — event registration sidebars, course / webinar enrollment, training signups, conference tickets, beta / waitlist, fundraising goal cards. Capacity progress + spots-left counter + status-aware primary CTA + optional share / actions slot.
Minimal example
import { RegistrationCard } from "@/components/registration-card";
<RegistrationCard
heading="Registration"
capacity={500}
registered={142}
onRegister={() => openRegisterDialog()}
onShare={() => share(eventUrl)}
/>;4-state state machine
Status auto-derives from (capacity, registered, closed):
open— capacity available, less thanlastSpotsRatiofull → primary "Register" CTAlastSpots— capacity available, ≥lastSpotsRatiofull (default 80%) → primary "Register" CTA + spots-left counter flips totext-destructivewhen ≤urgentSpotsCount(default 10)full—registered >= capacity→ disabled secondary "Sold out" CTAclosed— host-driven viaclosed: true→ disabled secondary "Closed" CTA. Wins over capacity (closed always closes regardless of remaining spots).
Public helper kernel — derive state without rendering
import {
RegistrationCard,
deriveRegistrationStatus,
type RegistrationStatus,
} from "@/components/registration-card";
// Header counter: how many events are still open?
const stillOpen = events.filter(
(e) =>
deriveRegistrationStatus({
capacity: e.capacity,
registered: e.registered,
closed: e.regClosed,
}) !== "full" &&
deriveRegistrationStatus({
capacity: e.capacity,
registered: e.registered,
closed: e.regClosed,
}) !== "closed",
).length;Composing with event-card's state
registration-card is architecturally independent of event timing — the host derives event status (using getEventStatus from event-card) and passes closed: true when registration should close. Two common composition patterns:
import { getEventStatus } from "@/components/event-card";
// Pattern 1 — close registration only when event has expired:
<RegistrationCard
capacity={event.capacity}
registered={event.registered}
closed={getEventStatus(event) === "expired"}
onRegister={handleRegister}
/>
// Pattern 2 — close registration when event is expired OR ongoing
// (i.e., no walk-ins after the event starts):
<RegistrationCard
capacity={event.capacity}
registered={event.registered}
closed={["expired", "ongoing"].includes(getEventStatus(event))}
onRegister={handleRegister}
/>No-quota mode
For unlimited / drop-in events, omit capacity and registered — the bar + counter rows hide; status stays open; CTA + share remain interactive.
<RegistrationCard
heading="Sign up"
onRegister={handleRegister}
onShare={handleShare}
/>Custom actions slot
The actions slot fully replaces the default share button. Drop in calendar export + save event + share clusters, or compose with share-bar:
<RegistrationCard
capacity={200}
registered={172}
onRegister={handleRegister}
actions={
<div className="grid grid-cols-3 gap-2">
<Button variant="outline" onClick={addToCalendar}>Cal</Button>
<Button variant="outline" onClick={save}>Save</Button>
<ShareBar url={eventUrl} variant="compact" />
</div>
}
/>Relabeled for fundraising
<RegistrationCard
heading="Fundraising Goal"
capacity={50000}
registered={32000}
onRegister={openDonateDialog}
labels={{
capacityLabel: "Goal",
spotsLeftSuffix: "remaining",
spotsLeftFull: "Goal reached!",
registeredSuffix: "raised",
capacitySuffix: "goal",
ctaRegister: "Donate now",
ctaSoldOut: "Goal reached",
}}
/>Configurable thresholds
lastSpotsRatio(default0.8) — percent of capacity (0–1) at which status flips tolastSpots. Drives the state machine.urgentSpotsCount(default10) — absolute spots-left count at which the counter color flips totext-destructive. Drives the visual color, not the state.
These are semantically distinct: lastSpotsRatio is percent (good for state), urgentSpotsCount is absolute (good for color urgency). For a 100-capacity event, 80% means 20 spots left — not yet urgent. For a 50-capacity event, 80% means 10 spots left — urgent.
Notes
- The primary CTA is a REAL interactive button — fires
onRegister()when clicked. (Differs fromevent-card's grid variant which has a decorative CTA.) - When
onRegisteris absent, the CTA renders disabled withlabels.ctaUnavailable— useful for preview / admin contexts. - The bar uses Radix
Progressprimitive — emitsrole="progressbar"+aria-valuenowautomatically. - Heading defaults to
h3(this is a sidebar card, typically nested under the page's mainh2). statusOverridewins over derived status — useful for preview / what-if states in admin UIs.
Features
- 4-state state machine — open / lastSpots / full / closed
- Public helper kernel — deriveRegistrationStatus pure function
- Capacity progress bar + spots-left counter with destructive-color threshold
- Status-aware primary CTA (Register / Sold out / Closed / Unavailable)
- Default share button when onShare provided
- actions slot fully replaces the default share for richer clusters
- No-quota mode (capacity-less events) hides bar + counter rows
- Configurable thresholds — lastSpotsRatio (state machine) + urgentSpotsCount (counter color)
- statusOverride for preview / what-if states
- Optional heading with configurable level (h2/h3/h4, default h3)
- Frame toggle — framed=true card chrome with shadow-lg / framed=false bare
- i18n via 11-key labels object
- WCAG — Radix Progress role=progressbar + aria-valuenow + section aria-labelledby
- Architecturally INDEPENDENT of event-card's date-driven state machine (host composes)