feat: p2 batch 28-31 reminders lists comments bang stats

Calendar events support multiple reminder offsets with presets and per-user defaults.

Lists index adds inline task entry and list property editing.

Generic entity comments on list detail. New bangs.stats dashboard widget.

Closes Gitea #28, #29, #30, #31. Migrations 0022 and 0023.
This commit is contained in:
ginnoir
2026-07-04 22:17:35 -05:00
parent 7714b1187c
commit e1c2a090fb
31 changed files with 1287 additions and 92 deletions
@@ -15,6 +15,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ShareButton } from "@/components/share-button";
import { ReminderPicker } from "@/components/reminder-picker";
import type { CalView } from "@/modules/_core/themes";
import {
Select,
@@ -25,6 +26,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import type { CalendarDto, CalendarEventDto } from "../server/queries";
import { getEventReminderOffsets } from "../server/queries";
import {
createCalendar,
createEvent,
@@ -57,7 +59,7 @@ type EventDraft = {
allDay: boolean;
location: string;
notes: string;
remindMinutesBefore: number | null;
reminderOffsets: number[];
};
type MobileView = "day" | "week" | "agenda";
@@ -99,10 +101,12 @@ export function CalendarShell({
calendars,
events,
defaultView = "month",
defaultReminderOffsets = [30],
}: {
calendars: CalendarDto[];
events: CalendarEventDto[];
defaultView?: CalView;
defaultReminderOffsets?: number[];
}) {
const [calendarRows, setCalendarRows] = useState(calendars);
const [eventRows, setEventRows] = useState(events);
@@ -160,13 +164,14 @@ export function CalendarShell({
allDay,
location: "",
notes: "",
remindMinutesBefore: 30,
reminderOffsets: [...defaultReminderOffsets],
});
}
function openExistingEvent({ event }: EventClickArg) {
const row = eventRows.find((item) => item.id === event.id);
if (!row) return;
const eventId = row.id;
setSelectedEvent({
id: row.id,
calendarId: row.calendarId,
@@ -176,7 +181,12 @@ export function CalendarShell({
allDay: row.allDay,
location: row.location ?? "",
notes: row.notes ?? "",
remindMinutesBefore: null,
reminderOffsets: [],
});
void getEventReminderOffsets(eventId).then((offsets) => {
setSelectedEvent((current) =>
current?.id === eventId ? { ...current, reminderOffsets: offsets } : current,
);
});
}
@@ -199,7 +209,11 @@ export function CalendarShell({
startTransition(async () => {
if (eventId) {
await updateEvent({ id: eventId, ...payload });
await updateEvent({
id: eventId,
...payload,
reminderOffsets: selectedEvent.reminderOffsets,
});
setLastCalendarId(payload.calendarId);
setEventRows((current) =>
current.map((event) =>
@@ -220,7 +234,7 @@ export function CalendarShell({
} else {
const created = await createEvent({
...payload,
remindMinutesBefore: selectedEvent.remindMinutesBefore,
reminderOffsets: selectedEvent.reminderOffsets,
});
setLastCalendarId(payload.calendarId);
setEventRows((current) => [...current, created]);
@@ -538,22 +552,11 @@ export function CalendarShell({
placeholder="Add details…"
/>
</div>
{!selectedEvent.id && (
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
className="size-4 cursor-pointer"
checked={selectedEvent.remindMinutesBefore !== null}
onChange={(e) =>
setSelectedEvent({
...selectedEvent,
remindMinutesBefore: e.target.checked ? 30 : null,
})
}
/>
Remind me 30 min before
</label>
)}
<ReminderPicker
offsets={selectedEvent.reminderOffsets}
onChange={(reminderOffsets) => setSelectedEvent({ ...selectedEvent, reminderOffsets })}
disabled={isPending}
/>
<div className="flex items-center justify-between gap-2 pt-2">
<div className="flex gap-2">
{selectedEvent.id && (