@valbuild/ui 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/src/assets/icons/Bold.tsx +23 -0
  3. package/src/assets/icons/Chevron.tsx +28 -0
  4. package/src/assets/icons/FontColor.tsx +30 -0
  5. package/src/assets/icons/ImageIcon.tsx +21 -0
  6. package/src/assets/icons/Italic.tsx +24 -0
  7. package/src/assets/icons/Strikethrough.tsx +22 -0
  8. package/src/assets/icons/Underline.tsx +22 -0
  9. package/src/assets/icons/Undo.tsx +20 -0
  10. package/src/components/Button.tsx +58 -0
  11. package/src/components/Checkbox.tsx +51 -0
  12. package/src/components/Dropdown.tsx +92 -0
  13. package/src/components/EditButton.tsx +10 -0
  14. package/src/components/ErrorText.tsx +3 -0
  15. package/src/components/RichTextEditor/ContentEditable.tsx +9 -0
  16. package/src/components/RichTextEditor/Nodes/ImageNode.tsx +117 -0
  17. package/src/components/RichTextEditor/Plugins/AutoFocus.tsx +12 -0
  18. package/src/components/RichTextEditor/Plugins/ImagePlugin.tsx +46 -0
  19. package/src/components/RichTextEditor/Plugins/Toolbar.tsx +381 -0
  20. package/src/components/RichTextEditor/RichTextEditor.tsx +176 -0
  21. package/src/components/UploadModal.tsx +109 -0
  22. package/src/components/ValOverlay.tsx +41 -0
  23. package/src/components/ValWindow.stories.tsx +182 -0
  24. package/src/components/ValWindow.tsx +137 -0
  25. package/src/components/forms/Form.tsx +122 -0
  26. package/src/components/forms/FormContainer.tsx +24 -0
  27. package/src/components/forms/ImageForm.tsx +195 -0
  28. package/src/components/forms/TextForm.tsx +22 -0
  29. package/src/exports.ts +3 -0
  30. package/src/index.css +79 -0
  31. package/src/index.tsx +14 -0
  32. package/src/server.ts +41 -0
  33. package/src/stories/Button.stories.tsx +20 -0
  34. package/src/stories/Checkbox.stories.tsx +14 -0
  35. package/src/stories/Dropdown.stories.tsx +23 -0
  36. package/src/stories/Introduction.mdx +221 -0
  37. package/src/stories/RichTextEditor.stories.tsx +314 -0
  38. package/src/stories/assets/code-brackets.svg +1 -0
  39. package/src/stories/assets/colors.svg +1 -0
  40. package/src/stories/assets/comments.svg +1 -0
  41. package/src/stories/assets/direction.svg +1 -0
  42. package/src/stories/assets/flow.svg +1 -0
  43. package/src/stories/assets/plugin.svg +1 -0
  44. package/src/stories/assets/repo.svg +1 -0
  45. package/src/stories/assets/stackalt.svg +1 -0
  46. package/src/vite-env.d.ts +1 -0
  47. package/src/vite-index.tsx +7 -0
  48. package/src/vite-server.ts +8 -0
