trails/apps/planner/app/components/NotesPanel.tsx
Ullrich Schäfer 0a8dd0b766
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>
2026-03-26 01:00:42 +01:00

66 lines
1.9 KiB
TypeScript

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>
);
}