Files
quietthanks/src/components/CalendarView.tsx
Gemini Agent e35545b156 Streamline save button and add calendar view with search
- Remove redundant Save button, keep only Save & New
- Add calendar view to timeline showing days with entries
- Add search functionality with highlighted matches
- Add date filtering by clicking calendar days
- Show results count when filtering

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 23:51:23 +00:00

156 lines
5.0 KiB
TypeScript

"use client";
import { useMemo } from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
interface CalendarViewProps {
entryDates: Set<string>; // YYYY-MM-DD format
selectedDate: string | null;
onSelectDate: (date: string | null) => void;
currentMonth: Date;
onChangeMonth: (date: Date) => void;
}
export function CalendarView({
entryDates,
selectedDate,
onSelectDate,
currentMonth,
onChangeMonth,
}: CalendarViewProps) {
const { days, monthLabel } = useMemo(() => {
const year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
// First day of month
const firstDay = new Date(year, month, 1);
const startDayOfWeek = firstDay.getDay();
// Days in month
const daysInMonth = new Date(year, month + 1, 0).getDate();
// Build calendar grid
const days: { date: string; day: number; isCurrentMonth: boolean; hasEntry: boolean }[] = [];
// Previous month padding
const prevMonth = new Date(year, month, 0);
const prevMonthDays = prevMonth.getDate();
for (let i = startDayOfWeek - 1; i >= 0; i--) {
const day = prevMonthDays - i;
const date = `${prevMonth.getFullYear()}-${String(prevMonth.getMonth() + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
days.push({ date, day, isCurrentMonth: false, hasEntry: entryDates.has(date) });
}
// Current month
for (let day = 1; day <= daysInMonth; day++) {
const date = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
days.push({ date, day, isCurrentMonth: true, hasEntry: entryDates.has(date) });
}
// Next month padding
const remaining = 42 - days.length; // 6 rows * 7 days
for (let day = 1; day <= remaining; day++) {
const nextMonth = new Date(year, month + 2, 0);
const date = `${nextMonth.getFullYear()}-${String(month + 2).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
days.push({ date, day, isCurrentMonth: false, hasEntry: entryDates.has(date) });
}
const monthLabel = firstDay.toLocaleDateString("en-US", { month: "long", year: "numeric" });
return { days, monthLabel };
}, [currentMonth, entryDates]);
const goToPrevMonth = () => {
onChangeMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1));
};
const goToNextMonth = () => {
onChangeMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1));
};
const goToToday = () => {
onChangeMonth(new Date());
onSelectDate(null);
};
return (
<div className="p-4 bg-surface border border-border rounded-xl">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<button
onClick={goToPrevMonth}
className="p-1 text-muted hover:text-foreground"
>
<ChevronLeft size={20} />
</button>
<div className="flex items-center gap-3">
<span className="font-medium">{monthLabel}</span>
<button
onClick={goToToday}
className="text-xs text-accent hover:underline"
>
Today
</button>
</div>
<button
onClick={goToNextMonth}
className="p-1 text-muted hover:text-foreground"
>
<ChevronRight size={20} />
</button>
</div>
{/* Day headers */}
<div className="grid grid-cols-7 gap-1 mb-1">
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((day) => (
<div key={day} className="text-center text-xs text-muted py-1">
{day}
</div>
))}
</div>
{/* Calendar grid */}
<div className="grid grid-cols-7 gap-1">
{days.map(({ date, day, isCurrentMonth, hasEntry }) => {
const isSelected = selectedDate === date;
const isToday = date === new Date().toISOString().split("T")[0];
return (
<button
key={date}
onClick={() => onSelectDate(isSelected ? null : date)}
className={`
relative aspect-square flex items-center justify-center text-sm rounded-lg transition-colors
${!isCurrentMonth ? "text-muted/30" : ""}
${isSelected ? "bg-accent text-white" : "hover:bg-border"}
${isToday && !isSelected ? "ring-1 ring-accent" : ""}
`}
>
{day}
{hasEntry && !isSelected && (
<span className="absolute bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 bg-accent rounded-full" />
)}
</button>
);
})}
</div>
{/* Legend */}
<div className="mt-3 flex items-center gap-4 text-xs text-muted">
<div className="flex items-center gap-1">
<span className="w-1.5 h-1.5 bg-accent rounded-full" />
<span>Has entry</span>
</div>
{selectedDate && (
<button
onClick={() => onSelectDate(null)}
className="text-accent hover:underline"
>
Clear filter
</button>
)}
</div>
</div>
);
}