@gnar-engine/cli 1.0.4 → 1.0.5

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 (119) hide show
  1. package/bootstrap/deploy.localdev.yml +30 -3
  2. package/bootstrap/secrets.localdev.yml +15 -4
  3. package/bootstrap/services/control/src/config.js +4 -0
  4. package/bootstrap/services/page/Dockerfile +23 -0
  5. package/bootstrap/services/page/package.json +16 -0
  6. package/bootstrap/services/page/src/app.js +50 -0
  7. package/bootstrap/services/page/src/commands/block.handler.js +94 -0
  8. package/bootstrap/services/page/src/commands/page.handler.js +167 -0
  9. package/bootstrap/services/page/src/config.js +62 -0
  10. package/bootstrap/services/page/src/controllers/block.http.controller.js +87 -0
  11. package/bootstrap/services/page/src/controllers/message.controller.js +51 -0
  12. package/bootstrap/services/page/src/controllers/page.http.controller.js +89 -0
  13. package/bootstrap/services/page/src/policies/block.policy.js +50 -0
  14. package/bootstrap/services/page/src/policies/page.policy.js +49 -0
  15. package/bootstrap/services/page/src/schema/page.schema.js +139 -0
  16. package/bootstrap/services/page/src/services/block.service.js +83 -0
  17. package/bootstrap/services/page/src/services/page.service.js +83 -0
  18. package/bootstrap/services/portal/Dockerfile +20 -0
  19. package/bootstrap/services/portal/README.md +73 -0
  20. package/bootstrap/services/portal/index.html +13 -0
  21. package/bootstrap/services/portal/nginx.conf +5 -0
  22. package/bootstrap/services/portal/package.json +33 -0
  23. package/bootstrap/services/portal/public/vite.svg +1 -0
  24. package/bootstrap/services/portal/react-router.config.js +7 -0
  25. package/bootstrap/services/portal/src/App.jsx +16 -0
  26. package/bootstrap/services/portal/src/assets/gnar-engine-white-logo.svg +9 -0
  27. package/bootstrap/services/portal/src/assets/icon-agent.svg +6 -0
  28. package/bootstrap/services/portal/src/assets/icon-cog.svg +4 -0
  29. package/bootstrap/services/portal/src/assets/icon-delete.svg +3 -0
  30. package/bootstrap/services/portal/src/assets/icon-home.svg +3 -0
  31. package/bootstrap/services/portal/src/assets/icon-padlock.svg +3 -0
  32. package/bootstrap/services/portal/src/assets/icon-page.svg +6 -0
  33. package/bootstrap/services/portal/src/assets/icon-reports.svg +3 -0
  34. package/bootstrap/services/portal/src/assets/icon-user.svg +3 -0
  35. package/bootstrap/services/portal/src/assets/icon-users.svg +3 -0
  36. package/bootstrap/services/portal/src/assets/login-green-rad-back-1.jpg +0 -0
  37. package/bootstrap/services/portal/src/assets/react.svg +1 -0
  38. package/bootstrap/services/portal/src/components/CrudList/CrudList.jsx +85 -0
  39. package/bootstrap/services/portal/src/components/CrudList/CrudList.less +59 -0
  40. package/bootstrap/services/portal/src/components/CustomSelect/CustomSelect.jsx +81 -0
  41. package/bootstrap/services/portal/src/components/CustomSelect/CustomSelect.less +0 -0
  42. package/bootstrap/services/portal/src/components/LoginForm/LoginForm.jsx +58 -0
  43. package/bootstrap/services/portal/src/components/PageBlockSwitch/PageBlockSwitch.jsx +129 -0
  44. package/bootstrap/services/portal/src/components/Sidebar/Sidebar.jsx +33 -0
  45. package/bootstrap/services/portal/src/components/Sidebar/Sidebar.less +37 -0
  46. package/bootstrap/services/portal/src/components/Topbar/Topbar.jsx +19 -0
  47. package/bootstrap/services/portal/src/components/Topbar/Topbar.less +22 -0
  48. package/bootstrap/services/portal/src/components/UserInfo/UserInfo.jsx +33 -0
  49. package/bootstrap/services/portal/src/components/UserInfo/UserInfo.less +21 -0
  50. package/bootstrap/services/portal/src/css/style.css +711 -0
  51. package/bootstrap/services/portal/src/data/pages.data.js +10 -0
  52. package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.jsx +65 -0
  53. package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.less +102 -0
  54. package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.jsx +115 -0
  55. package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.less +43 -0
  56. package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.jsx +124 -0
  57. package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.less +0 -0
  58. package/bootstrap/services/portal/src/elements/Repeater/Repeater.jsx +52 -0
  59. package/bootstrap/services/portal/src/elements/Repeater/Repeater.less +70 -0
  60. package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.jsx +18 -0
  61. package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.less +37 -0
  62. package/bootstrap/services/portal/src/elements/SaveButton/SaveButton.jsx +45 -0
  63. package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.jsx +63 -0
  64. package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.less +23 -0
  65. package/bootstrap/services/portal/src/elements/TextInput/TextInput.jsx +17 -0
  66. package/bootstrap/services/portal/src/layouts/Card/Card.jsx +15 -0
  67. package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.jsx +29 -0
  68. package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.less +49 -0
  69. package/bootstrap/services/portal/src/main.jsx +51 -0
  70. package/bootstrap/services/portal/src/pages/BlockSinglePage/BlockSinglePage.jsx +277 -0
  71. package/bootstrap/services/portal/src/pages/BlocksPage/BlocksPage.jsx +23 -0
  72. package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.jsx +11 -0
  73. package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.less +0 -0
  74. package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.jsx +21 -0
  75. package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.less +51 -0
  76. package/bootstrap/services/portal/src/pages/PageSinglePage/PageSinglePage.jsx +338 -0
  77. package/bootstrap/services/portal/src/pages/PagesPage/PagesPage.jsx +23 -0
  78. package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.jsx +9 -0
  79. package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.less +0 -0
  80. package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.jsx +25 -0
  81. package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.less +0 -0
  82. package/bootstrap/services/portal/src/services/block.js +28 -0
  83. package/bootstrap/services/portal/src/services/client.js +67 -0
  84. package/bootstrap/services/portal/src/services/gravatar.js +14 -0
  85. package/bootstrap/services/portal/src/services/page.js +28 -0
  86. package/bootstrap/services/portal/src/services/storage.js +62 -0
  87. package/bootstrap/services/portal/src/services/user.js +41 -0
  88. package/bootstrap/services/portal/src/slices/authSlice.js +101 -0
  89. package/bootstrap/services/portal/src/store/configureStore.js +10 -0
  90. package/bootstrap/services/portal/src/style/cards.less +57 -0
  91. package/bootstrap/services/portal/src/style/global.less +204 -0
  92. package/bootstrap/services/portal/src/style/icons.less +21 -0
  93. package/bootstrap/services/portal/src/style/inputs.less +52 -0
  94. package/bootstrap/services/portal/src/style/main.less +28 -0
  95. package/bootstrap/services/portal/src/utils/utils.js +9 -0
  96. package/bootstrap/services/portal/vite.config.js +12 -0
  97. package/bootstrap/services/user/src/app.js +6 -1
  98. package/bootstrap/services/user/src/commands/user.handler.js +0 -3
  99. package/bootstrap/services/user/src/config.js +5 -1
  100. package/bootstrap/services/user/src/policies/user.policy.js +3 -1
  101. package/bootstrap/services/user/src/tests/commands/user.test.js +22 -0
  102. package/install-from-clone.sh +30 -0
  103. package/package.json +1 -1
  104. package/src/cli.js +8 -0
  105. package/src/dev/commands.js +10 -2
  106. package/src/dev/dev.service.js +147 -60
  107. package/src/provisioner/Dockerfile +27 -0
  108. package/src/provisioner/package.json +19 -0
  109. package/src/provisioner/src/app.js +56 -0
  110. package/src/provisioner/src/services/mongodb.js +58 -0
  111. package/src/provisioner/src/services/mysql.js +51 -0
  112. package/src/provisioner/src/services/secrets.js +84 -0
  113. package/src/scaffolder/commands.js +1 -1
  114. package/src/scaffolder/scaffolder.handler.js +40 -15
  115. package/templates/service/src/app.js.hbs +12 -1
  116. package/templates/service/src/commands/{{serviceName}}.handler.js.hbs +1 -1
  117. package/templates/service/src/mongodb.config.js.hbs +5 -1
  118. package/templates/service/src/mysql.config.js.hbs +4 -0
  119. package/bootstrap/services/user/src/tests/user.test.js +0 -126
