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 initpnpm dlx shadcn@latest add @ilinxa/registration-cardAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/registration-card-fixturesCLI can't resolve @ilinxa? The namespace is listed in the official shadcn registry directory, so current CLIs need no configuration. If yours can't resolve it (older or pinned versions, self-hosted mirrors), register it manually in components.json:
"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}Preview
Kayıt Durumu
Demo source
"use client"; import { Bookmark, CalendarPlus, Share2 } from "lucide-react";import { Button } from "@/components/ui/button";import { Tabs, TabsContent, TabsTrigger } from "@/components/ui/tabs";import { SwipeTabsList } from "@/components/site/swipe-tabs-list";import { RegistrationCard } from "./registration-card";import { dummyRegistrationClosed, dummyRegistrationFull, dummyRegistrationLastSpots, dummyRegistrationOpen, dummyRegistrationUrgent, fundraisingLabels, trLabels,} from "./dummy-data"; const noop = () => {}; export default function RegistrationCardDemo() { return ( <Tabs defaultValue="default" className="w-full"> <SwipeTabsList> <TabsTrigger value="default">Default (TR)</TabsTrigger> <TabsTrigger value="states">All states</TabsTrigger> <TabsTrigger value="no-quota">No-quota</TabsTrigger> <TabsTrigger value="actions">Custom actions</TabsTrigger> <TabsTrigger value="bare">Bare + relabeled</TabsTrigger> </SwipeTabsList> {/* 1. Default (TR) — kasder Kayıt Durumu verbatim */} <TabsContent value="default" className="mt-6"> <div className="max-w-sm mx-auto"> <RegistrationCard heading="Kayıt Durumu" capacity={dummyRegistrationLastSpots.capacity} registered={dummyRegistrationLastSpots.registered} onRegister={noop} onShare={noop} labels={trLabels} /> </div> </TabsContent> {/* 2. All states — open / lastSpots / urgent / full / closed stacked */} <TabsContent value="states" className="mt-6"> <div className="max-w-sm mx-auto space-y-6"> <RegistrationCard heading="Open (142/500)" capacity={dummyRegistrationOpen.capacity} registered={dummyRegistrationOpen.registered} onRegister={noop} onShare={noop} /> <RegistrationCard heading="Last spots (423/500 — ≥80% full)" capacity={dummyRegistrationLastSpots.capacity} registered={dummyRegistrationLastSpots.registered} onRegister={noop} onShare={noop} /> <RegistrationCard heading="Urgent (95/100 — ≤10 spots → destructive color)" capacity={dummyRegistrationUrgent.capacity} registered={dummyRegistrationUrgent.registered} onRegister={noop} onShare={noop} /> <RegistrationCard heading="Full (50/50 — sold out)" capacity={dummyRegistrationFull.capacity} registered={dummyRegistrationFull.registered} onRegister={noop} onShare={noop} /> <RegistrationCard heading="Closed (host-driven — e.g. event expired)" capacity={dummyRegistrationClosed.capacity} registered={dummyRegistrationClosed.registered} closed={dummyRegistrationClosed.closed} onRegister={noop} onShare={noop} /> </div> </TabsContent> {/* 3. No-quota — capacity-less, just CTA + share */} <TabsContent value="no-quota" className="mt-6"> <div className="max-w-sm mx-auto space-y-2"> <p className="text-sm text-muted-foreground"> No <code>capacity</code> / <code>registered</code> — bar + counter rows hide; CTA + share remain. Useful for unlimited / drop-in events. </p> <RegistrationCard heading="Sign up" onRegister={noop} onShare={noop} /> </div> </TabsContent> {/* 4. Custom actions — replaces default share with cluster */} <TabsContent value="actions" className="mt-6"> <div className="max-w-sm mx-auto space-y-2"> <p className="text-sm text-muted-foreground"> <code>actions</code> slot — fully replaces the default share button. Drop in calendar + save + share cluster, or compose with{" "} <code>share-bar</code>. </p> <RegistrationCard heading="Registration" capacity={200} registered={172} onRegister={noop} actions={ <div className="grid grid-cols-3 gap-2"> <Button type="button" variant="outline" size="sm" className="gap-1" onClick={noop} > <CalendarPlus aria-hidden="true" className="size-4" /> Cal </Button> <Button type="button" variant="outline" size="sm" className="gap-1" onClick={noop} > <Bookmark aria-hidden="true" className="size-4" /> Save </Button> <Button type="button" variant="outline" size="sm" className="gap-1" onClick={noop} > <Share2 aria-hidden="true" className="size-4" /> Share </Button> </div> } /> </div> </TabsContent> {/* 5. Bare + relabeled — fundraising goal */} <TabsContent value="bare" className="mt-6"> <div className="max-w-sm mx-auto rounded-2xl border border-border/50 bg-card p-6 space-y-2"> <p className="text-sm text-muted-foreground"> <code>framed={false}</code> embedded inside an outer card + relabeled for a fundraising-goal context. </p> <RegistrationCard heading="Fundraising Goal" capacity={50000} registered={32000} framed={false} onRegister={noop} onShare={noop} labels={fundraisingLabels} /> </div> </TabsContent> </Tabs> );} 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)