Add transactional emails (SMTP) and planner features (no-go areas, notes, crash recovery)
Transactional emails: - Add nodemailer SMTP email module with dev-mode console logging - Magic link template and welcome template with HTML + plain text - Wire sendMagicLink into login flow, sendWelcome into registration - Update privacy page and deploy docs for SMTP configuration Planner features: - No-go areas: draw polygons on map (leaflet-geoman), synced via Yjs, passed to BRouter as nogos parameter, route recomputes on change - Session notes: collaborative Y.Text textarea in sidebar tab - Crash recovery: periodic localStorage save of Yjs state, restore on reconnect - Rate limit session creation (10/IP/hour) in /new route Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
05b5f6febf
commit
0a8dd0b766
27 changed files with 1030 additions and 69 deletions
66
apps/planner/app/components/NotesPanel.tsx
Normal file
66
apps/planner/app/components/NotesPanel.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { useEffect, useRef, useCallback } from "react";
|
||||
import type { YjsState } from "~/lib/use-yjs";
|
||||
|
||||
interface NotesPanelProps {
|
||||
yjs: YjsState;
|
||||
}
|
||||
|
||||
export function NotesPanel({ yjs }: NotesPanelProps) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const isLocalChange = useRef(false);
|
||||
|
||||
// Sync Y.Text → textarea
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
// Set initial value
|
||||
textarea.value = yjs.notes.toString();
|
||||
|
||||
const observer = () => {
|
||||
if (isLocalChange.current) return;
|
||||
const pos = textarea.selectionStart;
|
||||
textarea.value = yjs.notes.toString();
|
||||
textarea.selectionStart = pos;
|
||||
textarea.selectionEnd = pos;
|
||||
};
|
||||
|
||||
yjs.notes.observe(observer);
|
||||
return () => yjs.notes.unobserve(observer);
|
||||
}, [yjs.notes]);
|
||||
|
||||
// textarea → Y.Text
|
||||
const handleInput = useCallback(
|
||||
(e: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
const textarea = e.currentTarget;
|
||||
const newValue = textarea.value;
|
||||
const currentValue = yjs.notes.toString();
|
||||
|
||||
if (newValue === currentValue) return;
|
||||
|
||||
isLocalChange.current = true;
|
||||
yjs.doc.transact(() => {
|
||||
yjs.notes.delete(0, yjs.notes.length);
|
||||
yjs.notes.insert(0, newValue);
|
||||
});
|
||||
isLocalChange.current = false;
|
||||
},
|
||||
[yjs.notes, yjs.doc],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="border-b border-gray-200 px-4 py-3">
|
||||
<h2 className="text-sm font-medium text-gray-900">Notes</h2>
|
||||
</div>
|
||||
<div className="flex-1 p-2">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
onInput={handleInput}
|
||||
className="h-full w-full resize-none rounded border border-gray-200 p-3 text-sm text-gray-700 placeholder-gray-400 focus:border-blue-300 focus:outline-none"
|
||||
placeholder="Add notes for this session..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue