yiekheng d5b8c0beeb feat(reminders): name is required (was optional with auto-derive)
Previously the name field auto-derived from the first text part when
the operator left it blank. That's brittle once reminders carry
multiple parts of varying provenance, and confusing in lists where
"Reminder" or partial sentences crowd in.

Now: every reminder must carry a non-empty name, capped at 60 chars.

  - Zod schema on createReminder/updateReminder: name moves from
    `z.string().nullable().optional()` to
    `z.string().trim().min(1, "Give the reminder a name").max(60)`.
    Stale-URL legacy callers that omit it now get a clear server error.
  - Wizard compose step: input has `required` + `aria-required`,
    placeholder + label simplified ("(optional)" tag and the helper
    paragraph removed), Continue blocks on empty.
  - Edit-message form: same — required, aria-required, save blocked
    on empty, the "leave blank and we'll auto-derive" hint dropped.
  - Review-submit client: defensive fail-fast for stale-bookmark URLs
    that arrive at step 5 without a name — bounces back with
    "Give the reminder a name (back on the Message step)" instead of
    letting the server reject.

The resolveReminderName helper stays put — duplicateReminderAction
and any future caller still benefit from the trim+clamp+fallback
chain. Helper unit tests unaffected (they test the resolver in
isolation, the policy-tightening lives at the schema layer above).

298 web tests still passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:15:16 +08:00

109 lines
3.1 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { CalendarCheckIcon, AlertCircleIcon, Loader2Icon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { createReminderAction, updateReminderAction } from "@/actions/reminders";
import { cn } from "@/lib/utils";
import type { MessagePart } from "@/lib/reminder-messages";
interface ReviewSubmitClientProps {
accountId: string;
groupIds?: string;
name?: string;
messages: MessagePart[];
scheduledAt: string;
rrule?: string;
editReminderId?: string;
timezone: string;
}
export function ReviewSubmitClient({
accountId,
groupIds,
name,
messages,
scheduledAt,
rrule,
editReminderId,
timezone,
}: ReviewSubmitClientProps) {
const router = useRouter();
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleSchedule() {
const trimmedName = name?.trim();
if (!trimmedName) {
// The wizard's compose step now blocks Continue when the name is
// blank, so the only way to land here without one is a stale
// bookmarked URL. Bounce the operator back to step 2 with a
// clear error rather than letting the server reject it.
setError("Give the reminder a name (back on the Message step).");
return;
}
setSubmitting(true);
setError(null);
try {
const payload = {
accountId,
groupIds: groupIds ? groupIds.split(",").filter(Boolean) : [],
name: trimmedName,
messages,
scheduledAtIso: scheduledAt,
rrule: rrule ?? null,
timezone,
};
const result = editReminderId
? await updateReminderAction({ ...payload, reminderId: editReminderId })
: await createReminderAction(payload);
if (result.ok) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
router.push(`/reminders/${result.reminderId}` as any);
} else {
setError(result.error);
setSubmitting(false);
}
} catch (err) {
setError(err instanceof Error ? err.message : "An unexpected error occurred.");
setSubmitting(false);
}
}
return (
<div className="space-y-3 pt-2">
{error && (
<div className="flex items-start gap-2 rounded-lg bg-destructive/10 px-3 py-2.5 text-sm text-destructive">
<AlertCircleIcon className="size-4 shrink-0 mt-0.5" />
<span>{error}</span>
</div>
)}
<div className="flex justify-end">
<Button
type="button"
size="lg"
onClick={handleSchedule}
disabled={submitting}
className={cn("gap-2", submitting && "cursor-wait")}
>
{submitting ? (
<>
<Loader2Icon className="size-4 animate-spin" />
{editReminderId ? "Saving…" : "Scheduling…"}
</>
) : (
<>
<CalendarCheckIcon className="size-4" />
{editReminderId ? "Save changes" : "Schedule Reminder"}
</>
)}
</Button>
</div>
</div>
);
}