Skip to content

Commit 4179ef2

Browse files
faut vraiment "SINSIPIRER" de notions :
Notion est un outil d’écriture
1 parent 3ad87aa commit 4179ef2

File tree

1 file changed

+131
-67
lines changed

1 file changed

+131
-67
lines changed

src/components/project/DocumentEditor.tsx

Lines changed: 131 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import { cn } from '@/lib/utils';
2020
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
2121
import type { Project, GlobalDocument } from '@/types';
2222

23-
2423
const documentEditorFormSchema = z.object({
2524
title: z.string().min(1, 'Title is required.').max(255),
2625
content: z.string().optional(),
@@ -45,9 +44,76 @@ interface DocumentEditorProps {
4544
interface MarkdownTool {
4645
label: string;
4746
icon: React.ElementType;
48-
action: () => void;
47+
action: (textarea: HTMLTextAreaElement) => void;
4948
}
5049

50+
// Notion-like Block component
51+
const ContentBlock = ({
52+
blockContent,
53+
onUpdate,
54+
onFocus,
55+
}: {
56+
blockContent: string;
57+
onUpdate: (newContent: string) => void;
58+
onFocus: () => void;
59+
}) => {
60+
const [isEditing, setIsEditing] = useState(false);
61+
const [localContent, setLocalContent] = useState(blockContent);
62+
const textareaRef = useRef<HTMLTextAreaElement>(null);
63+
64+
useEffect(() => {
65+
setLocalContent(blockContent);
66+
}, [blockContent]);
67+
68+
useEffect(() => {
69+
if (isEditing && textareaRef.current) {
70+
textareaRef.current.focus();
71+
textareaRef.current.style.height = 'auto';
72+
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
73+
}
74+
}, [isEditing, localContent]);
75+
76+
const handleBlur = () => {
77+
setIsEditing(false);
78+
if (localContent !== blockContent) {
79+
onUpdate(localContent);
80+
}
81+
};
82+
83+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
84+
if (e.key === 'Enter' && !e.shiftKey) {
85+
e.preventDefault();
86+
handleBlur();
87+
}
88+
}
89+
90+
if (isEditing) {
91+
return (
92+
<Textarea
93+
ref={textareaRef}
94+
value={localContent}
95+
onChange={(e) => setLocalContent(e.target.value)}
96+
onBlur={handleBlur}
97+
onKeyDown={handleKeyDown}
98+
onFocus={onFocus}
99+
className="w-full p-0 border-0 focus-visible:ring-0 focus-visible:ring-offset-0 bg-transparent resize-none font-mono text-sm"
100+
placeholder="Type '/' for commands..."
101+
/>
102+
);
103+
}
104+
105+
return (
106+
<div
107+
className="prose dark:prose-invert max-w-none min-h-[24px] cursor-text"
108+
onClick={() => setIsEditing(true)}
109+
>
110+
<ReactMarkdown remarkPlugins={[remarkGfm]}>
111+
{localContent || '​'}
112+
</ReactMarkdown>
113+
</div>
114+
);
115+
};
116+
51117
export function DocumentEditor({
52118
initialData,
53119
onSave,
@@ -61,15 +127,13 @@ export function DocumentEditor({
61127
}: DocumentEditorProps) {
62128
const { toast } = useToast();
63129
const [isSubmitting, setIsSubmitting] = useState(false);
64-
const textareaRef = useRef<HTMLTextAreaElement>(null);
130+
const mainEditorRef = useRef<HTMLDivElement>(null);
131+
const [activeTextarea, setActiveTextarea] = useState<HTMLTextAreaElement | null>(null);
65132

66133
const [isAiDialogOpen, setIsAiDialogOpen] = useState(false);
67134
const [aiPrompt, setAiPrompt] = useState('');
68135
const [isAiGenerating, setIsAiGenerating] = useState(false);
69136

70-
// WYSIWYG-like state
71-
const [isEditingContent, setIsEditingContent] = useState(false);
72-
73137
const form = useForm<DocumentEditorFormValues>({
74138
resolver: zodResolver(documentEditorFormSchema),
75139
defaultValues: {
@@ -81,7 +145,7 @@ export function DocumentEditor({
81145
});
82146

83147
const contentValue = form.watch('content');
84-
const contentField = form.register('content');
148+
const contentBlocks = (contentValue || '').split('\n\n');
85149

86150
useEffect(() => {
87151
form.reset({
@@ -92,14 +156,20 @@ export function DocumentEditor({
92156
});
93157
}, [initialData, form]);
94158

159+
const updateContentBlock = (index: number, newContent: string) => {
160+
const newBlocks = [...contentBlocks];
161+
newBlocks[index] = newContent;
162+
form.setValue('content', newBlocks.join('\n\n'), { shouldDirty: true });
163+
};
164+
165+
95166
const applyMarkdownSyntax = (
96167
syntaxStart: string,
97168
syntaxEnd: string = '',
98-
_isBlock: boolean = false,
99169
prefixEachLine: boolean = false
100170
) => {
101-
if (!textareaRef.current) return;
102-
const { selectionStart, selectionEnd, value } = textareaRef.current;
171+
if (!activeTextarea) return;
172+
const { selectionStart, selectionEnd, value } = activeTextarea;
103173
const selectedText = value.substring(selectionStart, selectionEnd);
104174
let newText = '';
105175

@@ -113,42 +183,43 @@ export function DocumentEditor({
113183
}
114184

115185
const newValue = value.substring(0, selectionStart) + newText + value.substring(selectionEnd);
116-
form.setValue('content', newValue, { shouldValidate: true, shouldDirty: true });
186+
187+
// This is tricky. We need to find which block this textarea belongs to.
188+
// For now, let's assume one active textarea. The parent component will handle state update.
189+
const event = new Event('input', { bubbles: true });
190+
activeTextarea.value = newValue;
191+
activeTextarea.dispatchEvent(event);
192+
117193

118194
setTimeout(() => {
119-
if (textareaRef.current) {
120-
textareaRef.current.focus();
195+
if (activeTextarea) {
196+
activeTextarea.focus();
121197
if (selectedText) {
122-
if (prefixEachLine) {
123-
textareaRef.current.selectionStart = selectionStart;
124-
textareaRef.current.selectionEnd = selectionEnd + (syntaxStart.length * selectedText.split('\n').length) ;
125-
} else {
126-
textareaRef.current.selectionStart = selectionStart + syntaxStart.length;
127-
textareaRef.current.selectionEnd = selectionEnd + syntaxStart.length;
128-
}
198+
activeTextarea.selectionStart = selectionStart + syntaxStart.length;
199+
activeTextarea.selectionEnd = selectionEnd + syntaxStart.length;
129200
} else {
130-
textareaRef.current.selectionStart = selectionStart + syntaxStart.length;
131-
textareaRef.current.selectionEnd = selectionStart + syntaxStart.length;
201+
activeTextarea.selectionStart = selectionStart + syntaxStart.length;
202+
activeTextarea.selectionEnd = selectionStart + syntaxStart.length;
132203
}
133204
}
134205
}, 0);
135206
};
136-
207+
137208
const markdownTools: MarkdownTool[] = [
138-
{ label: 'H1', icon: Heading1, action: () => applyMarkdownSyntax('# ', '', false, true) },
139-
{ label: 'H2', icon: Heading2, action: () => applyMarkdownSyntax('## ', '', false, true) },
140-
{ label: 'H3', icon: Heading3, action: () => applyMarkdownSyntax('### ', '', false, true) },
141-
{ label: 'Bold', icon: Bold, action: () => applyMarkdownSyntax('**', '**') },
142-
{ label: 'Italic', icon: Italic, action: () => applyMarkdownSyntax('*', '*') },
143-
{ label: 'Strikethrough', icon: Strikethrough, action: () => applyMarkdownSyntax('~~', '~~') },
144-
{ label: 'Unordered List', icon: List, action: () => applyMarkdownSyntax('- ', '', false, true) },
145-
{ label: 'Ordered List', icon: ListOrdered, action: () => applyMarkdownSyntax('1. ', '', false, true) },
146-
{ label: 'Link', icon: LinkIcon, action: () => applyMarkdownSyntax('[', '](url)') },
147-
{ label: 'Image', icon: ImageIcon, action: () => applyMarkdownSyntax('![alt text](', 'image_url)') },
148-
{ label: 'Code Block', icon: SquareCode, action: () => applyMarkdownSyntax('\n```\n', '\n```\n', true) },
149-
{ label: 'Inline Code', icon: Code2, action: () => applyMarkdownSyntax('`', '`') },
150-
{ label: 'Quote', icon: Quote, action: () => applyMarkdownSyntax('> ', '', false, true) },
151-
{ label: 'Horizontal Line', icon: Minus, action: () => applyMarkdownSyntax('\n---\n', '', true) },
209+
{ label: 'H1', icon: Heading1, action: (ta) => applyMarkdownSyntax('# ', '', true) },
210+
{ label: 'H2', icon: Heading2, action: (ta) => applyMarkdownSyntax('## ', '', true) },
211+
{ label: 'H3', icon: Heading3, action: (ta) => applyMarkdownSyntax('### ', '', true) },
212+
{ label: 'Bold', icon: Bold, action: (ta) => applyMarkdownSyntax('**', '**') },
213+
{ label: 'Italic', icon: Italic, action: (ta) => applyMarkdownSyntax('*', '*') },
214+
{ label: 'Strikethrough', icon: Strikethrough, action: (ta) => applyMarkdownSyntax('~~', '~~') },
215+
{ label: 'Unordered List', icon: List, action: (ta) => applyMarkdownSyntax('- ', '', true) },
216+
{ label: 'Ordered List', icon: ListOrdered, action: (ta) => applyMarkdownSyntax('1. ', '', true) },
217+
{ label: 'Link', icon: LinkIcon, action: (ta) => applyMarkdownSyntax('[', '](url)') },
218+
{ label: 'Image', icon: ImageIcon, action: (ta) => applyMarkdownSyntax('![alt text](', 'image_url)') },
219+
{ label: 'Code Block', icon: SquareCode, action: (ta) => applyMarkdownSyntax('\n```\n', '\n```\n') },
220+
{ label: 'Inline Code', icon: Code2, action: (ta) => applyMarkdownSyntax('`', '`') },
221+
{ label: 'Quote', icon: Quote, action: (ta) => applyMarkdownSyntax('> ', '', true) },
222+
{ label: 'Horizontal Line', icon: Minus, action: (ta) => applyMarkdownSyntax('\n---\n', '') },
152223
];
153224

154225
const onSubmit = async (data: DocumentEditorFormValues) => {
@@ -198,7 +269,6 @@ export function DocumentEditor({
198269
}
199270
};
200271

201-
202272
return (
203273
<>
204274
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
@@ -228,16 +298,14 @@ export function DocumentEditor({
228298
<Label htmlFor="ai-prompt">Your Prompt</Label>
229299
<Textarea
230300
id="ai-prompt"
231-
placeholder="e.g., 'Create a getting started guide for a new SaaS product focusing on user onboarding and key features like X, Y, and Z.'"
301+
placeholder="e.g., 'Create a getting started guide for a new SaaS product...'"
232302
value={aiPrompt}
233303
onChange={(e) => setAiPrompt(e.target.value)}
234304
rows={5}
235305
/>
236306
</div>
237307
<DialogFooter>
238-
<DialogClose asChild>
239-
<Button type="button" variant="ghost" disabled={isAiGenerating}>Cancel</Button>
240-
</DialogClose>
308+
<DialogClose asChild><Button type="button" variant="ghost" disabled={isAiGenerating}>Cancel</Button></DialogClose>
241309
<Button type="button" onClick={handleAiGenerate} disabled={isAiGenerating || !aiPrompt.trim()}>
242310
{isAiGenerating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
243311
Generate Content
@@ -278,7 +346,7 @@ export function DocumentEditor({
278346
name="linkedProjectUuid"
279347
control={form.control}
280348
render={({ field }) => (
281-
<Select onValueChange={field.onChange} defaultValue={field.value} value={field.value}>
349+
<Select onValueChange={field.onChange} value={field.value || 'none'}>
282350
<SelectTrigger>
283351
<SelectValue placeholder="Select a project to link..." />
284352
</SelectTrigger>
@@ -305,40 +373,35 @@ export function DocumentEditor({
305373
type="button"
306374
variant="outline"
307375
size="icon"
308-
onClick={tool.action}
376+
onClick={() => activeTextarea && tool.action(activeTextarea)}
309377
title={tool.label}
310378
className="h-8 w-8"
379+
disabled={!activeTextarea}
311380
>
312381
<tool.icon className="h-4 w-4" />
313382
</Button>
314383
))}
315384
</div>
316385
<div
317-
className={cn("border rounded-b-md p-4 min-h-[450px] bg-background focus-within:ring-2 focus-within:ring-ring")}
318-
onClick={() => {
319-
setIsEditingContent(true);
320-
setTimeout(() => textareaRef.current?.focus(), 0);
386+
ref={mainEditorRef}
387+
className="border rounded-b-md p-4 min-h-[450px] bg-background focus-within:ring-2 focus-within:ring-ring space-y-2"
388+
onFocus={(e) => {
389+
if (e.target.tagName === 'TEXTAREA') {
390+
setActiveTextarea(e.target as HTMLTextAreaElement);
391+
}
321392
}}
322393
>
323-
{isEditingContent ? (
324-
<Textarea
325-
id="content"
326-
{...contentField}
327-
ref={(e) => {
328-
contentField.ref(e);
329-
textareaRef.current = e;
330-
}}
331-
className="w-full h-full min-h-[450px] p-0 border-0 focus-visible:ring-0 focus-visible:ring-offset-0 bg-transparent resize-none font-mono text-sm"
332-
placeholder="Write your content here..."
333-
onBlur={() => setIsEditingContent(false)}
334-
/>
335-
) : (
336-
<div className="prose dark:prose-invert max-w-none">
337-
<ReactMarkdown remarkPlugins={[remarkGfm]}>
338-
{contentValue || '*Click to start writing*'}
339-
</ReactMarkdown>
340-
</div>
341-
)}
394+
{contentBlocks.map((block, index) => (
395+
<ContentBlock
396+
key={index}
397+
blockContent={block}
398+
onUpdate={(newBlockContent) => updateContentBlock(index, newBlockContent)}
399+
onFocus={() => {
400+
const textarea = mainEditorRef.current?.querySelectorAll('textarea')[index];
401+
if(textarea) setActiveTextarea(textarea);
402+
}}
403+
/>
404+
))}
342405
</div>
343406
</div>
344407

@@ -357,3 +420,4 @@ export function DocumentEditor({
357420
</>
358421
);
359422
}
423+

0 commit comments

Comments
 (0)