Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Open-source, self-hosted note-taking tool built for quick capture. Markdown-nati
## Features

- **Instant Capture** — Timeline-first UI. Open, write, done — no folders to navigate.
- **Backdate Memos** — Set a custom creation date when writing a new memo to place it anywhere on your timeline — perfect for journaling, importing old notes, or filling in past events.
- **Total Data Ownership** — Self-hosted on your infrastructure. Notes stored in Markdown, always portable. Zero telemetry.
- **Radical Simplicity** — Single Go binary, ~20MB Docker image. One command to deploy with SQLite, MySQL, or PostgreSQL.
- **Open & Extensible** — MIT-licensed with full REST and gRPC APIs for integration.
Expand Down
73 changes: 73 additions & 0 deletions web/src/components/MemoEditor/components/TimestampPopover.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { CalendarClockIcon, XIcon } from "lucide-react";
import { type FC, useRef, useState } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { useTranslate } from "@/utils/i18n";
import { useEditorContext } from "../state";

Expand All @@ -10,6 +12,12 @@ function formatDate(date: Date): string {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}

/** Format a Date as a value for <input type="datetime-local"> (YYYY-MM-DDTHH:mm). */
function toDatetimeLocalValue(date: Date): string {
const pad = (n: number) => String(n).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
}

function parseDate(value: string): Date | undefined {
const match = value.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
if (!match) return undefined;
Expand Down Expand Up @@ -55,6 +63,7 @@ const TimestampInput: FC<{
);
};

/** Popover shown when editing an existing memo (full create + update time editing). */
export const TimestampPopover: FC = () => {
const t = useTranslate();
const { state, actions, dispatch } = useEditorContext();
Expand Down Expand Up @@ -87,3 +96,67 @@ export const TimestampPopover: FC = () => {
</Popover>
);
};

/** Calendar icon with a date & time picker for backdating new memos. */
export const BackdatePopover: FC = () => {
const t = useTranslate();
const { state, actions, dispatch } = useEditorContext();
const { createTime } = state.timestamps;
const [open, setOpen] = useState(false);

const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (!value) return;
// Parse datetime-local value (YYYY-MM-DDTHH:mm) as local time explicitly
const [datePart, timePart] = value.split("T");
const [year, month, day] = datePart.split("-").map(Number);
const [hours, minutes] = timePart.split(":").map(Number);
const date = new Date(year, month - 1, day, hours, minutes);
if (!Number.isNaN(date.getTime())) {
dispatch(actions.setTimestamps({ createTime: date }));
}
};

const handleClear = (e: React.MouseEvent) => {
e.stopPropagation();
dispatch(actions.setTimestamps({ createTime: undefined }));
setOpen(false);
};

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
className={cn(
"flex items-center gap-1.5 text-sm transition-colors cursor-pointer",
createTime ? "text-foreground hover:text-foreground/80" : "text-muted-foreground hover:text-foreground",
)}
title={t("editor.set-creation-date")}
>
<CalendarClockIcon className="size-4" />
{createTime && <span className="font-mono text-xs">{formatDate(createTime)}</span>}
</button>
</PopoverTrigger>
<PopoverContent align="start" className="w-auto p-3 space-y-2">
<label className="text-xs font-medium text-muted-foreground">{t("editor.set-creation-date")}</label>
<input
type="datetime-local"
className="block w-full rounded-md border border-border bg-background px-2 py-1 text-sm"
value={createTime ? toDatetimeLocalValue(createTime) : ""}
onChange={handleDateChange}
/>
{createTime && (
<button
type="button"
onClick={handleClear}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-destructive transition-colors cursor-pointer"
>
<XIcon className="size-3" />
{t("common.clear")}
</button>
)}
</PopoverContent>
</Popover>
);
};
2 changes: 1 addition & 1 deletion web/src/components/MemoEditor/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export * from "./EditorContent";
export * from "./EditorMetadata";
export * from "./EditorToolbar";
export { FocusModeExitButton, FocusModeOverlay } from "./FocusModeOverlay";
export { TimestampPopover } from "./TimestampPopover";
export { BackdatePopover, TimestampPopover } from "./TimestampPopover";
7 changes: 6 additions & 1 deletion web/src/components/MemoEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useTranslate } from "@/utils/i18n";
import { convertVisibilityFromString } from "@/utils/memo";
import {
AudioRecorderPanel,
BackdatePopover,
EditorContent,
EditorMetadata,
EditorToolbar,
Expand Down Expand Up @@ -289,10 +290,14 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
{/* Exit button is absolutely positioned in top-right corner when active */}
<FocusModeExitButton isActive={state.ui.isFocusMode} onToggle={handleToggleFocusMode} title={t("editor.exit-focus-mode")} />

{memoName && (
{memoName ? (
<div className="w-full -mb-1">
<TimestampPopover />
</div>
) : (
<div className="w-full -mb-1">
<BackdatePopover />
</div>
)}

{/* Editor content grows to fill available space in focus mode */}
Expand Down
3 changes: 2 additions & 1 deletion web/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@
"trigger": "Audio aufnehmen",
"unsupported": "Audioaufnahme wird nicht unterstützt",
"unsupported-description": "Dieser Browser kann keine Audiodaten vom Memo Composer aufzeichnen."
}
},
"set-creation-date": "Erstellungsdatum festlegen"
},
"inbox": {
"failed-to-load": "Fehler beim Laden des Eintrags",
Expand Down
3 changes: 2 additions & 1 deletion web/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@
"trigger": "Record audio",
"unsupported": "Audio recording unsupported",
"unsupported-description": "This browser cannot record audio from the memo composer."
}
},
"set-creation-date": "Set creation date"
},
"inbox": {
"failed-to-load": "Failed to load inbox item",
Expand Down