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
|
|
@ -10,6 +10,7 @@ import { ExportButton } from "~/components/ExportButton";
|
|||
import { SaveToJournalButton } from "~/components/SaveToJournalButton";
|
||||
import { YjsDebugPanel } from "~/components/YjsDebugPanel";
|
||||
import { ParticipantList } from "~/components/ParticipantList";
|
||||
import { NotesPanel } from "~/components/NotesPanel";
|
||||
|
||||
const PlannerMap = lazy(() =>
|
||||
import("~/components/PlannerMap").then((m) => ({ default: m.PlannerMap })),
|
||||
|
|
@ -104,6 +105,39 @@ function useAwarenessToasts(yjs: YjsState | null, t: TFunction) {
|
|||
return toasts;
|
||||
}
|
||||
|
||||
function SidebarTabs({ yjs, routeStats }: { yjs: YjsState; routeStats: ReturnType<typeof useRouting>["routeStats"] }) {
|
||||
const { t } = useTranslation("planner");
|
||||
const [tab, setTab] = useState<"waypoints" | "notes">("waypoints");
|
||||
|
||||
return (
|
||||
<aside className="hidden w-72 border-l border-gray-200 bg-white md:flex md:flex-col">
|
||||
<div className="flex border-b border-gray-200">
|
||||
<button
|
||||
onClick={() => setTab("waypoints")}
|
||||
className={`flex-1 px-3 py-2 text-xs font-medium ${tab === "waypoints" ? "border-b-2 border-blue-500 text-blue-600" : "text-gray-500 hover:text-gray-700"}`}
|
||||
>
|
||||
{t("sidebar.waypoints", "Waypoints")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab("notes")}
|
||||
className={`flex-1 px-3 py-2 text-xs font-medium ${tab === "notes" ? "border-b-2 border-blue-500 text-blue-600" : "text-gray-500 hover:text-gray-700"}`}
|
||||
>
|
||||
{t("sidebar.notes", "Notes")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{tab === "waypoints" ? (
|
||||
<Suspense fallback={null}>
|
||||
<WaypointSidebar yjs={yjs} routeStats={routeStats} />
|
||||
</Suspense>
|
||||
) : (
|
||||
<NotesPanel yjs={yjs} />
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
interface SessionViewProps {
|
||||
sessionId: string;
|
||||
callbackUrl?: string;
|
||||
|
|
@ -184,11 +218,7 @@ export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl,
|
|||
<ElevationChart yjs={yjs} onHover={handleElevationHover} />
|
||||
</Suspense>
|
||||
</main>
|
||||
<aside className="hidden w-72 border-l border-gray-200 bg-white md:block">
|
||||
<Suspense fallback={null}>
|
||||
<WaypointSidebar yjs={yjs} routeStats={routeStats} />
|
||||
</Suspense>
|
||||
</aside>
|
||||
<SidebarTabs yjs={yjs} routeStats={routeStats} />
|
||||
</div>
|
||||
<YjsDebugPanel yjs={yjs} />
|
||||
{toasts.length > 0 && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue