M3 reminders: notes.remind_at + Reminders view
- Migration 0010: notes.remind_at (nullable tz). PATCH accepts remind_at (ISO set / null clear); GET /api/notes/reminders (soonest first, non-trashed); serialize includes remind_at. - Frontend: datetime util (local<->ISO, format, overdue); notes store setReminder; editor datetime-local picker + clear; card reminder chip (overdue = red); sidebar Reminders entry + /reminders view. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -130,6 +130,13 @@ async function signOut() {
|
||||
<RouterLink to="/trash" class="nav-link" :class="route.name === 'trash' ? 'nav-link-active' : ''">
|
||||
<Icon name="trash" /> Trash
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/reminders"
|
||||
class="nav-link"
|
||||
:class="route.name === 'reminders' ? 'nav-link-active' : ''"
|
||||
>
|
||||
<Icon name="bell" /> Reminders
|
||||
</RouterLink>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const paths: Record<string, string> = {
|
||||
checkbox: '<rect width="18" height="18" x="3" y="3" rx="2"/><path d="m9 12 2 2 4-4"/>',
|
||||
image: '<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>',
|
||||
graph: '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" x2="15.42" y1="13.51" y2="17.49"/><line x1="15.41" x2="8.59" y1="6.51" y2="10.49"/>',
|
||||
bell: '<path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/>',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Note } from "../stores/notes";
|
||||
import Icon from "./Icon.vue";
|
||||
import LinkedText from "./LinkedText.vue";
|
||||
import NoteChecklist from "./NoteChecklist.vue";
|
||||
import { formatReminder, isOverdue } from "../notes/datetime";
|
||||
|
||||
defineProps<{ note: Note; reorderable?: boolean }>();
|
||||
const emit = defineEmits<{
|
||||
@@ -79,6 +80,31 @@ function cardClass(color: NoteColor): string {
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="note.remind_at" class="mt-2">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs"
|
||||
:class="
|
||||
isOverdue(note.remind_at)
|
||||
? 'bg-red-100 text-red-700 dark:bg-red-950/50 dark:text-red-300'
|
||||
: 'bg-black/5 text-neutral-600 dark:bg-white/10 dark:text-neutral-300'
|
||||
"
|
||||
>
|
||||
<svg
|
||||
class="h-3 w-3"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
{{ formatReminder(note.remind_at) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-2 flex items-center justify-end gap-0.5 opacity-0 transition focus-within:opacity-100 group-hover:opacity-100"
|
||||
>
|
||||
|
||||
@@ -7,6 +7,7 @@ import ColorPicker from "./ColorPicker.vue";
|
||||
import Icon from "./Icon.vue";
|
||||
import LabelPicker from "./LabelPicker.vue";
|
||||
import NoteChecklist from "./NoteChecklist.vue";
|
||||
import { fromLocalInput, toLocalInput } from "../notes/datetime";
|
||||
import type { Note, NoteLabel } from "../stores/notes";
|
||||
import type { NoteColor } from "../notes/colors";
|
||||
|
||||
@@ -86,6 +87,12 @@ async function openLink(link: { title: string; id: string | null }) {
|
||||
emit("navigate", created.id);
|
||||
}
|
||||
|
||||
const reminderLocal = computed(() => toLocalInput(liveNote.value.remind_at));
|
||||
|
||||
function onReminderChange(e: Event) {
|
||||
void notes.setReminder(props.note.id, fromLocalInput((e.target as HTMLInputElement).value));
|
||||
}
|
||||
|
||||
async function onLabelsChange(next: NoteLabel[]) {
|
||||
labelList.value = next;
|
||||
await notes.setLabels(
|
||||
@@ -226,6 +233,24 @@ async function act(fn: () => Promise<void>) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 pt-1">
|
||||
<Icon name="bell" class="text-neutral-400" />
|
||||
<input
|
||||
type="datetime-local"
|
||||
:value="reminderLocal"
|
||||
class="rounded-md border border-neutral-300 bg-white px-2 py-1 text-xs text-neutral-700 outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200"
|
||||
@change="onReminderChange"
|
||||
/>
|
||||
<button
|
||||
v-if="liveNote.remind_at"
|
||||
type="button"
|
||||
class="text-xs text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"
|
||||
@click="notes.setReminder(note.id, null)"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="outgoingLinks.length || backlinks.length"
|
||||
class="flex flex-col gap-2 border-t border-neutral-100 pt-2 dark:border-neutral-800"
|
||||
|
||||
Reference in New Issue
Block a user