Week calendar
Hour-by-hour week view with events.
Built with
My week
Meetings, interviews and leave for the week. Select an event for its details.
12 to 18 October 2026
Monday, 12 October 2026
Tuesday, 13 October 2026
Wednesday, 14 October 2026
Thursday, 15 October 2026, today
Friday, 16 October 2026
Saturday, 17 October 2026
Sunday, 18 October 2026
All day
week-calendar.tsx
"use client";
import { useState, type ReactElement } from "react";
import { CalendarDate } from "@internationalized/date";
import { LuChevronLeft, LuChevronRight, LuPlus } from "react-icons/lu";
import {
Button,
CalendarDayHeader,
CalendarEvent,
CalendarMonth,
Checkbox,
DirectionalIcon,
EventPopover,
IconButton,
Input,
Modal,
} from "@nexera-ui/react";
type Kind = "timed" | "tentative" | "focus-time" | "declined" | "out-of-office" | "all-day";
interface WeekEvent {
title: string;
day: number;
from: string;
to: string;
type: Kind;
cal: string;
half?: 0 | 1;
}
const ZONE = "Asia/Karachi";
const TODAY = new CalendarDate(2026, 10, 15);
const FIRST_HOUR = 8;
const HOURS = 11; // 08:00 to 19:00
const ROW = 48; // px per hour
const CALENDARS = ["Meetings", "Interviews", "Team leave", "Focus time"];
/** The week of Monday 12 October 2026. `day` 0 is Monday; `half` splits two overlapping events side by side. */
const EVENTS: WeekEvent[] = [
...[0, 1, 2, 3, 4].map((day) => ({
title: "Daily standup",
day,
from: "09:30",
to: "09:45",
type: "timed" as const,
cal: "Meetings",
})),
{ title: "Payroll review", day: 0, from: "11:00", to: "12:00", type: "timed", cal: "Meetings" },
{
title: "Focus time",
day: 0,
from: "14:00",
to: "16:00",
type: "focus-time",
cal: "Focus time",
},
{
title: "Interview: Mehak Ali",
day: 1,
from: "10:30",
to: "11:15",
type: "tentative",
cal: "Interviews",
},
{
title: "Design review",
day: 1,
from: "14:00",
to: "15:00",
type: "tentative",
cal: "Meetings",
},
{
title: "Lunch with new joiners",
day: 2,
from: "13:00",
to: "14:00",
type: "timed",
cal: "Meetings",
},
{
title: "Quarterly town hall",
day: 2,
from: "15:00",
to: "16:00",
type: "timed",
cal: "Meetings",
},
{
title: "Interview: Zara Siddiqui",
day: 3,
from: "10:30",
to: "11:15",
type: "timed",
cal: "Interviews",
half: 0,
},
{
title: "1:1 Sara and Ayesha",
day: 3,
from: "11:00",
to: "11:30",
type: "timed",
cal: "Meetings",
half: 1,
},
{
title: "Write Q4 plan",
day: 3,
from: "13:30",
to: "15:00",
type: "focus-time",
cal: "Focus time",
},
{ title: "Vendor demo", day: 3, from: "16:00", to: "16:30", type: "declined", cal: "Meetings" },
{ title: "Leadership sync", day: 4, from: "12:00", to: "13:00", type: "timed", cal: "Meetings" },
];
const ALL_DAY = [
{ title: "Hina Raza: first day", day: 2, type: "all-day" as const, cal: "Meetings" },
{ title: "Ayesha Khan: annual leave", day: 4, type: "out-of-office" as const, cal: "Team leave" },
];
const hours = (hm: string) => Number(hm.slice(0, 2)) + Number(hm.slice(3)) / 60;
const instant = (d: CalendarDate, hm: string) => new Date(`${d.toString()}T${hm}:00+05:00`);
const column = (day: number, half?: 0 | 1) =>
half === undefined
? {
left: `calc(3.5rem + (100% - 3.5rem) * ${day} / 7 + 2px)`,
width: `calc((100% - 3.5rem) / 7 - 4px)`,
}
: {
left: `calc(3.5rem + (100% - 3.5rem) * ${day * 2 + half} / 14 + 2px)`,
width: `calc((100% - 3.5rem) / 14 - 4px)`,
};
/** Week calendar: an hour-by-hour week with an all-day row, overlapping events, calendar filters and event details. */
export default function WeekCalendar() {
const [date, setDate] = useState(TODAY);
const [shown, setShown] = useState(CALENDARS);
const [created, setCreated] = useState<string[]>([]);
const [creating, setCreating] = useState(false);
const [newTitle, setNewTitle] = useState("");
const week = date.subtract({ days: (date.toDate("UTC").getUTCDay() + 6) % 7 });
const isSampleWeek = week.toString() === "2026-10-12";
const days = Array.from({ length: 7 }, (_, i) => week.add({ days: i }));
const fmt = (d: CalendarDate, o: Intl.DateTimeFormatOptions) =>
new Intl.DateTimeFormat("en-GB", { ...o, timeZone: "UTC" }).format(d.toDate("UTC"));
const visible = (cal: string) => isSampleWeek && shown.includes(cal);
const details = (title: string, d: CalendarDate, when: string, trigger: ReactElement) => (
<EventPopover
key={`${title}-${d.toString()}`}
title={title}
when={`${fmt(d, { weekday: "long", day: "numeric", month: "long" })} · ${when}`}
meta="Pakistan Standard Time"
placement="bottom start"
>
{trigger}
</EventPopover>
);
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<header className="mb-4 grid gap-1">
<h2 className="text-heading-h3 text-primary">My week</h2>
<p className="text-body-default text-secondary">
Meetings, interviews and leave for the week. Select an event for its details.
</p>
</header>
<div className="flex overflow-hidden rounded-xl border border-default bg-surface">
<aside
aria-label="Calendar sidebar"
className="hidden w-60 shrink-0 flex-col gap-4 border-e border-default p-4 @5xl:flex"
>
<Button
leadingIcon={<LuPlus />}
className="self-start"
onPress={() => {
setCreating(true);
}}
>
Create
</Button>
<CalendarMonth
aria-label="Choose a date"
value={date}
onChange={(v) => {
setDate(new CalendarDate(v.year, v.month, v.day));
}}
className="w-full"
/>
<h3 className="text-body-strong text-primary">My calendars</h3>
{CALENDARS.map((cal) => (
<Checkbox
key={cal}
isSelected={shown.includes(cal)}
onChange={(on) => {
setShown((s) => (on ? [...s, cal] : s.filter((x) => x !== cal)));
}}
>
{cal}
</Checkbox>
))}
</aside>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2 border-b border-default px-4 py-3">
<Button
variant="secondary"
onPress={() => {
setDate(TODAY);
}}
>
Today
</Button>
<IconButton
variant="ghost"
aria-label="Previous week"
icon={
<DirectionalIcon>
<LuChevronLeft />
</DirectionalIcon>
}
onPress={() => {
setDate(date.subtract({ weeks: 1 }));
}}
/>
<IconButton
variant="ghost"
aria-label="Next week"
icon={
<DirectionalIcon>
<LuChevronRight />
</DirectionalIcon>
}
onPress={() => {
setDate(date.add({ weeks: 1 }));
}}
/>
<h3 aria-live="polite" className="flex-1 text-heading-h4 text-primary">
{fmt(week, { day: "numeric" })} to{" "}
{fmt(week.add({ days: 6 }), { day: "numeric", month: "long", year: "numeric" })}
</h3>
<IconButton
className="@5xl:hidden"
aria-label="Create event"
icon={<LuPlus />}
onPress={() => {
setCreating(true);
}}
/>
</div>
<div className="overflow-x-auto">
<div className="min-w-[46rem]">
<div className="flex ps-14">
{days.map((d) => (
<CalendarDayHeader
key={d.toString()}
date={d}
locale="en-GB"
isToday={d.compare(TODAY) === 0}
className="flex-1"
/>
))}
</div>
<div className="relative h-8 border-y border-default">
<span className="absolute start-1 top-2 text-body-micro text-muted">All day</span>
{ALL_DAY.filter((e) => visible(e.cal)).map((e) => {
const d = week.add({ days: e.day });
return details(
e.title,
d,
"All day",
<CalendarEvent
title={e.title}
type={e.type}
start={instant(d, "00:00")}
timeZone={ZONE}
className="absolute top-1"
style={column(e.day)}
/>,
);
})}
</div>
<div className="relative" style={{ height: HOURS * ROW }}>
{Array.from({ length: HOURS }, (_, i) => (
<div
key={i}
aria-hidden="true"
className="absolute inset-x-0 flex items-start"
style={{ top: i * ROW }}
>
<span className="relative -top-2 w-14 text-center text-body-micro text-muted">
{i > 0 ? `${String(FIRST_HOUR + i).padStart(2, "0")}:00` : ""}
</span>
<span className="h-px flex-1 bg-border-subtle" />
</div>
))}
{days.map((d, i) => (
<div
key={d.toString()}
aria-hidden="true"
className="absolute top-0 h-full w-px bg-border-subtle"
style={{ left: `calc(3.5rem + (100% - 3.5rem) * ${i} / 7)` }}
/>
))}
<ul aria-label="Events" className="m-0 list-none p-0">
{EVENTS.filter((e) => visible(e.cal)).map((e, i) => {
const height = (hours(e.to) - hours(e.from)) * ROW - 2;
const d = week.add({ days: e.day });
return (
<li
key={i}
className="absolute"
style={{
top: (hours(e.from) - FIRST_HOUR) * ROW + 1,
height,
...column(e.day, e.half),
}}
>
{details(
e.title,
d,
`${e.from} to ${e.to}`,
<CalendarEvent
title={e.title}
type={e.type}
layout="block"
start={instant(d, e.from)}
end={instant(d, e.to)}
timeZone={ZONE}
{...(height < 40 ? { time: "" } : {})}
className="h-full min-h-0 w-full py-0.5"
/>,
)}
</li>
);
})}
{(isSampleWeek ? created : []).map((title, i) => (
<li
key={`new-${i}`}
className="absolute"
style={{
top: (17 - FIRST_HOUR + i) * ROW + 1,
height: ROW - 2,
...column(3),
}}
>
<CalendarEvent
title={title}
layout="block"
start={instant(TODAY, `${17 + i}:00`)}
end={instant(TODAY, `${18 + i}:00`)}
timeZone={ZONE}
className="h-full min-h-0 w-full"
/>
</li>
))}
</ul>
{isSampleWeek ? (
<div
role="img"
aria-label="Current time, 10:25"
className="absolute h-0.5 bg-status-danger-solid"
style={{ top: (hours("10:25") - FIRST_HOUR) * ROW, ...column(3) }}
/>
) : (
<p className="absolute inset-x-14 top-24 text-center text-body-small text-secondary">
No events this week. Go to Today to see the sample week.
</p>
)}
</div>
</div>
</div>
</div>
</div>
<Modal
title="New event"
subtitle="Thursday 15 October, after 17:00"
isOpen={creating}
onOpenChange={setCreating}
primaryAction={
<Button
type="submit"
form="new-event"
isDisabled={!newTitle.trim() || created.length >= 2}
>
Create event
</Button>
}
secondaryAction={
<Button
variant="secondary"
onPress={() => {
setCreating(false);
}}
>
Cancel
</Button>
}
>
<form
id="new-event"
onSubmit={(event) => {
event.preventDefault();
if (!newTitle.trim() || created.length >= 2) return;
setCreated((c) => [...c, newTitle.trim()]);
setNewTitle("");
setCreating(false);
setDate(TODAY);
}}
>
<Input
label="Event title"
placeholder="Add a title"
value={newTitle}
onChange={setNewTitle}
/>
</form>
</Modal>
</div>
);
}
More scheduling and calendar blocks
- Month calendarEvents across a month, with a 'more' overflow.
- Team availabilityWho is free when, side by side.
- Book a meetingPick a day, then a free time slot.
- Event detailsA popover with time, place and people.
- Leave requestRequest time off and see the balance.
- Leave approvalReview a request and approve or decline.
- Shift pickerChoose a morning, evening or night shift.