@@ -0,0 +1,65 @@
1
+ import React, { useState } from "react";
2
+
3
+
4
+ const CustomSelect = ({label, placeholder, name, options, labelKey, icon = null, setSelectedOption, selectedOption}) => {
5
+
6
+ const [isOpen, setIsOpen] = useState(false);
7
+ const [isClosing, setIsClosing] = useState(false);
8
+
9
+ // Close dropdown
10
+ const closeDropdown = () => {
11
+ setIsClosing(true);
12
+ setIsOpen(false);
13
+ setTimeout(() => {
14
+ setIsClosing(false);
15
+ }, 300);
16
+ };
17
+
18
+ // Open dropdown
19
+ const openDropdown = () => {
20
+ setIsOpen(true);
21
+ setIsClosing(false);
22
+ };
23
+
24
+ // Handle click for opening and closing
25
+ const handleClick = () => {
26
+ if (!isOpen) {
27
+ openDropdown();
28
+ } else {
29
+ closeDropdown();
30
+ }
31
+ };
32
+
33
+ return (
34
+ <div className="custom-select-cont">
35
+ <label>{label}</label>
36
+ {placeholder && name && options && labelKey && setSelectedOption &&
37
+ <div className={`custom-select ${isOpen && "open"} ${isClosing ? "closing" : ""}`}>
38
+ <div className="custom-select-input" id={name} name={name} onClick={handleClick}>
39
+ {icon && <img src={icon} alt="icon" />}
40
+ {selectedOption && selectedOption[labelKey] ? (
41
+ <span>{selectedOption[labelKey]}</span>
42
+ ) : (
43
+ <span>{placeholder}</span>
44
+ )}
45
+ </div>
46
+ {isOpen && (
47
+ <div className="custom-select-options" onMouseLeave={closeDropdown} >
48
+ <div className="custom-select-options-inner">
49
+ {options.map((option, index) => {
50
+ return (
51
+ <div key={index} className="custom-select-option" onClick={() => {setSelectedOption(option); setIsOpen(false)}}>
52
+ {option[labelKey]}
53
+ </div>
54
+ )
55
+ })}
56
+ </div>
57
+ </div>
58
+ )}
59
+ </div>
60
+ }
61
+ </div>
62
+ )
63
+ }
64
+
65
+ export default CustomSelect;
@@ -0,0 +1,102 @@
1
+ @select-box-options-height: 200px;
2
+
3
+ .custom-select-cont {
4
+ display: inline-block;
5
+
6
+ label {
7
+ width: 100%;
8
+ }
9
+ }
10
+
11
+ .custom-select {
12
+ max-width: 215px;
13
+ min-width: 215px;
14
+ position: relative;
15
+
16
+ .custom-select-input {
17
+ position: relative;
18
+ width: 100%;
19
+ padding: 6px 10px;
20
+ margin: 10px 0;
21
+ border: 1px solid @dark-grey;
22
+ background: @dark-3;
23
+ border-radius: 25px;
24
+ box-sizing: border-box;
25
+ font-size: 15px;
26
+ color: @dark-1;
27
+ z-index: 3;
28
+
29
+ &:hover {
30
+ cursor: pointer;
31
+ }
32
+
33
+ &:after {
34
+ content: url('../assets/chevron.svg');
35
+ transition-duration: 0.1s;
36
+ position: absolute;
37
+ right: 8px;
38
+ top: 5px;
39
+ transform-origin: center;
40
+ height: 20px;
41
+ }
42
+ }
43
+
44
+ .custom-select-options {
45
+ position: absolute;
46
+ width: 100%;
47
+ top: 10px;
48
+ left: 0px;
49
+ max-height: @select-box-options-height;
50
+ height: 0px !important;
51
+ overflow: hidden;
52
+ background: @dark-3;
53
+ border: 1px solid @dark-grey;
54
+ box-sizing: border-box;
55
+ border-radius: 15px;
56
+ z-index: 2;
57
+ transition-duration: 0.3s;
58
+
59
+ .custom-select-options-inner {
60
+ min-height: @select-box-options-height;
61
+ max-height: @select-box-options-height;
62
+ overflow-y: auto;
63
+ }
64
+
65
+ .custom-select-option:first-child {
66
+ margin-top: 30px;
67
+ }
68
+
69
+ .custom-select-option {
70
+ padding: 10px;
71
+ opacity: 0.8;
72
+ transition-duration: 0.1s;
73
+ color: @light-grey;
74
+ font-size: 15px;
75
+
76
+ &:hover {
77
+ opacity: 1;
78
+ cursor: pointer;
79
+ color: @green-main;
80
+ }
81
+ }
82
+ }
83
+
84
+ &.open {
85
+ z-index: 5;
86
+
87
+ .custom-select-options {
88
+ height: 400px !important;
89
+ }
90
+ .custom-select-input {
91
+ border: 1px solid @green-main;
92
+
93
+ &:after {
94
+ transform: rotate(180deg);
95
+ }
96
+ }
97
+ }
98
+
99
+ &.closing {
100
+ z-index: 5;
101
+ }
102
+ }
@@ -0,0 +1,115 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { fileToBase64 } from '../../utils/utils.js';
3
+
4
+ function ImageInput({ key, label, value: uploadedImages, onChange: setUploadedImages, maxFileSizeMb = 5 }) {
5
+
6
+ const [selectedFiles, setSelectedFiles] = useState([]);
7
+ const [imagePreviews, setImagePreviews] = useState([]);
8
+
9
+ const handleFileSelection = (files) => {
10
+ const validFiles = Array.from(files).filter(
11
+ (file) =>
12
+ [
13
+ 'image/jpeg',
14
+ 'image/png',
15
+ 'image/jpg',
16
+ 'image/gif',
17
+ 'image/svg+xml'
18
+ ].includes(file.type) &&
19
+ file.size <= maxFileSizeMb * 1024 * 1024
20
+ );
21
+
22
+ const filePreviews = validFiles.map((file) => URL.createObjectURL(file));
23
+
24
+ setSelectedFiles(validFiles);
25
+ setImagePreviews(filePreviews);
26
+
27
+ (async () => {
28
+ const file = validFiles[0];
29
+ const mimeType = file.type;
30
+ const fileName = file.name;
31
+ const base64File = await fileToBase64(file);
32
+ setUploadedImages(base64File, mimeType, fileName);
33
+ })();
34
+ }
35
+
36
+ const handleFileChange = (event) => {
37
+ const files = event.target.files;
38
+ if (files) {
39
+ handleFileSelection(files);
40
+ }
41
+ }
42
+
43
+ const handleDrop = (event) => {
44
+ event.preventDefault();
45
+ const files = event.dataTransfer.files;
46
+ if (files) {
47
+ handleFileSelection(files);
48
+ }
49
+ }
50
+
51
+ const handleRemoveFile = (index) => {
52
+ setSelectedFiles((prev) => {
53
+ const updatedFiles = prev.filter((_, i) => i !== index);
54
+ setUploadedImages(updatedFiles);
55
+ return updatedFiles;
56
+ });
57
+ setImagePreviews((prev) => prev.filter((_, i) => i !== index));
58
+ }
59
+
60
+ useEffect(() => {
61
+ if (uploadedImages?.url) {
62
+ setImagePreviews([uploadedImages.url]);
63
+ }
64
+ }, [uploadedImages])
65
+
66
+ useEffect(() => {
67
+ return () => {
68
+ imagePreviews.forEach((preview) => URL.revokeObjectURL(preview));
69
+ };
70
+ }, [imagePreviews])
71
+
72
+ return (
73
+ <div className="image-input">
74
+ {label && <label>{label}</label>}
75
+ <div className="flex-row">
76
+ <div
77
+ className="upload-area"
78
+ onDrop={handleDrop}
79
+ onDragOver={(e) => e.preventDefault()}
80
+ style={{
81
+ border: '2px dashed #ccc',
82
+ padding: '20px',
83
+ textAlign: 'center',
84
+ marginBottom: '20px',
85
+ }}
86
+ >
87
+ <input
88
+ type="file"
89
+ multiple
90
+ accept="image/*"
91
+ onChange={handleFileChange}
92
+ style={{ display: 'none' }}
93
+ id="file-input"
94
+ />
95
+ <label htmlFor="file-input" style={{ cursor: 'pointer' }}>
96
+ <p className="upload-text">Click or drag and drop here to upload</p>
97
+ <p className="upload-text">(JPEG, PNG, JPG, GIF, SVG)</p>
98
+ <p className="upload-text">Maximum file size: {maxFileSizeMb} MB.</p>
99
+ </label>
100
+ </div>
101
+
102
+ <div className="preview-area">
103
+ {imagePreviews.map((preview, index) => (
104
+ <div key={index} style={{ position: 'relative' }} className="image-preview">
105
+ <img src={preview} alt="preview" />
106
+ <span onClick={() => handleRemoveFile(index)} className="icon-delete"></span>
107
+ </div>
108
+ ))}
109
+ </div>
110
+ </div>
111
+ </div>
112
+ )
113
+ }
114
+
115
+ export default ImageInput;
@@ -0,0 +1,43 @@
1
+ .image-input {
2
+ position: relative;
3
+
4
+ & > .flex-row {
5
+ margin-top: 10px;
6
+ flex-direction: row-reverse;
7
+ gap: 0px;
8
+ }
9
+ .upload-area {
10
+ border-color: @dark-grey !important;
11
+ min-width: 190px;
12
+ margin-bottom: auto;
13
+ flex-grow: 2;
14
+ }
15
+ .preview-area {
16
+ .image-preview {
17
+ margin-right: 35px;
18
+
19
+ img {
20
+ max-width: 100%;
21
+ object-fit: contain;
22
+ }
23
+ }
24
+ }
25
+
26
+
27
+ .icon-delete {
28
+ min-width: 20px;
29
+ min-height: 20px;
30
+ display: inline-block;
31
+ background-size: contain;
32
+ background-repeat: no-repeat;
33
+ position: absolute;
34
+ right: -25px;
35
+ top: -3px;
36
+ opacity: 0.7;
37
+
38
+ &:hover {
39
+ cursor: pointer;
40
+ opacity: 1;
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,124 @@
1
+ import React, { useState, useEffect } from 'react';
2
+
3
+ function ImageMultiInput({ key, label, value: uploadedImages, onChange: setUploadedImages }) {
4
+
5
+ const [selectedFiles, setSelectedFiles] = useState([]);
6
+ const [imagePreviews, setImagePreviews] = useState([]);
7
+
8
+ const handleFileSelection = (files) => {
9
+ const validFiles = Array.from(files).filter(
10
+ (file) =>
11
+ [
12
+ 'image/jpeg',
13
+ 'image/png',
14
+ 'image/jpg',
15
+ 'image/gif',
16
+ 'image/svg+xml'
17
+ ].includes(file.type) &&
18
+ file.size <= 5 * 1024 * 1024
19
+ );
20
+
21
+ const filePreviews = validFiles.map((file) => URL.createObjectURL(file));
22
+
23
+ setSelectedFiles((prev) => {
24
+ const updatedFiles = [...prev, ...validFiles];
25
+ setUploadedImages(updatedFiles);
26
+ return updatedFiles;
27
+ });
28
+ setImagePreviews((prev) => [...prev, ...filePreviews]);
29
+ };
30
+
31
+ const handleFileChange = (event) => {
32
+ const files = event.target.files;
33
+ if (files) {
34
+ handleFileSelection(files);
35
+ }
36
+ };
37
+
38
+ const handleDrop = (event) => {
39
+ event.preventDefault();
40
+ const files = event.dataTransfer.files;
41
+ if (files) {
42
+ handleFileSelection(files);
43
+ }
44
+ };
45
+
46
+ const handleRemoveFile = (index) => {
47
+ setSelectedFiles((prev) => {
48
+ const updatedFiles = prev.filter((_, i) => i !== index);
49
+ setUploadedImages(updatedFiles);
50
+ return updatedFiles;
51
+ });
52
+ setImagePreviews((prev) => prev.filter((_, i) => i !== index));
53
+ };
54
+
55
+ useEffect(() => {
56
+ return () => {
57
+ imagePreviews.forEach((preview) => URL.revokeObjectURL(preview));
58
+ };
59
+ }, [imagePreviews]);
60
+
61
+ return (
62
+ <div className="image-input">
63
+ {label && <label>{label}</label>}
64
+
65
+ <div
66
+ onDrop={handleDrop}
67
+ onDragOver={(e) => e.preventDefault()}
68
+ style={{
69
+ border: '2px dashed #ccc',
70
+ padding: '20px',
71
+ textAlign: 'center',
72
+ marginBottom: '20px',
73
+ }}
74
+ >
75
+ <input
76
+ type="file"
77
+ multiple
78
+ accept="image/*"
79
+ onChange={handleFileChange}
80
+ style={{ display: 'none' }}
81
+ id="file-input"
82
+ />
83
+ <label htmlFor="file-input" style={{ cursor: 'pointer' }}>
84
+ <p className="plus-symbol">+</p>
85
+ <p className="upload-text">Click or drag and drop here to upload</p>
86
+ <p className="upload-text">(JPEG, PNG, JPG, GIF, SVG)</p>
87
+ <p className="upload-text">Maximum file size: 5 MB.</p>
88
+ </label>
89
+ </div>
90
+
91
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
92
+ {imagePreviews.map((preview, index) => (
93
+ <div key={index} style={{ position: 'relative' }}>
94
+ <img
95
+ src={preview}
96
+ alt="preview"
97
+ style={{ width: '100px', height: '100px', objectFit: 'cover' }}
98
+ />
99
+ <button
100
+ onClick={() => handleRemoveFile(index)}
101
+ style={{
102
+ position: 'absolute',
103
+ width: '20px',
104
+ height: '20px',
105
+ top: '0px',
106
+ right: '5px',
107
+ background: 'red',
108
+ color: '#fff',
109
+ border: 'none',
110
+ borderRadius: '50%',
111
+ cursor: 'pointer',
112
+ padding: '5px',
113
+ }}
114
+ >
115
+ ×
116
+ </button>
117
+ </div>
118
+ ))}
119
+ </div>
120
+ </div>
121
+ )
122
+ }
123
+
124
+ export default ImageMultiInput;
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+
3
+ /**
4
+ * Repeater component allows users to add, update, and remove items from a list
5
+ *
6
+ * Use setItems to manage state setting here, or use setItem override to manage state setting higher up
7
+ */
8
+ const Repeater = ({ items = [], setItems, setItem, defaultItem, renderRow, buttonText }) => {
9
+
10
+ const addItem = () => {
11
+ if (setItem) {
12
+ setItem(defaultItem);
13
+ } else if (setItems) {
14
+ setItems([...items, defaultItem]);
15
+ }
16
+ }
17
+
18
+ const updateItem = (index, newItem) => {
19
+ if (setItem) {
20
+ setItem(newItem, index);
21
+ } else if (setItems) {
22
+ const newItems = [...items];
23
+ newItems[index] = newItem;
24
+ setItems(newItems);
25
+ }
26
+ }
27
+
28
+ const removeItem = (item, index) => {
29
+ if (setItem) {
30
+ const remove = true;
31
+ setItem(item, index, remove);
32
+ } else {
33
+ setItems(items.filter((_, i) => i !== index));
34
+ }
35
+ }
36
+
37
+ return (
38
+ <div className="repeater">
39
+ {items.length > 0 && items.map((item, index) =>
40
+ <div className="repeater-row" key={index}>
41
+ {renderRow(item, index, (newItem) => updateItem(index, newItem))}
42
+ <span onClick={() => removeItem(item, index)} className="icon-delete remove-repeater-row"></span>
43
+ </div>
44
+ )}
45
+ <div className="button-cont">
46
+ <button className="add-repeater-row" onClick={addItem}>{buttonText}</button>
47
+ </div>
48
+ </div>
49
+ );
50
+ };
51
+
52
+ export default Repeater;
@@ -0,0 +1,70 @@
1
+ .repeater-row {
2
+ display: flex;
3
+ flex-direction: row;
4
+ flex-wrap: nowrap;
5
+ position: relative;
6
+ align-items: center;
7
+ gap: 20px;
8
+ }
9
+
10
+ .remove-repeater-row {
11
+ display: inline-block;
12
+ width: 30px;
13
+ height: 30px;
14
+ background-repeat: no-repeat;
15
+ background-position: center center;
16
+ margin-bottom: 10px;
17
+ opacity: 0.6;
18
+
19
+ &:hover {
20
+ cursor: pointer;
21
+ opacity: 1;
22
+ }
23
+ }
24
+
25
+ .repeater-row {
26
+ position: relative;
27
+ flex-wrap: wrap;
28
+
29
+ .nested-block .nested-block {
30
+ width: 100%;
31
+ margin-bottom: 0px;
32
+ position: relative;
33
+
34
+ .repeater {
35
+ box-sizing: border-box;
36
+ margin-bottom: 60px;
37
+ border-left: 1px solid @dark-grey;
38
+ padding-left: 35px;
39
+ }
40
+ .repeater-row {
41
+ justify-content: flex-start;
42
+ }
43
+
44
+ .add-repeater-row {
45
+ position: absolute;
46
+ left: 0px;
47
+ bottom: 10px;
48
+
49
+ }
50
+
51
+ input[type="text"],
52
+ input[type="password"],
53
+ input[type="email"],
54
+ textarea,
55
+ .custom-select .custom-select-input {
56
+ margin-bottom: 0px;
57
+ }
58
+ }
59
+
60
+ .nested-block {
61
+ .remove-repeater-row {
62
+ top: 25px;
63
+ }
64
+ }
65
+ .remove-repeater-row {
66
+ position: absolute;
67
+ right: -35px;
68
+ }
69
+ }
70
+
@@ -0,0 +1,18 @@
1
+ import ReactQuill from 'react-quill-new';
2
+ import 'react-quill-new/dist/quill.snow.css';
3
+
4
+ function RichTextInput({ key, label, value, onChange, errorMessage, isValid }) {
5
+
6
+ return (
7
+ <div className="rich-text-input">
8
+ {label && <label>{label}</label>}
9
+ <ReactQuill
10
+ theme="snow"
11
+ value={value}
12
+ onChange={onChange}
13
+ />
14
+ </div>
15
+ )
16
+ }
17
+
18
+ export default RichTextInput;
@@ -0,0 +1,37 @@
1
+ .quill {
2
+ margin-top: 10px;
3
+ .ql-toolbar {
4
+ border-color: @dark-grey !important;
5
+ border-top-left-radius: 10px;
6
+ border-top-right-radius: 10px;
7
+
8
+ button {
9
+ svg * {
10
+ stroke: @light-grey !important;
11
+ }
12
+ }
13
+ }
14
+ .ql-container {
15
+ border-color: @dark-grey !important;
16
+ border-bottom-left-radius: 10px;
17
+ border-bottom-right-radius: 10px;
18
+ }
19
+
20
+ .ql-editor {
21
+ min-height: 150px;
22
+ }
23
+
24
+ button {
25
+ min-width: unset;
26
+ }
27
+ .ql-picker .ql-picker-label {
28
+ border: none !important;
29
+ }
30
+ .ql-picker-options {
31
+ background: @dark-3 !important;
32
+ border-color: @dark-grey !important;
33
+ }
34
+ }
35
+ .ql-snow.ql-toolbar button:hover, .ql-snow .ql-toolbar button:hover, .ql-snow.ql-toolbar button:focus, .ql-snow .ql-toolbar button:focus, .ql-snow.ql-toolbar button.ql-active, .ql-snow .ql-toolbar button.ql-active, .ql-snow.ql-toolbar .ql-picker-label:hover, .ql-snow .ql-toolbar .ql-picker-label:hover, .ql-snow.ql-toolbar .ql-picker-label.ql-active, .ql-snow .ql-toolbar .ql-picker-label.ql-active, .ql-snow.ql-toolbar .ql-picker-item:hover, .ql-snow .ql-toolbar .ql-picker-item:hover, .ql-snow.ql-toolbar .ql-picker-item.ql-selected, .ql-snow .ql-toolbar .ql-picker-item.ql-selected {
36
+ color: @green-main !important;
37
+ }
@@ -0,0 +1,45 @@
1
+
2
+
3
+
4
+ import React, { useState, useEffect } from 'react';
5
+
6
+
7
+ function SaveButton({onClick, itemName, loading, error, isNew}) {
8
+
9
+ const [text, setText] = useState('Save');
10
+ const [disabled, setDisabled] = useState(false);
11
+
12
+ useEffect(() => {
13
+ if (loading) {
14
+ setDisabled(true);
15
+
16
+ if (isNew) {
17
+ setText(`Creating new ${itemName}...`);
18
+ } else {
19
+ setText(`Updating ${itemName}...`);
20
+ }
21
+ } else if (error) {
22
+ setText(`Error saving ${itemName}`);
23
+
24
+ setTimeout(() => {
25
+ setDisabled(false);
26
+ setText(`Save`);
27
+ }, 2000);
28
+ } else {
29
+ setDisabled(false);
30
+ setText('Save');
31
+ }
32
+ }, [loading, error]);
33
+
34
+ return (
35
+ <button
36
+ disabled={disabled}
37
+ onClick={onClick}
38
+ className={`save-button ${loading ? 'loading' : ''} ${error ? 'error' : ''}`}
39
+ >
40
+ {text}
41
+ </button>
42
+ )
43
+ }
44
+
45
+ export default SaveButton;