Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions src/components/CalendarMonthView.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
.monthView {
display: flex;
flex-direction: column;
gap: 8px;
}

.grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
}

.dayHeader {
text-align: center;
font-size: 0.7rem;
font-weight: 600;
color: var(--text-secondary);
padding: 4px 0;
text-transform: uppercase;
}

.emptyCell {
aspect-ratio: 1;
}

.dayCell {
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--text-primary);
transition: all 0.15s ease;
padding: 2px;
font-family: inherit;
}

.dayCell:hover {
background: var(--surface-hover);
border-color: var(--surface-border);
}

.dayNumber {
font-size: 0.8rem;
line-height: 1;
}

.today {
background: rgba(212, 148, 107, 0.1);
border-color: var(--accent-color);
}

.today .dayNumber {
color: var(--accent-color);
font-weight: 700;
}

.selected {
background: rgba(212, 148, 107, 0.2);
border-color: var(--accent-light);
}

.dots {
display: flex;
gap: 2px;
align-items: center;
}

.dot {
width: 4px;
height: 4px;
border-radius: 50%;
}

.eventDot {
background: var(--accent-color);
}

.taskDot {
background: var(--color-info);
}

.moreCount {
font-size: 0.55rem;
color: var(--text-secondary);
}

.dayDetail {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px;
background: rgba(0, 0, 0, 0.15);
border: 1px solid var(--surface-border);
border-radius: var(--radius-md);
}

.dayDetailHeader {
font-size: 0.8rem;
font-weight: 600;
color: var(--accent-light);
}

.emptyDay {
font-size: 0.8rem;
color: var(--text-secondary);
font-style: italic;
}

.dayItem {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
}

.itemDot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}

.dayItemTitle {
flex-grow: 1;
}

.itemBadge {
font-size: 0.65rem;
padding: 0.1rem 0.4rem;
border-radius: 999px;
white-space: nowrap;
}

.eventBadge {
background: rgba(212, 148, 107, 0.15);
color: var(--accent-light);
}

.taskBadge {
background: rgba(123, 159, 191, 0.14);
color: #A3C4DB;
}
148 changes: 148 additions & 0 deletions src/components/CalendarMonthView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { useMemo } from "react";
import styles from "./CalendarMonthView.module.css";
import type { AgendaItem } from "./EventsWidget";
Comment on lines +1 to +3

Copilot AI Apr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CalendarMonthView uses React hooks (useMemo) but the file is missing a "use client" directive. In Next.js App Router this will be treated as a Server Component and cannot be imported/used by the client-side EventsWidget, causing a build/runtime error. Add "use client" at the top of this file (before imports).

Copilot uses AI. Check for mistakes.

const DAY_LABELS = ["S", "M", "T", "W", "T", "F", "S"];

function sameDay(a: Date, b: Date): boolean {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}

export default function CalendarMonthView({
items,
viewDate,
selectedDate,
onSelectDate,
}: {
items: AgendaItem[];
viewDate: Date;
selectedDate: Date | null;
onSelectDate: (date: Date) => void;
}) {
const year = viewDate.getFullYear();
const month = viewDate.getMonth();

const firstDayOfWeek = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();

const today = new Date();

const itemsByDay = useMemo(() => {
const map = new Map<number, AgendaItem[]>();
for (const item of items) {
const d = item.date;
if (d.getFullYear() === year && d.getMonth() === month) {
const day = d.getDate();
if (!map.has(day)) map.set(day, []);
map.get(day)!.push(item);
}
}
return map;
}, [items, year, month]);

const cells: (number | null)[] = [];
for (let i = 0; i < firstDayOfWeek; i++) cells.push(null);
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
while (cells.length % 7 !== 0) cells.push(null);

const selectedDayItems = selectedDate
? items.filter((item) => sameDay(item.date, selectedDate))
: [];

return (
<div className={styles.monthView}>
<div className={styles.grid}>
{DAY_LABELS.map((label, i) => (
<div key={i} className={styles.dayHeader}>
{label}
</div>
))}
{cells.map((day, i) => {
if (day === null) {
return <div key={`e-${i}`} className={styles.emptyCell} />;
}

const cellDate = new Date(year, month, day);
const isToday = sameDay(cellDate, today);
const isSelected =
selectedDate !== null && sameDay(cellDate, selectedDate);
const dayItems = itemsByDay.get(day) || [];

return (
<button
key={day}
type="button"
className={[
styles.dayCell,
isToday ? styles.today : "",
isSelected ? styles.selected : "",
]
.filter(Boolean)
.join(" ")}
onClick={() => onSelectDate(cellDate)}
>
<span className={styles.dayNumber}>{day}</span>
{dayItems.length > 0 && (
<div className={styles.dots}>
{dayItems.slice(0, 3).map((item) => (
<span
key={`${item.kind}-${item.id}`}
className={`${styles.dot} ${
item.kind === "task" ? styles.taskDot : styles.eventDot
}`}
/>
))}
{dayItems.length > 3 && (
<span className={styles.moreCount}>
+{dayItems.length - 3}
</span>
)}
</div>
)}
</button>
);
})}
</div>

{selectedDate && (
<div className={styles.dayDetail}>
<div className={styles.dayDetailHeader}>
{selectedDate.toLocaleDateString("en-US", {
weekday: "long",
month: "long",
day: "numeric",
})}
</div>
{selectedDayItems.length === 0 ? (
<p className={styles.emptyDay}>Nothing scheduled</p>
) : (
selectedDayItems.map((item) => (
<div
key={`${item.kind}-${item.id}`}
className={styles.dayItem}
>
<span
className={`${styles.itemDot} ${
item.kind === "task" ? styles.taskDot : styles.eventDot
}`}
/>
<span className={styles.dayItemTitle}>{item.title}</span>
<span
className={`${styles.itemBadge} ${
item.kind === "task" ? styles.taskBadge : styles.eventBadge
}`}
>
{item.kind === "task" ? "Task" : "Event"}
</span>
</div>
))
)}
</div>
)}
</div>
);
}
Loading
Loading