Componentnavigation0.93 KB gzipZero dependencies
Shift Planner
Staff shift board with claims.
Preview
Live Preview
Shift planner
- FRIGate 3 - Evening - Ada
- SATGate 3 - Matinee - Row
- SATGate 4 - Evening - Max
- SUNWill call desk - Ada
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add shift-plannerOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { ShiftPlanner } from "@/components/ui/shift-planner";
<ShiftPlanner />Usage guidance
Use Shift Planner when a ticket stub surface needs Staff shift board with claims. Keep props minimal and prefer composition with sibling stubs.
Avoid Shift Planner for long form flows or dense data grids where a dedicated layout block fits better.
Source code
shift-planner.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface ShiftSlot {
id: string;
day: string;
label: string;
staff: string;
}
export interface ShiftPlannerProps extends React.HTMLAttributes<HTMLDivElement> {
slots?: ShiftSlot[];
}
const FALLBACK: ShiftSlot[] = [
{ id: "1", day: "FRI", label: "Gate 3 - Evening", staff: "Ada" },
{ id: "2", day: "SAT", label: "Gate 3 - Matinee", staff: "Row" },
{ id: "3", day: "SAT", label: "Gate 4 - Evening", staff: "Max" },
{ id: "4", day: "SUN", label: "Will call desk", staff: "Ada" },
];
/** Weekly staff shift board with claim buttons. */
export function ShiftPlanner({ slots = FALLBACK, className, ...props }: ShiftPlannerProps) {
const [claimed, setClaimed] = React.useState<string[]>([]);
return (
<div className={cn("w-full rounded-lg border-2 border-foreground bg-card p-4 shadow-md", className)} {...props}>
<p className="border-b border-dashed border-border pb-2 font-mono text-[10px] font-bold uppercase tracking-widest text-muted-foreground">Shift planner</p>
<ul className="mt-2 space-y-2">
{slots.map((s) => {
const done = claimed.includes(s.id);
return (
<li key={s.id} className="flex items-center gap-3 rounded-md border border-dashed border-border bg-secondary/30 px-3 py-2">
<span className="rounded bg-primary px-2 py-1 font-mono text-[11px] font-bold text-primary-foreground">{s.day}</span>
<span className="flex-1 font-mono text-xs text-foreground">{s.label} <span className="text-muted-foreground">- {s.staff}</span></span>
<button
type="button"
onClick={() => setClaimed((c) => (done ? c.filter((x) => x !== s.id) : [...c, s.id]))}
aria-pressed={done}
className={cn("rounded border px-2 py-0.5 font-mono text-[10px] font-bold uppercase focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", done ? "border-foreground bg-accent text-accent-foreground" : "border-border bg-card text-muted-foreground hover:text-foreground")}
>
{done ? "Held" : "Claim"}
</button>
</li>
);
})}
</ul>
</div>
);
}