diff --git a/components/note-editor/README.md b/components/note-editor/README.md new file mode 100644 index 0000000..1021d94 --- /dev/null +++ b/components/note-editor/README.md @@ -0,0 +1,423 @@ +# File Editor Notes Component + +A Retool Custom Component for creating, editing, grouping, filtering, saving, updating, and deleting notes using dynamic database field mapping. + +This component is designed so every user can connect their own database table, even if their column names are different. + +--- + +## Features + +- Display notes from a database +- Group notes by group/category +- Create new groups +- Add notes inside groups +- Edit existing notes +- Delete selected notes +- Grid and list view +- Filter/search notes +- Dynamic database field mapping +- Dynamic validation limits from Retool Inspector +- Save and update events for database queries +- Exposes selected/draft note data through `notesMeta` + +--- + +## Inspector Inputs + +These inputs are configurable from the Retool Inspector. + +### Database Field Mapping + +Use these to match your database column names. + +| Inspector input | Default value | Purpose | +|---|---:|---| +| `idField` | `id` | Primary key column | +| `titleField` | `title` | Note title column | +| `contentField` | `content` | Note content/body column | +| `dateField` | `date` | Date column | +| `groupField` | `groupName` | Group/category column | + +Example: + +If your database columns are: + +```sql +note_id +note_title +note_body +created_at +category +``` + +Set the inspector values like this: + +```txt +idField = note_id +titleField = note_title +contentField = note_body +dateField = created_at +groupField = category +``` + +--- + +## Dynamic Validation Inputs + +These values are also configurable from the Retool Inspector. + +| Inspector input | Default value | Purpose | +|---|---:|---| +| `maxTitleLength` | `200` | Maximum title characters | +| `maxGroupLength` | `100` | Maximum group name characters | +| `maxContentLength` | `20000` | Maximum note content characters | +| `fontSize` | `14` | Editor font size | + +--- + +## Component State + +### `notesList` + +Pass your database query result into this state. + +Example: + +```js +{{ getNotesQuery.data }} +``` + +The data should be an array of objects. + +Example database result: + +```json +[ + { + "id": 1, + "title": "First note", + "content": "This is my first note", + "date": "27 Apr 2026", + "groupName": "Work" + }, + { + "id": 2, + "title": "Second note", + "content": "This is another note", + "date": "27 Apr 2026", + "groupName": "Personal" + } +] +``` + +--- + +## Output States + +### `selectedId` + +Stores the currently selected note ID. + +### `editorTitle` + +Stores the current editor title. + +### `editorText` + +Stores the current editor content. + +### `notesMeta` + +Main output object used for save, update, and delete queries. + +It includes: + +```js +notesMeta.draft +notesMeta.savedNote +notesMeta.pendingSave +notesMeta.removeCandidate +notesMeta.validation +``` + +--- + +## Events + +The component exposes these Retool events: + +| Event | Purpose | +|---|---| +| `saveClick` | Triggered when saving a new note | +| `updateClick` | Triggered when updating an existing note | +| `selectedNoteRemoveConfirmClick` | Triggered when deleting a note | + +--- + +# Database Setup + +## Example Table + +You can create a notes table like this: + +```sql +CREATE TABLE notes ( + id SERIAL PRIMARY KEY, + title TEXT, + content TEXT, + date TEXT, + groupName TEXT +); +``` + +You can also use different column names. Just update the inspector field mapping. + +--- + +## Get Notes Query + +Create a Retool query named: + +```txt +getNotesQuery +``` + +Example SQL: + +```sql +SELECT * +FROM notes +ORDER BY id DESC; +``` + +Then pass this query data into the component: + +```js +{{ getNotesQuery.data }} +``` + +--- + +## Save New Note Query + +Create a query named: + +```txt +saveNoteQuery +``` + +Example SQL: + +```sql +INSERT INTO notes ( + title, + content, + date, + groupName +) +VALUES ( + {{ notesComponent.notesMeta.savedNote.title }}, + {{ notesComponent.notesMeta.savedNote.content }}, + {{ notesComponent.notesMeta.savedNote.date }}, + {{ notesComponent.notesMeta.savedNote.groupName }} +); +``` + +After success, run: + +```txt +getNotesQuery.trigger() +``` + +Connect this query to the component's `saveClick` event. + +--- + +## Update Existing Note Query + +Create a query named: + +```txt +updateNoteQuery +``` + +Example SQL: + +```sql +UPDATE notes +SET + title = {{ notesComponent.notesMeta.savedNote.title }}, + content = {{ notesComponent.notesMeta.savedNote.content }}, + date = {{ notesComponent.notesMeta.savedNote.date }}, + groupName = {{ notesComponent.notesMeta.savedNote.groupName }} +WHERE id = {{ notesComponent.notesMeta.savedNote.id }}; +``` + +After success, run: + +```txt +getNotesQuery.trigger() +``` + +Connect this query to the component's `updateClick` event. + +--- + +## Delete Note Query + +Create a query named: + +```txt +deleteNoteQuery +``` + +Example SQL: + +```sql +DELETE FROM notes +WHERE id = {{ notesComponent.notesMeta.removeCandidate.id }}; +``` + +After success, run: + +```txt +getNotesQuery.trigger() +``` + +Connect this query to the component's `selectedNoteRemoveConfirmClick` event. + +--- + +# Using Custom Database Columns + +If your table uses custom column names, for example: + +```sql +CREATE TABLE user_notes ( + note_id SERIAL PRIMARY KEY, + note_title TEXT, + note_text TEXT, + created_date TEXT, + folder_name TEXT +); +``` + +Set the inspector values: + +```txt +idField = note_id +titleField = note_title +contentField = note_text +dateField = created_date +groupField = folder_name +``` + +Then your insert query should use those same fields: + +```sql +INSERT INTO user_notes ( + note_title, + note_text, + created_date, + folder_name +) +VALUES ( + {{ notesComponent.notesMeta.savedNote.note_title }}, + {{ notesComponent.notesMeta.savedNote.note_text }}, + {{ notesComponent.notesMeta.savedNote.created_date }}, + {{ notesComponent.notesMeta.savedNote.folder_name }} +); +``` + +Update query: + +```sql +UPDATE user_notes +SET + note_title = {{ notesComponent.notesMeta.savedNote.note_title }}, + note_text = {{ notesComponent.notesMeta.savedNote.note_text }}, + created_date = {{ notesComponent.notesMeta.savedNote.created_date }}, + folder_name = {{ notesComponent.notesMeta.savedNote.folder_name }} +WHERE note_id = {{ notesComponent.notesMeta.savedNote.note_id }}; +``` + +Delete query: + +```sql +DELETE FROM user_notes +WHERE note_id = {{ notesComponent.notesMeta.removeCandidate.note_id }}; +``` + +--- + +# How to Use + +1. Add the custom component to Retool. +2. Paste the component code. +3. Create a database table for notes. +4. Create a query to fetch notes. +5. Pass the query result into `notesList`. +6. Configure field names in the inspector. +7. Create insert, update, and delete queries. +8. Connect those queries to the component events. +9. Refresh the notes query after each save, update, or delete. + +--- + +# User Interaction + +## Create Group + +Enter a group name and click: + +```txt ++ New Group +``` + +This opens the editor for a new note inside that group. + +## Add Note + +Click the `+` button inside any group. + +## Select Note + +Single-click a note card. + +## Edit Note + +Double-click a note card. + +## Delete Note + +Select a note, then click the `×` button. + +## Save Note + +Click the `Save` button inside the editor. + +--- + +# Validation + +The component validates: + +- Group name is required +- Title or content is required +- Title cannot exceed `maxTitleLength` +- Group name cannot exceed `maxGroupLength` +- Content cannot exceed `maxContentLength` + +All limits can be changed from the Retool Inspector. + +--- + +# Notes + +- New notes use `id: null` before saving. +- Existing notes use their database ID. +- The component does not directly write to the database. +- Retool queries handle insert, update, and delete actions. +- `notesMeta.savedNote` is the main object used for save and update. +- `notesMeta.removeCandidate` is used for delete. diff --git a/components/note-editor/cover.png b/components/note-editor/cover.png new file mode 100644 index 0000000..62bcb00 Binary files /dev/null and b/components/note-editor/cover.png differ diff --git a/components/note-editor/metadata.json b/components/note-editor/metadata.json new file mode 100644 index 0000000..5f562b9 --- /dev/null +++ b/components/note-editor/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "file-editor-notes", + "title": "File Editor Notes", + "author": "@widlestudiollp", + "shortDescription": "A flexible notes management component that supports grouping, editing, filtering, and dynamic database field mapping with full CRUD integration in Retool.", + "tags": [ + "Notes", + "Editor", + "CRUD", + "React", + "Retool", + "Data Management", + "Dashboard", + "Dynamic Fields", + "Productivity", + "UI Component" + ] +} \ No newline at end of file diff --git a/components/note-editor/package.json b/components/note-editor/package.json new file mode 100644 index 0000000..642206a --- /dev/null +++ b/components/note-editor/package.json @@ -0,0 +1,48 @@ +{ + "name": "custom-component-collection", + "version": "0.1.0", + "private": true, + "dependencies": { + "@tryretool/custom-component-support": "latest", + "qrcode.react": "^4.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "scripts": { + "dev": "npx retool-ccl dev", + "deploy": "npx retool-ccl deploy", + "test": "vitest" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/react": "^18.2.55", + "@typescript-eslint/eslint-plugin": "^7.3.1", + "@typescript-eslint/parser": "^7.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.1", + "postcss-modules": "^6.0.0", + "prettier": "^3.0.3", + "vitest": "^4.0.17" + }, + "retoolCustomComponentLibraryConfig": { + "name": "Notes", + "label": "Notes", + "description": "File editor", + "entryPoint": "src/index.tsx", + "outputPath": "dist" + } +} \ No newline at end of file diff --git a/components/note-editor/src/component/index.tsx b/components/note-editor/src/component/index.tsx new file mode 100644 index 0000000..8bd88ef --- /dev/null +++ b/components/note-editor/src/component/index.tsx @@ -0,0 +1,1176 @@ +import React, { FC, useEffect, useMemo, useState } from "react"; +import { Retool } from "@tryretool/custom-component-support"; + +type Note = { + id: string; + title: string; + content: string; + date: string; + groupName: string; +}; + +type ViewMode = "grid" | "list"; + +const TEMP_NOTE_ID = "__new__"; + +function formatDate(date = new Date()) { + const day = String(date.getDate()).padStart(2, "0"); + const month = date.toLocaleString("en-US", { month: "short" }); + const year = date.getFullYear(); + return `${day} ${month} ${year}`; +} + +function safeText(value: any, fallback = "") { + const text = String(value ?? "").trim(); + return text || fallback; +} + +function getSafeLimit(value: number, fallback: number) { + return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback; +} + +export const FileEditorNotes: FC = () => { + Retool.useComponentSettings({ + defaultHeight: 10, + defaultWidth: 14, + }); + + const [notesList] = Retool.useStateArray({ + name: "notesList", + initialValue: [], + }); + + const [idField] = Retool.useStateString({ + name: "idField", + initialValue: "id", + inspector: "text", + label: "ID field", + }); + + const [titleField] = Retool.useStateString({ + name: "titleField", + initialValue: "title", + inspector: "text", + label: "Title field", + }); + + const [contentField] = Retool.useStateString({ + name: "contentField", + initialValue: "content", + inspector: "text", + label: "Content field", + }); + + const [dateField] = Retool.useStateString({ + name: "dateField", + initialValue: "date", + inspector: "text", + label: "Date field", + }); + + const [groupField] = Retool.useStateString({ + name: "groupField", + initialValue: "groupName", + inspector: "text", + label: "Group field", + }); + + const [fontSize] = Retool.useStateNumber({ + name: "fontSize", + initialValue: 14, + inspector: "text", + label: "Font size", + }); + + const [maxTitleLengthInput] = Retool.useStateNumber({ + name: "maxTitleLength", + initialValue: 200, + inspector: "text", + label: "Maximum title characters", + }); + + const [maxGroupLengthInput] = Retool.useStateNumber({ + name: "maxGroupLength", + initialValue: 100, + inspector: "text", + label: "Maximum group characters", + }); + + const [maxContentLengthInput] = Retool.useStateNumber({ + name: "maxContentLength", + initialValue: 20000, + inspector: "text", + label: "Maximum content characters", + }); + + const maxTitleLength = getSafeLimit(maxTitleLengthInput, 200); + const maxGroupLength = getSafeLimit(maxGroupLengthInput, 100); + const maxContentLength = getSafeLimit(maxContentLengthInput, 20000); + + const [selectedId, setSelectedId] = Retool.useStateString({ + name: "selectedId", + initialValue: "", + inspector: "hidden", + }); + + const [, setEditorTitle] = Retool.useStateString({ + name: "editorTitle", + initialValue: "", + inspector: "hidden", + }); + + const [, setEditorText] = Retool.useStateString({ + name: "editorText", + initialValue: "", + inspector: "hidden", + }); + + const [, setNotesMeta] = Retool.useStateObject({ + name: "notesMeta", + initialValue: {}, + inspector: "hidden", + }); + + const saveClick = Retool.useEventCallback({ name: "saveClick" }); + const updateClick = Retool.useEventCallback({ name: "updateClick" }); + + const selectedNoteRemoveConfirmClick = Retool.useEventCallback({ + name: "selectedNoteRemoveConfirmClick", + }); + + const [selectedGroupName, setSelectedGroupName] = useState(""); + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + const [newGroupName, setNewGroupName] = useState(""); + const [isEditorOpen, setIsEditorOpen] = useState(false); + const [viewMode, setViewMode] = useState("grid"); + const [filterText, setFilterText] = useState(""); + + const [newGroupError, setNewGroupError] = useState(""); + const [editorGroupError, setEditorGroupError] = useState(""); + const [noteError, setNoteError] = useState(""); + + const dbNotes = useMemo(() => { + const rows = Array.isArray(notesList) ? notesList : []; + + return rows + .map((row: any, index: number) => ({ + id: String( + row?.[idField || "id"] ?? row?.id ?? row?.ID ?? `note-${index}` + ), + title: String( + row?.[titleField || "title"] ?? row?.title ?? row?.TITLE ?? "" + ), + content: String( + row?.[contentField || "content"] ?? + row?.content ?? + row?.CONTENT ?? + "" + ), + date: String( + row?.[dateField || "date"] ?? row?.date ?? row?.DATE ?? formatDate() + ), + groupName: safeText( + row?.[groupField || "groupName"] ?? + row?.groupName ?? + row?.GROUPNAME ?? + row?.groupname, + "" + ), + })) + .filter((note) => note.groupName); + }, [notesList, idField, titleField, contentField, dateField, groupField]); + + const filteredDbNotes = useMemo(() => { + const q = filterText.trim().toLowerCase(); + if (!q) return dbNotes; + + return dbNotes.filter((note) => { + return ( + note.title.toLowerCase().includes(q) || + note.content.toLowerCase().includes(q) || + note.groupName.toLowerCase().includes(q) || + note.date.toLowerCase().includes(q) + ); + }); + }, [dbNotes, filterText]); + + const groupNames = useMemo(() => { + const set = new Set(); + + filteredDbNotes.forEach((note) => { + if (note.groupName) set.add(note.groupName); + }); + + return Array.from(set); + }, [filteredDbNotes]); + + const selectedNote = useMemo(() => { + if (selectedId === TEMP_NOTE_ID) { + return { + id: TEMP_NOTE_ID, + title, + content, + date: formatDate(), + groupName: selectedGroupName, + }; + } + + return dbNotes.find((note) => note.id === selectedId) || null; + }, [selectedId, dbNotes, title, content, selectedGroupName]); + + const groupedNotes = useMemo(() => { + const map = new Map(); + + groupNames.forEach((group) => { + if (group) map.set(group, []); + }); + + filteredDbNotes.forEach((note) => { + if (!note.groupName) return; + + if (!map.has(note.groupName)) { + map.set(note.groupName, []); + } + + map.get(note.groupName)!.push(note); + }); + + if (selectedId === TEMP_NOTE_ID && selectedGroupName.trim()) { + const group = selectedGroupName.trim(); + + if (!map.has(group)) { + map.set(group, []); + } + + map.get(group)!.unshift({ + id: TEMP_NOTE_ID, + title, + content, + date: formatDate(), + groupName: group, + }); + } + + return Array.from(map.entries()).map(([groupName, notes]) => ({ + groupName, + notes, + })); + }, [ + filteredDbNotes, + groupNames, + selectedId, + selectedGroupName, + title, + content, + ]); + + useEffect(() => { + setEditorTitle(title); + }, [title, setEditorTitle]); + + useEffect(() => { + setEditorText(content); + }, [content, setEditorText]); + + useEffect(() => { + setNotesMeta({ + selectedId, + selectedGroupName: selectedGroupName.trim(), + viewMode, + filterText, + draft: { + [idField || "id"]: selectedId, + [titleField || "title"]: title, + [contentField || "content"]: content, + [dateField || "date"]: selectedNote?.date || formatDate(), + [groupField || "groupName"]: selectedGroupName.trim(), + + id: selectedId, + title, + content, + date: selectedNote?.date || formatDate(), + groupName: selectedGroupName.trim(), + isNew: selectedId === TEMP_NOTE_ID, + }, + validation: { + maxTitleLength, + maxGroupLength, + maxContentLength, + titleLength: title.length, + groupLength: selectedGroupName.length, + contentLength: content.length, + }, + totalNotes: dbNotes.length, + filteredNotes: filteredDbNotes.length, + updatedAt: new Date().toISOString(), + }); + }, [ + selectedId, + selectedGroupName, + title, + content, + selectedNote, + dbNotes.length, + filteredDbNotes.length, + viewMode, + filterText, + idField, + titleField, + contentField, + dateField, + groupField, + setNotesMeta, + maxTitleLength, + maxGroupLength, + maxContentLength, + ]); + + const validateNote = () => { + const cleanTitle = title.trim(); + const cleanGroup = selectedGroupName.trim(); + const cleanContent = content.trim(); + + let hasError = false; + + if (!cleanGroup) { + setEditorGroupError("Group name is required"); + hasError = true; + } else if (cleanGroup.length > maxGroupLength) { + setEditorGroupError( + `Group name cannot exceed ${maxGroupLength} characters` + ); + hasError = true; + } else { + setEditorGroupError(""); + } + + if (cleanTitle.length > maxTitleLength) { + setNoteError(`Title cannot exceed ${maxTitleLength} characters`); + hasError = true; + } else if (content.length > maxContentLength) { + setNoteError( + `Note is too large. Maximum ${maxContentLength.toLocaleString()} characters allowed.` + ); + hasError = true; + } else if (!cleanTitle && !cleanContent) { + setNoteError("Add a title or note content"); + hasError = true; + } else { + setNoteError(""); + } + + return !hasError; + }; + + const handleCreateGroup = () => { + const group = newGroupName.trim(); + + if (!group) { + setNewGroupError("Group name is required"); + return; + } + + if (group.length > maxGroupLength) { + setNewGroupError( + `Group name cannot exceed ${maxGroupLength} characters` + ); + return; + } + + setNewGroupError(""); + setEditorGroupError(""); + setNoteError(""); + + setSelectedId(TEMP_NOTE_ID); + setSelectedGroupName(group); + setTitle(""); + setContent(""); + setNewGroupName(""); + setIsEditorOpen(true); + }; + + const handleAddNewNote = (groupName: string) => { + const group = groupName.trim(); + + if (!group) return; + + setEditorGroupError(""); + setNoteError(""); + + setSelectedId(TEMP_NOTE_ID); + setSelectedGroupName(group); + setTitle(""); + setContent(""); + setIsEditorOpen(true); + }; + + const handleSelectOnly = (note: Note) => { + setSelectedId(note.id); + setSelectedGroupName(note.groupName); + }; + + const handleOpenNote = (note: Note) => { + setEditorGroupError(""); + setNoteError(""); + + setSelectedId(note.id); + setSelectedGroupName(note.groupName); + setTitle(note.title); + setContent(note.content); + setIsEditorOpen(true); + }; + + const handleSave = () => { + if (!validateNote()) return; + + const cleanTitle = title.trim(); + const groupName = selectedGroupName.trim(); + + const isNew = selectedId === TEMP_NOTE_ID || !selectedId; + const noteId = isNew ? null : selectedId; + const noteDate = selectedNote?.date || formatDate(); + + const savedNote = { + [idField || "id"]: noteId, + [titleField || "title"]: cleanTitle || "Untitled", + [contentField || "content"]: content, + [dateField || "date"]: noteDate, + [groupField || "groupName"]: groupName, + + id: noteId, + title: cleanTitle || "Untitled", + content, + date: noteDate, + groupName, + }; + + setEditorTitle(cleanTitle || "Untitled"); + setEditorText(content); + + setNotesMeta({ + savedNote, + pendingSave: { + ...savedNote, + isNew, + isUpdate: !isNew, + }, + validation: { + isValid: true, + maxTitleLength, + maxGroupLength, + maxContentLength, + contentLength: content.length, + titleLength: cleanTitle.length, + groupLength: groupName.length, + }, + updatedAt: new Date().toISOString(), + }); + + window.setTimeout(() => { + if (isNew) { + saveClick(); + + setSelectedId(""); + setTitle(""); + setContent(""); + setIsEditorOpen(false); + } else { + updateClick(); + + setIsEditorOpen(false); + } + }, 150); + }; + + const handleRemoveCard = (note: Note) => { + if (note.id === TEMP_NOTE_ID) { + setSelectedId(""); + setTitle(""); + setContent(""); + setSelectedGroupName(""); + setIsEditorOpen(false); + return; + } + + setNotesMeta({ + removeCandidate: { + [idField || "id"]: note.id, + [titleField || "title"]: note.title, + [contentField || "content"]: note.content, + [dateField || "date"]: note.date, + [groupField || "groupName"]: note.groupName, + + ...note, + }, + updatedAt: new Date().toISOString(), + }); + + selectedNoteRemoveConfirmClick(); + + if (selectedId === note.id) { + setSelectedId(""); + setTitle(""); + setContent(""); + setSelectedGroupName(""); + setIsEditorOpen(false); + } + }; + + const handleCloseModal = () => { + setEditorGroupError(""); + setNoteError(""); + setIsEditorOpen(false); + }; + + const renderNoteCard = (note: Note) => { + const isSelected = note.id === selectedId; + + return ( +
+ {isSelected && ( + + )} + + +
+ ); + }; + + const renderNoteListItem = (note: Note) => { + const isSelected = note.id === selectedId; + + return ( +
+ {isSelected && ( + + )} + + +
+ ); + }; + + return ( +
+
+
+
+
+ { + setNewGroupName(e.target.value); + + if (e.target.value.trim()) { + setNewGroupError(""); + } + }} + placeholder="New group name" + style={{ + width: 220, + background: "#111826", + border: `1px solid ${newGroupError ? "#ef4444" : "#233045" + }`, + borderRadius: 10, + color: "#f3f4f6", + padding: "9px 10px", + outline: "none", + fontSize: 14, + }} + /> + + +
+ + {newGroupError && ( +
+ {newGroupError} +
+ )} +
+ +
+ setFilterText(e.target.value)} + placeholder="Filter notes..." + style={{ + width: 220, + background: "#111826", + border: "1px solid #233045", + borderRadius: 10, + color: "#f3f4f6", + padding: "9px 10px", + outline: "none", + fontSize: 14, + }} + /> + +
+ + + +
+
+
+ + {groupedNotes.length === 0 && ( +
+ {filterText.trim() + ? "No notes match your filter." + : "Create a group first, then add notes inside it."} +
+ )} + + {groupedNotes.map((group) => ( +
+
+
+ {group.groupName} +
+ + {viewMode === "list" && !filterText.trim() && ( + + )} +
+ + {viewMode === "grid" ? ( +
+ {group.notes.map(renderNoteCard)} + + {!filterText.trim() && ( + + )} +
+ ) : ( +
+ {group.notes.map(renderNoteListItem)} +
+ )} +
+ ))} +
+ + {isEditorOpen && ( +
+
+ {selectedNote?.date || formatDate()} + + +
+ +
+ { + setSelectedGroupName(e.target.value); + + if (e.target.value.trim()) { + setEditorGroupError(""); + } + }} + placeholder="Group name" + style={{ + width: "100%", + marginBottom: editorGroupError ? 6 : 14, + background: "#111826", + border: `1px solid ${editorGroupError ? "#ef4444" : "#233045" + }`, + borderRadius: 10, + color: "#f3f4f6", + padding: "9px 10px", + outline: "none", + fontSize: 14, + boxSizing: "border-box", + }} + /> + + {editorGroupError && ( +
+ {editorGroupError} +
+ )} + + { + setTitle(e.target.value); + + if (e.target.value.trim() || content.trim()) { + setNoteError(""); + } + }} + placeholder="Title" + style={{ + width: "100%", + background: "transparent", + border: "none", + outline: "none", + color: "#f3f4f6", + fontSize: 28, + fontWeight: 800, + }} + /> +
+ +