@@ -0,0 +1,381 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import {
3
+ INSERT_CHECK_LIST_COMMAND,
4
+ INSERT_ORDERED_LIST_COMMAND,
5
+ INSERT_UNORDERED_LIST_COMMAND,
6
+ REMOVE_LIST_COMMAND,
7
+ $isListNode,
8
+ ListNode,
9
+ } from "@lexical/list";
10
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
11
+ import {
12
+ $createHeadingNode,
13
+ $isHeadingNode,
14
+ HeadingTagType,
15
+ } from "@lexical/rich-text";
16
+ import {
17
+ $getSelectionStyleValueForProperty,
18
+ $patchStyleText,
19
+ $setBlocksType,
20
+ } from "@lexical/selection";
21
+ import {
22
+ $findMatchingParent,
23
+ $getNearestBlockElementAncestorOrThrow,
24
+ mergeRegister,
25
+ $getNearestNodeOfType,
26
+ } from "@lexical/utils";
27
+ import {
28
+ $createParagraphNode,
29
+ $getSelection,
30
+ $isRangeSelection,
31
+ $isRootOrShadowRoot,
32
+ $isTextNode,
33
+ COMMAND_PRIORITY_CRITICAL,
34
+ DEPRECATED_$isGridSelection,
35
+ FORMAT_TEXT_COMMAND,
36
+ LexicalEditor,
37
+ NodeKey,
38
+ REDO_COMMAND,
39
+ SELECTION_CHANGE_COMMAND,
40
+ SerializedLexicalNode,
41
+ UNDO_COMMAND,
42
+ } from "lexical";
43
+ import { SerializedEditorState } from "lexical/LexicalEditorState";
44
+ import { FC, useCallback, useEffect, useState } from "react";
45
+ import Bold from "../../../assets/icons/Bold";
46
+ import ImageIcon from "../../../assets/icons/ImageIcon";
47
+ import Italic from "../../../assets/icons/Italic";
48
+ import Strikethrough from "../../../assets/icons/Strikethrough";
49
+ import Underline from "../../../assets/icons/Underline";
50
+ import Undo from "../../../assets/icons/Undo";
51
+ import Button from "../../Button";
52
+ import Dropdown from "../../Dropdown";
53
+ import UploadModal from "../../UploadModal";
54
+ import { INSERT_IMAGE_COMMAND } from "./ImagePlugin";
55
+
56
+ export interface ToolbarSettingsProps {
57
+ fontsFamilies?: string[];
58
+ fontSizes?: string[];
59
+ colors?: string[];
60
+ onEditor?: (editor: LexicalEditor) => void;
61
+ }
62
+
63
+ const Toolbar: FC<ToolbarSettingsProps> = ({
64
+ fontSizes,
65
+ fontsFamilies,
66
+ onEditor,
67
+ colors,
68
+ }) => {
69
+ const [editor] = useLexicalComposerContext();
70
+ useEffect(() => {
71
+ if (onEditor) {
72
+ onEditor(editor);
73
+ }
74
+ }, [editor]);
75
+ const [activeEditor, setActiveEditor] = useState(editor);
76
+ const [selectedElementKey, setSelectedElementKey] = useState<NodeKey | null>(
77
+ null
78
+ );
79
+ const [isBold, setIsBold] = useState(false);
80
+ const [isItalic, setIsItalic] = useState(false);
81
+ const [isStrikethrough, setIsStrikethrough] = useState(false);
82
+ const [isUnderline, setIsUnderline] = useState(false);
83
+ const [fontSize, setFontSize] = useState<string>("15px");
84
+ const [fontColor, setFontColor] = useState<string>("#000");
85
+ const [fontFamily, setFontFamily] = useState<string>("Sans");
86
+ const [blockType, setBlockType] =
87
+ useState<keyof typeof blockTypes>("paragraph");
88
+
89
+ const [showModal, setShowModal] = useState<boolean>(false);
90
+ const [inputUrl, setInputUrl] = useState<boolean>(false);
91
+ const [uploadMode, setUploadMode] = useState<"url" | "file">("url");
92
+ const [file, setFile] = useState<File | null>(null);
93
+ const [url, setUrl] = useState<string>("");
94
+
95
+ const blockTypes: { [key: string]: string } = {
96
+ paragraph: "Normal",
97
+ h1: "Heading 1",
98
+ h2: "Heading 2",
99
+ h3: "Heading 3",
100
+ h4: "Heading 4",
101
+ h5: "Heading 5",
102
+ h6: "Heading 6",
103
+ number: "Numbered List",
104
+ bullet: "Bulleted List",
105
+ };
106
+ const blockTypesLookup: { [key: string]: string } = {};
107
+
108
+ for (const key in blockTypes) {
109
+ blockTypesLookup[blockTypes[key]] = key;
110
+ }
111
+
112
+ const updateToolbar = useCallback(() => {
113
+ const selection = $getSelection();
114
+ if ($isRangeSelection(selection)) {
115
+ const anchorNode = selection.anchor.getNode();
116
+ let element =
117
+ anchorNode.getKey() === "root"
118
+ ? anchorNode
119
+ : $findMatchingParent(anchorNode, (e) => {
120
+ const parent = e.getParent();
121
+ return parent !== null && $isRootOrShadowRoot(parent);
122
+ });
123
+
124
+ if (element === null) {
125
+ element = anchorNode.getTopLevelElementOrThrow();
126
+ }
127
+
128
+ const elementKey = element.getKey();
129
+ const elementDOM = activeEditor.getElementByKey(elementKey);
130
+ setIsBold(selection.hasFormat("bold"));
131
+ setIsItalic(selection.hasFormat("italic"));
132
+ setIsStrikethrough(selection.hasFormat("strikethrough"));
133
+ setIsUnderline(selection.hasFormat("underline"));
134
+ if (elementDOM !== null) {
135
+ setSelectedElementKey(elementKey);
136
+ if ($isListNode(element)) {
137
+ const parentList = $getNearestNodeOfType<ListNode>(
138
+ anchorNode,
139
+ ListNode
140
+ );
141
+ const type = parentList
142
+ ? parentList.getListType()
143
+ : element.getListType();
144
+ setBlockType(type);
145
+ } else {
146
+ const type = $isHeadingNode(element)
147
+ ? element.getTag()
148
+ : element.getType();
149
+ if (type in blockTypes) {
150
+ setBlockType(type as keyof typeof blockTypes);
151
+ }
152
+ }
153
+ }
154
+
155
+ setFontSize(
156
+ $getSelectionStyleValueForProperty(selection, "font-size", "15px")
157
+ );
158
+ setFontColor(
159
+ $getSelectionStyleValueForProperty(selection, "color", "#000")
160
+ );
161
+ setFontFamily(
162
+ $getSelectionStyleValueForProperty(selection, "font-family", "Arial")
163
+ );
164
+ }
165
+ }, [activeEditor]);
166
+
167
+ useEffect(() => {
168
+ return mergeRegister(
169
+ editor.registerUpdateListener(({ editorState }) => {
170
+ editorState.read(() => {
171
+ updateToolbar();
172
+ });
173
+ })
174
+ );
175
+ }, [updateToolbar, activeEditor, editor]);
176
+
177
+ useEffect(() => {
178
+ return editor.registerCommand(
179
+ SELECTION_CHANGE_COMMAND,
180
+ (_payload, newEditor) => {
181
+ updateToolbar();
182
+ setActiveEditor(newEditor);
183
+ return false;
184
+ },
185
+ COMMAND_PRIORITY_CRITICAL
186
+ );
187
+ }, [editor, updateToolbar]);
188
+
189
+ const formatText = (format: keyof typeof blockTypes) => {
190
+ if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(format as string)) {
191
+ if (blockType !== format) {
192
+ editor.update(() => {
193
+ const selection = $getSelection();
194
+ if ($isRangeSelection(selection)) {
195
+ $setBlocksType(selection, () => {
196
+ return $createHeadingNode(format as HeadingTagType);
197
+ });
198
+ }
199
+ });
200
+ }
201
+ } else if (format === "paragraph" && blockType !== "paragraph") {
202
+ editor.update(() => {
203
+ const selection = $getSelection();
204
+ if (
205
+ $isRangeSelection(selection) ||
206
+ DEPRECATED_$isGridSelection(selection)
207
+ ) {
208
+ $setBlocksType(selection, () => $createParagraphNode());
209
+ }
210
+ });
211
+ } else {
212
+ if (format === "number" && blockType !== "number") {
213
+ editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
214
+ } else if (format === "bullet" && blockType !== "bullet") {
215
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
216
+ } else {
217
+ editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
218
+ }
219
+ }
220
+ };
221
+
222
+ const clearFormatting = useCallback(() => {
223
+ editor.update(() => {
224
+ const selection = $getSelection();
225
+ if ($isRangeSelection(selection)) {
226
+ const anchor = selection.anchor;
227
+ const focus = selection.focus;
228
+ const nodes = selection.getNodes();
229
+
230
+ if (anchor.key === focus.key && anchor.offset === focus.offset) {
231
+ return;
232
+ }
233
+ nodes.forEach((node, idx) => {
234
+ if ($isTextNode(node)) {
235
+ if (idx === 0 && anchor.offset !== 0) {
236
+ node = node.splitText(anchor.offset)[1] || node;
237
+ }
238
+ if (idx === nodes.length - 1) {
239
+ node = node.splitText(focus.offset)[0] || node;
240
+ }
241
+
242
+ if (node.__style !== "") {
243
+ node.setStyle("");
244
+ }
245
+ if (node.__format !== 0) {
246
+ node.setFormat(0);
247
+ $getNearestBlockElementAncestorOrThrow(node).setFormat("");
248
+ }
249
+ } else if ($isHeadingNode(node)) {
250
+ node.replace($createParagraphNode(), true);
251
+ }
252
+ });
253
+ }
254
+ });
255
+ }, [activeEditor]);
256
+
257
+ const changeFontFamily = (fontFamily: string) => {
258
+ editor.update(() => {
259
+ const selection = $getSelection();
260
+ if ($isRangeSelection(selection)) {
261
+ $patchStyleText(selection, {
262
+ ["font-family"]: fontFamily,
263
+ });
264
+ }
265
+ });
266
+ };
267
+
268
+ const changeFontSize = (fontSize: string) => {
269
+ editor.update(() => {
270
+ const selection = $getSelection();
271
+ if ($isRangeSelection(selection)) {
272
+ $patchStyleText(selection, {
273
+ ["font-size"]: fontSize,
274
+ });
275
+ }
276
+ });
277
+ };
278
+
279
+ const uploadImage = (url: string, alt?: string) => {
280
+ editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
281
+ altText: "URL image",
282
+ src: url,
283
+ });
284
+ };
285
+
286
+ return (
287
+ <div className="flex flex-row items-center gap-6 p-2 overflow-scroll">
288
+ {/* <div className="flex flex-row gap-2">
289
+ <button className="hidden w-0 h-0 " disabled></button>
290
+ <Button
291
+ icon={<Undo />}
292
+ onClick={() => {
293
+ editor.dispatchCommand(UNDO_COMMAND, undefined);
294
+ }}
295
+ ></Button>
296
+ <Button
297
+ icon={<Undo className="transform -scale-x-100" />}
298
+ onClick={() => {
299
+ editor.dispatchCommand(REDO_COMMAND, undefined);
300
+ }}
301
+ />
302
+ </div> */}
303
+ <div className="flex flex-row gap-2">
304
+ <Dropdown
305
+ options={Object.values(blockTypes)}
306
+ label={
307
+ blockTypes[blockType as string] ?? blockType + " (not supported)"
308
+ }
309
+ onChange={(selectedOption) => {
310
+ formatText(blockTypesLookup[selectedOption]);
311
+ }}
312
+ />
313
+ <Dropdown
314
+ onChange={changeFontFamily}
315
+ options={fontsFamilies ?? ["sans", "serif", "solina"]}
316
+ label={fontFamily}
317
+ />
318
+ <Dropdown
319
+ onChange={changeFontSize}
320
+ options={
321
+ fontSizes ??
322
+ [11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((size) => `${size}px`)
323
+ }
324
+ label={fontSize}
325
+ />
326
+ </div>
327
+ <div className="flex flex-row gap-2">
328
+ <Button
329
+ variant="primary"
330
+ onClick={(ev) => {
331
+ ev.preventDefault();
332
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
333
+ }}
334
+ tooltip="Format text as bold"
335
+ active={isBold}
336
+ icon={<Bold className={`${isBold && "stroke-[3px]"}`} />}
337
+ />
338
+ <Button
339
+ active={isStrikethrough}
340
+ onClick={(ev) => {
341
+ ev.preventDefault();
342
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
343
+ }}
344
+ icon={
345
+ <Strikethrough className={`${isStrikethrough && "stroke-[2px]"}`} />
346
+ }
347
+ />
348
+ <Button
349
+ active={isItalic}
350
+ onClick={(ev) => {
351
+ ev.preventDefault();
352
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
353
+ }}
354
+ icon={<Italic className={`${isItalic && "stroke-[3px]"}`} />}
355
+ />
356
+ <Button
357
+ active={isUnderline}
358
+ onClick={(ev) => {
359
+ ev.preventDefault();
360
+ editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline");
361
+ }}
362
+ icon={<Underline className={`${isUnderline && "stroke-[3px]"}`} />}
363
+ />
364
+ </div>
365
+ <Button
366
+ icon={<ImageIcon />}
367
+ onClick={(ev) => {
368
+ ev.preventDefault();
369
+ setShowModal(true);
370
+ }}
371
+ ></Button>
372
+ <UploadModal
373
+ setShowModal={setShowModal}
374
+ showModal={showModal}
375
+ uploadImage={uploadImage}
376
+ />
377
+ </div>
378
+ );
379
+ };
380
+
381
+ export default Toolbar;
@@ -0,0 +1,176 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import {
3
+ $createParagraphNode,
4
+ $createTextNode,
5
+ $getRoot,
6
+ LexicalEditor,
7
+ LexicalNode,
8
+ } from "lexical";
9
+ import {
10
+ ListItemNode,
11
+ ListNode,
12
+ $createListNode,
13
+ $createListItemNode,
14
+ } from "@lexical/list";
15
+ import { LexicalComposer } from "@lexical/react/LexicalComposer";
16
+ import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
17
+ import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
18
+ import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
19
+ import { ListPlugin } from "@lexical/react/LexicalListPlugin";
20
+ import { FC } from "react";
21
+ import LexicalContentEditable from "./ContentEditable";
22
+ import { ImageNode } from "./Nodes/ImageNode";
23
+ import { AutoFocus } from "./Plugins/AutoFocus";
24
+ import ImagesPlugin from "./Plugins/ImagePlugin";
25
+ import Toolbar from "./Plugins/Toolbar";
26
+ import {
27
+ RichText,
28
+ TextNode as ValTextNode,
29
+ HeadingNode as ValHeadingNode,
30
+ ListItemNode as ValListItemNode,
31
+ ParagraphNode as ValParagraphNode,
32
+ ListNode as ValListNode,
33
+ } from "@valbuild/core";
34
+ import { $createHeadingNode, HeadingNode } from "@lexical/rich-text";
35
+
36
+ export interface RichTextEditorProps {
37
+ richtext: RichText;
38
+ onEditor?: (editor: LexicalEditor) => void; // Not the ideal way of passing the editor to the upper context, we need it to be able to save
39
+ }
40
+
41
+ function onError(error: any) {
42
+ console.error(error);
43
+ }
44
+
45
+ type ValNode =
46
+ | ValTextNode
47
+ | ValHeadingNode
48
+ | ValListItemNode
49
+ | ValParagraphNode
50
+ | ValListNode;
51
+ function toLexicalNode(node: ValNode): LexicalNode {
52
+ switch (node.type) {
53
+ case "heading":
54
+ return toLexicalHeadingNode(node);
55
+ case "listitem":
56
+ return toLexicalListItemNode(node);
57
+ case "paragraph":
58
+ return toLexicalParagraphNode(node);
59
+ case "list":
60
+ return toLexicalListNode(node);
61
+ case "text":
62
+ return toLexicalTextNode(node);
63
+ }
64
+ }
65
+
66
+ function toLexicalHeadingNode(heading: ValHeadingNode): LexicalNode {
67
+ const node = $createHeadingNode(heading.tag);
68
+ node.setFormat(heading.format);
69
+ node.setIndent(heading.indent);
70
+ node.setDirection(heading.direction);
71
+ node.append(...heading.children.map((child) => toLexicalNode(child)));
72
+ return node;
73
+ }
74
+
75
+ function toLexicalParagraphNode(paragraph: ValParagraphNode): LexicalNode {
76
+ const node = $createParagraphNode();
77
+ node.setFormat(paragraph.format);
78
+ node.setIndent(paragraph.indent);
79
+ node.setDirection(paragraph.direction);
80
+ node.append(...paragraph.children.map((child) => toLexicalNode(child)));
81
+ return node;
82
+ }
83
+
84
+ function toLexicalListItemNode(listItem: ValListItemNode): LexicalNode {
85
+ const node = $createListItemNode();
86
+ node.setFormat(listItem.format);
87
+ node.setIndent(listItem.indent);
88
+ node.setDirection(listItem.direction);
89
+ node.setValue(listItem.value);
90
+ node.setChecked(listItem.checked);
91
+ node.append(...listItem.children.map((child) => toLexicalNode(child)));
92
+ return node;
93
+ }
94
+
95
+ function toLexicalListNode(list: ValListNode): LexicalNode {
96
+ const node = $createListNode(list.listType, list.start);
97
+ node.setFormat(list.format);
98
+ node.setIndent(list.indent);
99
+ node.setDirection(list.direction);
100
+ node.append(...list.children.map((child) => toLexicalNode(child)));
101
+ return node;
102
+ }
103
+
104
+ function toLexicalTextNode(text: ValTextNode): LexicalNode {
105
+ const node = $createTextNode(text.text);
106
+ node.setFormat(text.format as any); // TODO: why is text.format numbers when we are trying it out?
107
+ text.indent && node.setIndent(text.indent);
108
+ text.direction && node.setDirection(text.direction);
109
+ node.setStyle(text.style);
110
+ node.setDetail(text.detail);
111
+ return node;
112
+ }
113
+
114
+ export const RichTextEditor: FC<RichTextEditorProps> = ({
115
+ richtext,
116
+ onEditor,
117
+ }) => {
118
+ const prePopulatedState = () => {
119
+ const root = $getRoot();
120
+ $getRoot().append(
121
+ ...richtext.children.map((child) => toLexicalNode(child))
122
+ );
123
+ root.selectEnd();
124
+ };
125
+ const initialConfig = {
126
+ namespace: "val",
127
+ editorState: prePopulatedState,
128
+ nodes: [HeadingNode, ImageNode, ListNode, ListItemNode],
129
+ theme: {
130
+ root: "relative p-4 bg-base min-h-[200px] text-white font-roboto",
131
+ text: {
132
+ bold: "font-semibold",
133
+ underline: "underline",
134
+ italic: "italic",
135
+ strikethrough: "line-through",
136
+ underlineStrikethrough: "underlined-line-through",
137
+ },
138
+ list: {
139
+ listitem: "ml-[20px]",
140
+ ol: "list-decimal",
141
+ ul: "list-disc",
142
+ },
143
+ heading: {
144
+ h1: "text-4xl font-bold",
145
+ h2: "text-3xl font-bold",
146
+ h3: "text-2xl font-bold",
147
+ h4: "text-xl font-bold",
148
+ h5: "text-lg font-bold",
149
+ h6: "text-base font-bold",
150
+ },
151
+ },
152
+ onError,
153
+ };
154
+ return (
155
+ <div className=" relative bg-base min-h-[200px] mt-2 border border-highlight rounded">
156
+ <LexicalComposer initialConfig={initialConfig}>
157
+ <Toolbar onEditor={onEditor} />
158
+ <ImagesPlugin />
159
+ <RichTextPlugin
160
+ contentEditable={
161
+ <LexicalContentEditable className="relative bg-fill flex flex-col h-full w-full min-h-[200px] text-primary outline-none" />
162
+ }
163
+ placeholder={
164
+ <div className="absolute top-[calc(58px+1rem)] left-4 text-base/25 ">
165
+ Enter some text...
166
+ </div>
167
+ }
168
+ ErrorBoundary={LexicalErrorBoundary}
169
+ />
170
+ <ListPlugin />
171
+ <AutoFocus />
172
+ <HistoryPlugin />
173
+ </LexicalComposer>
174
+ </div>
175
+ );
176
+ };
@@ -0,0 +1,109 @@
1
+ import { FC, useEffect, useState } from "react";
2
+ import Button from "./Button";
3
+
4
+ interface UploadModalProps {
5
+ showModal: boolean;
6
+ setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
7
+ uploadImage: (url: string, alt?: string) => void;
8
+ }
9
+
10
+ const UploadModal: FC<UploadModalProps> = ({
11
+ showModal,
12
+ setShowModal,
13
+ uploadImage,
14
+ }) => {
15
+ const [uploadUrl, setUploadUrl] = useState<boolean>(true);
16
+ const [url, setUrl] = useState<string>("");
17
+
18
+ useEffect(() => {
19
+ setUrl("");
20
+ }, [uploadUrl]);
21
+
22
+ const loadImage = (files: FileList | null) => {
23
+ const reader = new FileReader();
24
+ reader.onload = function () {
25
+ if (typeof reader.result === "string") {
26
+ setUrl(reader.result);
27
+ }
28
+ return "";
29
+ };
30
+ if (files !== null) {
31
+ reader.readAsDataURL(files[0]);
32
+ }
33
+ };
34
+
35
+ const onSubmit = () => {
36
+ if (url) {
37
+ uploadImage(url);
38
+ setUrl("");
39
+ setShowModal(false);
40
+ setUploadUrl(true);
41
+ }
42
+ };
43
+
44
+ return (
45
+ <div className="absolute z-10 flex flex-col justify-center items-center top-[50%] left-[50%] font-mono">
46
+ {showModal && (
47
+ <div className="flex flex-col items-center justify-center">
48
+ <div className="flex flex-col items-start justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
49
+ <div className="fixed inset-0 transition-opacity">
50
+ <div className="absolute inset-0 bg-gray-500 opacity-75" />
51
+ </div>
52
+
53
+ <div className="flex flex-col items-center justify-between bg-fill rounded-lg transform transition-all min-h-[300px] min-w-[500px] h-full px-5 py-7">
54
+ <div className="flex flex-col items-center w-full gap-5">
55
+ <div className="mb-4">
56
+ <Button
57
+ variant={uploadUrl ? "primary" : "secondary"}
58
+ onClick={() => setUploadUrl(true)}
59
+ >
60
+ Paste URL
61
+ </Button>
62
+ <Button
63
+ variant={uploadUrl ? "secondary" : "primary"}
64
+ onClick={() => setUploadUrl(false)}
65
+ >
66
+ Upload file
67
+ </Button>
68
+ </div>
69
+ {uploadUrl ? (
70
+ <div className="flex flex-col items-center justify-center w-full gap-5">
71
+ <label className="text-primary">Upload URL</label>
72
+ <input
73
+ className="w-full h-10 rounded-lg bg-border"
74
+ value={url}
75
+ onChange={(event) => setUrl(event.target.value)}
76
+ />
77
+ </div>
78
+ ) : (
79
+ <div className="flex flex-col items-center justify-center w-full gap-5">
80
+ <label className="text-primary">Choose File</label>
81
+ <input
82
+ className="h-10 rounded-lg w-fit"
83
+ type="file"
84
+ onChange={(e) => loadImage(e.target.files)}
85
+ />
86
+ </div>
87
+ )}
88
+ </div>
89
+ <div className="flex flex-row items-center justify-center gap-5 ">
90
+ <Button variant="secondary" onClick={() => setShowModal(false)}>
91
+ Cancel
92
+ </Button>
93
+ <Button
94
+ variant="primary"
95
+ disabled={url === ""}
96
+ onClick={() => onSubmit()}
97
+ >
98
+ Upload
99
+ </Button>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ )}
105
+ </div>
106
+ );
107
+ };
108
+
109
+ export default UploadModal;
@@ -0,0 +1,41 @@
1
+ import { EditButton } from "./EditButton";
2
+ import { Form, FormProps } from "./forms/Form";
3
+ import { ValWindow } from "./ValWindow";
4
+
5
+ type ValWindow = {
6
+ position: {
7
+ left: number;
8
+ top: number;
9
+ };
10
+ } & FormProps;
11
+
12
+ export type ValOverlayProps = {
13
+ editMode: boolean;
14
+ setEditMode: (editMode: boolean) => void;
15
+ valWindow?: ValWindow;
16
+ closeValWindow: () => void;
17
+ };
18
+
19
+ export function ValOverlay({
20
+ editMode,
21
+ setEditMode,
22
+ valWindow,
23
+ closeValWindow,
24
+ }: ValOverlayProps) {
25
+ return (
26
+ <>
27
+ <div className="fixed -translate-x-1/2 left-1/2 bottom-4">
28
+ <EditButton
29
+ onClick={() => {
30
+ setEditMode(!editMode);
31
+ }}
32
+ />
33
+ </div>
34
+ {editMode && valWindow && (
35
+ <ValWindow onClose={closeValWindow} position={valWindow.position}>
36
+ <Form onSubmit={valWindow.onSubmit} inputs={valWindow.inputs} />
37
+ </ValWindow>
38
+ )}
39
+ </>
40
+ );
41
+ }