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.
133 lines
4.1 KiB
TypeScript
133 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { Plus, X } from "lucide-react";
|
|
import { useMemo, useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
REMINDER_PRESETS,
|
|
customOffsetToMinutes,
|
|
formatReminderOffset,
|
|
normalizeReminderOffsets,
|
|
type ReminderOffsetUnit,
|
|
} from "@/lib/reminder-offsets";
|
|
|
|
type Props = {
|
|
offsets: number[];
|
|
onChange: (offsets: number[]) => void;
|
|
disabled?: boolean;
|
|
};
|
|
|
|
export function ReminderPicker({ offsets, onChange, disabled }: Props) {
|
|
const [customValue, setCustomValue] = useState("1");
|
|
const [customUnit, setCustomUnit] = useState<ReminderOffsetUnit>("hours");
|
|
const normalized = useMemo(() => normalizeReminderOffsets(offsets), [offsets]);
|
|
|
|
function addOffset(minutes: number) {
|
|
onChange(normalizeReminderOffsets([...normalized, minutes]));
|
|
}
|
|
|
|
function removeOffset(minutes: number) {
|
|
onChange(normalized.filter((o) => o !== minutes));
|
|
}
|
|
|
|
function addCustom() {
|
|
const parsed = Number.parseInt(customValue, 10);
|
|
if (!Number.isFinite(parsed) || parsed < 0) return;
|
|
addOffset(customOffsetToMinutes(parsed, customUnit));
|
|
}
|
|
|
|
const availablePresets = REMINDER_PRESETS.filter((p) => !normalized.includes(p.offsetMinutes));
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<Label>Reminders</Label>
|
|
{normalized.length === 0 ? (
|
|
<p className="text-[13px] text-[var(--ink-mute)]">No reminders set.</p>
|
|
) : (
|
|
<ul className="space-y-1.5">
|
|
{normalized.map((offset) => (
|
|
<li
|
|
key={offset}
|
|
className="flex items-center justify-between gap-2 rounded-md border border-[var(--hair)] px-2.5 py-1.5 text-[13px]"
|
|
>
|
|
<span>{formatReminderOffset(offset)}</span>
|
|
<Button
|
|
type="button"
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
aria-label={`Remove ${formatReminderOffset(offset)}`}
|
|
disabled={disabled}
|
|
onClick={() => removeOffset(offset)}
|
|
>
|
|
<X className="size-3.5" />
|
|
</Button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
{availablePresets.length > 0 && (
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{availablePresets.map((preset) => (
|
|
<Button
|
|
key={preset.offsetMinutes}
|
|
type="button"
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={disabled}
|
|
onClick={() => addOffset(preset.offsetMinutes)}
|
|
>
|
|
<Plus className="size-3" />
|
|
{preset.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid gap-2 sm:grid-cols-[80px_120px_auto] sm:items-end">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="reminder-custom-value">Custom</Label>
|
|
<Input
|
|
id="reminder-custom-value"
|
|
type="number"
|
|
min={0}
|
|
value={customValue}
|
|
disabled={disabled}
|
|
onChange={(e) => setCustomValue(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="reminder-custom-unit">Unit</Label>
|
|
<Select
|
|
value={customUnit}
|
|
onValueChange={(value) => setCustomUnit(value as ReminderOffsetUnit)}
|
|
disabled={disabled}
|
|
>
|
|
<SelectTrigger id="reminder-custom-unit">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="minutes">Minutes</SelectItem>
|
|
<SelectItem value="hours">Hours</SelectItem>
|
|
<SelectItem value="days">Days</SelectItem>
|
|
<SelectItem value="weeks">Weeks</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Button type="button" variant="secondary" disabled={disabled} onClick={addCustom}>
|
|
Add custom
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|