カレンダー週次ビュー
:LiTarget: 用途
Google Calendar 風の週次ビュー。イベント表示・追加対応。
:LiSparkle: 特徴
- 週次表示
- イベント追加
- D&D配置
- 色分け
:LiCode: コード(コピペ用)
'use client';
import { useMemo } from 'react';
type Event = { id: string; title: string; start: Date; end: Date; color?: string };
// 週次ビュー(時間帯 6:00 - 24:00)
export function WeekCalendar({
weekStart, events, onClickEvent, onClickSlot,
}: {
weekStart: Date; // 月曜0:00
events: Event[];
onClickEvent?: (e: Event) => void;
onClickSlot?: (date: Date) => void;
}) {
const days = useMemo(() => Array.from({ length: 7 }, (_, i) => {
const d = new Date(weekStart);
d.setDate(d.getDate() + i);
return d;
}), [weekStart]);
const HOUR_HEIGHT = 48;
const START_HOUR = 6;
const END_HOUR = 24;
const HOURS = Array.from({ length: END_HOUR - START_HOUR }, (_, i) => START_HOUR + i);
const eventsByDay = useMemo(() => {
const map = new Map<string, Event[]>();
for (const ev of events) {
const key = ev.start.toDateString();
const arr = map.get(key) ?? [];
arr.push(ev);
map.set(key, arr);
}
return map;
}, [events]);
const eventStyle = (ev: Event) => {
const startMin = (ev.start.getHours() - START_HOUR) * 60 + ev.start.getMinutes();
const durMin = (ev.end.getTime() - ev.start.getTime()) / 60000;
return { top: `${(startMin / 60) * HOUR_HEIGHT}px`, height: `${(durMin / 60) * HOUR_HEIGHT}px` };
};
return (
<div className="bg-zinc-900 rounded-2xl border border-zinc-800 overflow-hidden">
<div className="grid grid-cols-[60px_repeat(7,1fr)] border-b border-zinc-800">
<div />
{days.map((d) => (
<div key={d.toISOString()} className="text-center py-2 text-xs text-zinc-400">
<div>{['月','火','水','木','金','土','日'][d.getDay() === 0 ? 6 : d.getDay() - 1]}</div>
<div className="text-base text-zinc-100">{d.getDate()}</div>
</div>
))}
</div>
<div className="grid grid-cols-[60px_repeat(7,1fr)] relative">
<div>
{HOURS.map((h) => (
<div key={h} className="text-[10px] text-zinc-500 text-right pr-2" style={{ height: `${HOUR_HEIGHT}px` }}>{h}:00</div>
))}
</div>
{days.map((d) => (
<div key={d.toISOString()} className="relative border-l border-zinc-800" style={{ height: `${HOURS.length * HOUR_HEIGHT}px` }}
onClick={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
const y = e.clientY - rect.top;
const hour = START_HOUR + Math.floor(y / HOUR_HEIGHT);
const date = new Date(d); date.setHours(hour, 0, 0, 0);
onClickSlot?.(date);
}}>
{(eventsByDay.get(d.toDateString()) ?? []).map((ev) => (
<div key={ev.id}
onClick={(e) => { e.stopPropagation(); onClickEvent?.(ev); }}
className="absolute left-1 right-1 rounded px-1.5 py-1 text-[11px] text-white cursor-pointer overflow-hidden"
style={{ ...eventStyle(ev), background: ev.color ?? '#6366f1' }}>
{ev.title}
</div>
))}
</div>
))}
</div>
</div>
);
}
:LiHandPointer: 使い方
対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。
:LiAlertCircle: 注意事項
- 依存パッケージを忘れず追加