180 lines
5.4 KiB
TypeScript
180 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Button, Card } from "@/components/ui/Primitives";
|
|
import { createNote } from "@/lib/notes-client";
|
|
import { NOTE_TEMPLATES } from "@/lib/note-templates";
|
|
import type { WorkspaceSummary } from "@/lib/types";
|
|
|
|
interface CreateNoteModalProps {
|
|
workspaces: WorkspaceSummary[];
|
|
defaultWorkspaceId?: string;
|
|
onCreated: () => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function CreateNoteModal({ workspaces, defaultWorkspaceId, onCreated, onClose }: CreateNoteModalProps) {
|
|
const [title, setTitle] = useState("");
|
|
const [body, setBody] = useState("");
|
|
const [workspaceId, setWorkspaceId] = useState(defaultWorkspaceId ?? workspaces[0]?.id ?? "");
|
|
const [tags, setTags] = useState("");
|
|
const [templateId, setTemplateId] = useState<string>("");
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
function applyTemplate(id: string) {
|
|
setTemplateId(id);
|
|
const t = NOTE_TEMPLATES.find((x) => x.id === id);
|
|
if (t) {
|
|
setTitle(t.title);
|
|
setBody(t.body);
|
|
}
|
|
}
|
|
|
|
const canSubmit = title.trim().length > 0 && body.trim().length > 0 && workspaceId.length > 0;
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!canSubmit || saving) return;
|
|
|
|
setSaving(true);
|
|
setError(null);
|
|
|
|
try {
|
|
await createNote({
|
|
id: crypto.randomUUID(),
|
|
workspaceId,
|
|
title: title.trim(),
|
|
body: body.includes("<") && body.includes(">") ? body.trim() : `<p>${body.trim()}</p>`,
|
|
tags: tags
|
|
.split(",")
|
|
.map((t) => t.trim())
|
|
.filter(Boolean),
|
|
sourceType: "manual",
|
|
});
|
|
onCreated();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to create note");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="modal-overlay"
|
|
style={{
|
|
position: "fixed",
|
|
inset: 0,
|
|
background: "var(--nl-overlay-scrim)",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
zIndex: 1000,
|
|
}}
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) onClose();
|
|
}}
|
|
>
|
|
<Card
|
|
padding="lg"
|
|
style={{ width: "100%", maxWidth: 520 }}
|
|
>
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
style={{
|
|
display: "grid",
|
|
gap: "var(--nl-space-4)",
|
|
}}
|
|
>
|
|
<div style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>Create Note</div>
|
|
|
|
{error && <div style={{ color: "var(--nl-danger)", fontSize: "0.875rem" }}>{error}</div>}
|
|
|
|
<label style={{ display: "grid", gap: "var(--nl-space-1)" }}>
|
|
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Template</span>
|
|
<select
|
|
className="input"
|
|
aria-label="Note template"
|
|
value={templateId}
|
|
onChange={(e) => {
|
|
const v = e.target.value;
|
|
if (!v) {
|
|
setTemplateId("");
|
|
return;
|
|
}
|
|
applyTemplate(v);
|
|
}}
|
|
>
|
|
<option value="">Blank</option>
|
|
{NOTE_TEMPLATES.map((t) => (
|
|
<option key={t.id} value={t.id}>
|
|
{t.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label style={{ display: "grid", gap: "var(--nl-space-1)" }}>
|
|
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Workspace</span>
|
|
<select value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} className="input" aria-label="Workspace">
|
|
{workspaces.map((ws) => (
|
|
<option key={ws.id} value={ws.id}>
|
|
{ws.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<label style={{ display: "grid", gap: "var(--nl-space-1)" }}>
|
|
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Title</span>
|
|
<input
|
|
className="input"
|
|
type="text"
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder="Note title"
|
|
aria-label="Note title"
|
|
autoFocus
|
|
/>
|
|
</label>
|
|
|
|
<label style={{ display: "grid", gap: "var(--nl-space-1)" }}>
|
|
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Body</span>
|
|
<textarea
|
|
className="input"
|
|
value={body}
|
|
onChange={(e) => setBody(e.target.value)}
|
|
placeholder="Note content..."
|
|
aria-label="Note body"
|
|
rows={6}
|
|
style={{ resize: "vertical" }}
|
|
/>
|
|
</label>
|
|
|
|
<label style={{ display: "grid", gap: "var(--nl-space-1)" }}>
|
|
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Tags (comma-separated)</span>
|
|
<input
|
|
className="input"
|
|
type="text"
|
|
value={tags}
|
|
onChange={(e) => setTags(e.target.value)}
|
|
placeholder="launch, meeting, review"
|
|
aria-label="Note tags"
|
|
/>
|
|
</label>
|
|
|
|
<div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--nl-space-3)" }}>
|
|
<Button type="button" variant="secondary" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={!canSubmit || saving} loading={saving}>
|
|
Create
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|