@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.
- package/bootstrap/deploy.localdev.yml +30 -3
- package/bootstrap/secrets.localdev.yml +15 -4
- package/bootstrap/services/control/src/config.js +4 -0
- package/bootstrap/services/page/Dockerfile +23 -0
- package/bootstrap/services/page/package.json +16 -0
- package/bootstrap/services/page/src/app.js +50 -0
- package/bootstrap/services/page/src/commands/block.handler.js +94 -0
- package/bootstrap/services/page/src/commands/page.handler.js +167 -0
- package/bootstrap/services/page/src/config.js +62 -0
- package/bootstrap/services/page/src/controllers/block.http.controller.js +87 -0
- package/bootstrap/services/page/src/controllers/message.controller.js +51 -0
- package/bootstrap/services/page/src/controllers/page.http.controller.js +89 -0
- package/bootstrap/services/page/src/policies/block.policy.js +50 -0
- package/bootstrap/services/page/src/policies/page.policy.js +49 -0
- package/bootstrap/services/page/src/schema/page.schema.js +139 -0
- package/bootstrap/services/page/src/services/block.service.js +83 -0
- package/bootstrap/services/page/src/services/page.service.js +83 -0
- package/bootstrap/services/portal/Dockerfile +20 -0
- package/bootstrap/services/portal/README.md +73 -0
- package/bootstrap/services/portal/index.html +13 -0
- package/bootstrap/services/portal/nginx.conf +5 -0
- package/bootstrap/services/portal/package.json +33 -0
- package/bootstrap/services/portal/public/vite.svg +1 -0
- package/bootstrap/services/portal/react-router.config.js +7 -0
- package/bootstrap/services/portal/src/App.jsx +16 -0
- package/bootstrap/services/portal/src/assets/gnar-engine-white-logo.svg +9 -0
- package/bootstrap/services/portal/src/assets/icon-agent.svg +6 -0
- package/bootstrap/services/portal/src/assets/icon-cog.svg +4 -0
- package/bootstrap/services/portal/src/assets/icon-delete.svg +3 -0
- package/bootstrap/services/portal/src/assets/icon-home.svg +3 -0
- package/bootstrap/services/portal/src/assets/icon-padlock.svg +3 -0
- package/bootstrap/services/portal/src/assets/icon-page.svg +6 -0
- package/bootstrap/services/portal/src/assets/icon-reports.svg +3 -0
- package/bootstrap/services/portal/src/assets/icon-user.svg +3 -0
- package/bootstrap/services/portal/src/assets/icon-users.svg +3 -0
- package/bootstrap/services/portal/src/assets/login-green-rad-back-1.jpg +0 -0
- package/bootstrap/services/portal/src/assets/react.svg +1 -0
- package/bootstrap/services/portal/src/components/CrudList/CrudList.jsx +85 -0
- package/bootstrap/services/portal/src/components/CrudList/CrudList.less +59 -0
- package/bootstrap/services/portal/src/components/CustomSelect/CustomSelect.jsx +81 -0
- package/bootstrap/services/portal/src/components/CustomSelect/CustomSelect.less +0 -0
- package/bootstrap/services/portal/src/components/LoginForm/LoginForm.jsx +58 -0
- package/bootstrap/services/portal/src/components/PageBlockSwitch/PageBlockSwitch.jsx +129 -0
- package/bootstrap/services/portal/src/components/Sidebar/Sidebar.jsx +33 -0
- package/bootstrap/services/portal/src/components/Sidebar/Sidebar.less +37 -0
- package/bootstrap/services/portal/src/components/Topbar/Topbar.jsx +19 -0
- package/bootstrap/services/portal/src/components/Topbar/Topbar.less +22 -0
- package/bootstrap/services/portal/src/components/UserInfo/UserInfo.jsx +33 -0
- package/bootstrap/services/portal/src/components/UserInfo/UserInfo.less +21 -0
- package/bootstrap/services/portal/src/css/style.css +711 -0
- package/bootstrap/services/portal/src/data/pages.data.js +10 -0
- package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.jsx +65 -0
- package/bootstrap/services/portal/src/elements/CustomSelect/CustomSelect.less +102 -0
- package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.jsx +115 -0
- package/bootstrap/services/portal/src/elements/ImageInput/ImageInput.less +43 -0
- package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.jsx +124 -0
- package/bootstrap/services/portal/src/elements/ImageMultiInput/ImageMultiInput.less +0 -0
- package/bootstrap/services/portal/src/elements/Repeater/Repeater.jsx +52 -0
- package/bootstrap/services/portal/src/elements/Repeater/Repeater.less +70 -0
- package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.jsx +18 -0
- package/bootstrap/services/portal/src/elements/RichTextInput/RichTextInput.less +37 -0
- package/bootstrap/services/portal/src/elements/SaveButton/SaveButton.jsx +45 -0
- package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.jsx +63 -0
- package/bootstrap/services/portal/src/elements/SelectRepeater/SelectRepeater.less +23 -0
- package/bootstrap/services/portal/src/elements/TextInput/TextInput.jsx +17 -0
- package/bootstrap/services/portal/src/layouts/Card/Card.jsx +15 -0
- package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.jsx +29 -0
- package/bootstrap/services/portal/src/layouts/PortalLayout/PortalLayout.less +49 -0
- package/bootstrap/services/portal/src/main.jsx +51 -0
- package/bootstrap/services/portal/src/pages/BlockSinglePage/BlockSinglePage.jsx +277 -0
- package/bootstrap/services/portal/src/pages/BlocksPage/BlocksPage.jsx +23 -0
- package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.jsx +11 -0
- package/bootstrap/services/portal/src/pages/DashboardPage/DashboardPage.less +0 -0
- package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.jsx +21 -0
- package/bootstrap/services/portal/src/pages/LoginPage/LoginPage.less +51 -0
- package/bootstrap/services/portal/src/pages/PageSinglePage/PageSinglePage.jsx +338 -0
- package/bootstrap/services/portal/src/pages/PagesPage/PagesPage.jsx +23 -0
- package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.jsx +9 -0
- package/bootstrap/services/portal/src/pages/UserSinglePage/UserSinglePage.less +0 -0
- package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.jsx +25 -0
- package/bootstrap/services/portal/src/pages/UsersPage/UsersPage.less +0 -0
- package/bootstrap/services/portal/src/services/block.js +28 -0
- package/bootstrap/services/portal/src/services/client.js +67 -0
- package/bootstrap/services/portal/src/services/gravatar.js +14 -0
- package/bootstrap/services/portal/src/services/page.js +28 -0
- package/bootstrap/services/portal/src/services/storage.js +62 -0
- package/bootstrap/services/portal/src/services/user.js +41 -0
- package/bootstrap/services/portal/src/slices/authSlice.js +101 -0
- package/bootstrap/services/portal/src/store/configureStore.js +10 -0
- package/bootstrap/services/portal/src/style/cards.less +57 -0
- package/bootstrap/services/portal/src/style/global.less +204 -0
- package/bootstrap/services/portal/src/style/icons.less +21 -0
- package/bootstrap/services/portal/src/style/inputs.less +52 -0
- package/bootstrap/services/portal/src/style/main.less +28 -0
- package/bootstrap/services/portal/src/utils/utils.js +9 -0
- package/bootstrap/services/portal/vite.config.js +12 -0
- package/bootstrap/services/user/src/app.js +6 -1
- package/bootstrap/services/user/src/commands/user.handler.js +0 -3
- package/bootstrap/services/user/src/config.js +5 -1
- package/bootstrap/services/user/src/policies/user.policy.js +3 -1
- package/bootstrap/services/user/src/tests/commands/user.test.js +22 -0
- package/install-from-clone.sh +30 -0
- package/package.json +1 -1
- package/src/cli.js +8 -0
- package/src/dev/commands.js +10 -2
- package/src/dev/dev.service.js +147 -60
- package/src/provisioner/Dockerfile +27 -0
- package/src/provisioner/package.json +19 -0
- package/src/provisioner/src/app.js +56 -0
- package/src/provisioner/src/services/mongodb.js +58 -0
- package/src/provisioner/src/services/mysql.js +51 -0
- package/src/provisioner/src/services/secrets.js +84 -0
- package/src/scaffolder/commands.js +1 -1
- package/src/scaffolder/scaffolder.handler.js +40 -15
- package/templates/service/src/app.js.hbs +12 -1
- package/templates/service/src/commands/{{serviceName}}.handler.js.hbs +1 -1
- package/templates/service/src/mongodb.config.js.hbs +5 -1
- package/templates/service/src/mysql.config.js.hbs +4 -0
- package/bootstrap/services/user/src/tests/user.test.js +0 -126
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import CustomSelect from "../CustomSelect/CustomSelect";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SelectRepeater component allows users to add, update, and remove items from a list using a select dropdown.
|
|
6
|
+
*
|
|
7
|
+
* Use setItems to manage state setting here, or use setItem override to manage state setting higher up
|
|
8
|
+
*/
|
|
9
|
+
const SelectRepeater = ({ items = [], setItems, setItem, renderRow, selectLabel, selectText, selectOptions, selectLabelKey }) => {
|
|
10
|
+
|
|
11
|
+
const addItem = (item) => {
|
|
12
|
+
if (setItem) {
|
|
13
|
+
setItem(item);
|
|
14
|
+
} else if (setItems) {
|
|
15
|
+
setItems([...items, item]);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const updateItem = (index, newItem) => {
|
|
20
|
+
if (setItem) {
|
|
21
|
+
setItem(newItem, index);
|
|
22
|
+
} else if (setItems) {
|
|
23
|
+
const newItems = [...items];
|
|
24
|
+
newItems[index] = newItem;
|
|
25
|
+
setItems(newItems);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const removeItem = (item, index) => {
|
|
30
|
+
if (setItem) {
|
|
31
|
+
const remove = true;
|
|
32
|
+
setItem(item, index, remove);
|
|
33
|
+
} else {
|
|
34
|
+
setItems(items.filter((_, i) => i !== index));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="repeater">
|
|
40
|
+
{items.length > 0 && items.map((item, index) =>
|
|
41
|
+
<div className="repeater-row" key={index}>
|
|
42
|
+
{renderRow(item, index, (newItem) => updateItem(index, newItem))}
|
|
43
|
+
<span onClick={() => removeItem(item, index)} className="icon-delete remove-repeater-row"></span>
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
<div className="button-cont">
|
|
47
|
+
<CustomSelect
|
|
48
|
+
label={selectLabel}
|
|
49
|
+
placeholder={selectText}
|
|
50
|
+
name="repeater-select"
|
|
51
|
+
options={selectOptions}
|
|
52
|
+
labelKey={selectLabelKey}
|
|
53
|
+
setSelectedOption={(option) => {
|
|
54
|
+
addItem(option);
|
|
55
|
+
}}
|
|
56
|
+
selectedOption={null}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default SelectRepeater;
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
function TextInput({ label, value, onChange, placeholder }) {
|
|
3
|
+
|
|
4
|
+
return (
|
|
5
|
+
<div className="text-input">
|
|
6
|
+
{label && <label>{label}</label>}
|
|
7
|
+
<input
|
|
8
|
+
type="text"
|
|
9
|
+
value={value}
|
|
10
|
+
onChange={onChange}
|
|
11
|
+
placeholder={placeholder}
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default TextInput;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
function Card({ title, children }) {
|
|
4
|
+
|
|
5
|
+
return (
|
|
6
|
+
<div className="card">
|
|
7
|
+
<h2 className="card-title">{title}</h2>
|
|
8
|
+
<div className="card-content">
|
|
9
|
+
{children}
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default Card;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Outlet } from "react-router-dom";
|
|
2
|
+
import gnarEngineLogo from '../../assets/gnar-engine-white-logo.svg';
|
|
3
|
+
import Topbar from "../../components/Topbar/Topbar";
|
|
4
|
+
import Sidebar from "../../components/Sidebar/Sidebar";
|
|
5
|
+
|
|
6
|
+
function PortalLayout() {
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<Topbar />
|
|
11
|
+
<div className="portal-main">
|
|
12
|
+
<div className="portal-main-inner flex-row">
|
|
13
|
+
<Sidebar />
|
|
14
|
+
<div className="portal-page">
|
|
15
|
+
<div className="portal-page-header">
|
|
16
|
+
<img src={gnarEngineLogo} className="logo" alt="Gnar Engine" />
|
|
17
|
+
</div>
|
|
18
|
+
<Outlet />
|
|
19
|
+
<div className="portal-page-footer">
|
|
20
|
+
<p>©2025 Gnar Engine. All rights reserved.</p>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default PortalLayout;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.portal-main {
|
|
2
|
+
background: @dark-2;
|
|
3
|
+
min-height: calc(100vh - 71px);
|
|
4
|
+
width: 100vw;
|
|
5
|
+
position: absolute;
|
|
6
|
+
top: 71px;
|
|
7
|
+
left: 0px;
|
|
8
|
+
border-top-left-radius: 15px;
|
|
9
|
+
border-top-right-radius: 15px;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
|
|
12
|
+
.portal-main-inner {
|
|
13
|
+
position: relative;
|
|
14
|
+
width: 100%;
|
|
15
|
+
min-height: calc(100vh - 71px);
|
|
16
|
+
gap: 0px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.portal-page {
|
|
20
|
+
width: 100%;
|
|
21
|
+
padding: 45px 45px 70px 45px;
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
position: relative;
|
|
24
|
+
|
|
25
|
+
.portal-page-header {
|
|
26
|
+
margin-bottom: 30px;
|
|
27
|
+
|
|
28
|
+
.logo {
|
|
29
|
+
max-width: 200px;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.portal-page-footer {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: row-reverse;
|
|
36
|
+
width: 100%;
|
|
37
|
+
position: absolute;
|
|
38
|
+
box-sizing: border-box;
|
|
39
|
+
bottom: 30px;
|
|
40
|
+
left: 0px;
|
|
41
|
+
padding: 0px 45px;
|
|
42
|
+
|
|
43
|
+
p, span {
|
|
44
|
+
margin: 0px;
|
|
45
|
+
color: @mid-grey;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import './css/style.css'
|
|
4
|
+
import App from './App.jsx'
|
|
5
|
+
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
|
6
|
+
import LoginPage from './pages/LoginPage/LoginPage.jsx'
|
|
7
|
+
import PortalLayout from './layouts/PortalLayout/PortalLayout.jsx'
|
|
8
|
+
import DashboardPage from './pages/DashboardPage/DashboardPage.jsx'
|
|
9
|
+
import UsersPage from './pages/UsersPage/UsersPage.jsx'
|
|
10
|
+
import UserSinglePage from './pages/UserSinglePage/UserSinglePage.jsx'
|
|
11
|
+
import PagesPage from './pages/PagesPage/PagesPage.jsx'
|
|
12
|
+
import PageSinglePage from './pages/PageSinglePage/PageSinglePage.jsx'
|
|
13
|
+
import BlocksPage from './pages/BlocksPage/BlocksPage.jsx'
|
|
14
|
+
import BlockSinglePage from './pages/BlockSinglePage/BlockSinglePage.jsx'
|
|
15
|
+
|
|
16
|
+
const router = createBrowserRouter([
|
|
17
|
+
{
|
|
18
|
+
path: "portal",
|
|
19
|
+
element: <App />,
|
|
20
|
+
children: [
|
|
21
|
+
{
|
|
22
|
+
path: "login",
|
|
23
|
+
element: <LoginPage />,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
path: "",
|
|
27
|
+
element: <PortalLayout />,
|
|
28
|
+
children: [
|
|
29
|
+
{ index: true, element: <DashboardPage /> },
|
|
30
|
+
{ path: "dashboard", element: <DashboardPage /> },
|
|
31
|
+
|
|
32
|
+
// Users
|
|
33
|
+
{ path: "users", element: <UsersPage /> },
|
|
34
|
+
{ path: "users/:id", element: <UserSinglePage /> },
|
|
35
|
+
|
|
36
|
+
// CMS / Pages
|
|
37
|
+
{ path: "pages", element: <PagesPage /> },
|
|
38
|
+
{ path: "pages/:id", element: <PageSinglePage /> },
|
|
39
|
+
{ path: "blocks", element: <BlocksPage /> },
|
|
40
|
+
{ path: "blocks/:id", element: <BlockSinglePage /> }
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
createRoot(document.getElementById('root')).render(
|
|
48
|
+
<StrictMode>
|
|
49
|
+
<RouterProvider router={router} />
|
|
50
|
+
</StrictMode>
|
|
51
|
+
)
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import Card from '../../layouts/Card/Card';
|
|
3
|
+
import Repeater from '../../elements/Repeater/Repeater';
|
|
4
|
+
import SaveButton from '../../elements/SaveButton/SaveButton';
|
|
5
|
+
import TextInput from '../../elements/TextInput/TextInput';
|
|
6
|
+
import CustomSelect from '../../elements/CustomSelect/CustomSelect';
|
|
7
|
+
import { blocks } from '../../services/block.js';
|
|
8
|
+
import { fieldTypes } from '../../data/pages.data.js';
|
|
9
|
+
import { useParams } from "react-router-dom";
|
|
10
|
+
|
|
11
|
+
function BlockSinglePage() {
|
|
12
|
+
|
|
13
|
+
const { id } = useParams();
|
|
14
|
+
const [blockId, setBlockId] = useState('');
|
|
15
|
+
const [blockFields, setBlockFields] = useState([]);
|
|
16
|
+
const [blockDetails, setBlockDetails] = useState({});
|
|
17
|
+
const [allBlocks, setAllBlocks] = useState([]);
|
|
18
|
+
const [errors, setErrors] = useState([]);
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
// fetch all blocks for repeater field type options
|
|
23
|
+
(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const blocksData = await blocks.getMany();
|
|
26
|
+
const blockOptions = (blocksData.blocks || []).map(block => ({
|
|
27
|
+
name: block.name,
|
|
28
|
+
value: block.key
|
|
29
|
+
}));
|
|
30
|
+
setAllBlocks(blockOptions || []);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Error fetching blocks data:', error);
|
|
33
|
+
setErrors([error]);
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
// fetch data if editing existing
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (id && id !== 'new') {
|
|
41
|
+
(async () => {
|
|
42
|
+
setLoading(true);
|
|
43
|
+
try {
|
|
44
|
+
let blockData = await blocks.getSingle(id);
|
|
45
|
+
blockData = blockData.block;
|
|
46
|
+
setBlockId(blockData.id);
|
|
47
|
+
setBlockDetails({
|
|
48
|
+
blockName: blockData.name,
|
|
49
|
+
blockKey: blockData.key
|
|
50
|
+
});
|
|
51
|
+
setBlockFields(blockData.fields || []);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Error fetching block data:', error);
|
|
54
|
+
setErrors([error]);
|
|
55
|
+
}
|
|
56
|
+
setLoading(false);
|
|
57
|
+
})();
|
|
58
|
+
}
|
|
59
|
+
}, [id]);
|
|
60
|
+
|
|
61
|
+
const validateField = (e) => {
|
|
62
|
+
const value = e.target.value;
|
|
63
|
+
setErrors([]);
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const save = async () => {
|
|
68
|
+
setLoading(true);
|
|
69
|
+
console.log('Saving...', { blockId, blockFields, blockDetails });
|
|
70
|
+
|
|
71
|
+
// create new
|
|
72
|
+
if (!blockId) {
|
|
73
|
+
try {
|
|
74
|
+
await blocks.create({
|
|
75
|
+
name: blockDetails.blockName,
|
|
76
|
+
key: blockDetails.blockKey,
|
|
77
|
+
fields: blockFields
|
|
78
|
+
})
|
|
79
|
+
setErrors([]);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Error creating block:', error);
|
|
82
|
+
setErrors([error]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// update existing
|
|
87
|
+
else {
|
|
88
|
+
confirm('Are you sure you would like to update this block? This could result in page content being removed when they are next updated.')
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await blocks.update(blockId, {
|
|
92
|
+
name: blockDetails.blockName,
|
|
93
|
+
key: blockDetails.blockKey,
|
|
94
|
+
fields: blockFields
|
|
95
|
+
});
|
|
96
|
+
setErrors([]);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Error creating block:', error);
|
|
99
|
+
setErrors([error]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setLoading(false);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="single-crud-page">
|
|
108
|
+
<h1>Create / Update Block</h1>
|
|
109
|
+
|
|
110
|
+
<div className="flex-row top-bar">
|
|
111
|
+
<p className="single-crud-id">{blockId || 'Creating new block...'}</p>
|
|
112
|
+
<div className="button-group">
|
|
113
|
+
<button onClick={() => window.history.back()} className="secondary-btn">Back</button>
|
|
114
|
+
<button onClick={save}>Save</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="card-columns">
|
|
119
|
+
<div className="col-66">
|
|
120
|
+
<Card
|
|
121
|
+
title="Block Fields"
|
|
122
|
+
>
|
|
123
|
+
<p className="instruction">Add fields to your block, save it and then use the block in your pages</p>
|
|
124
|
+
|
|
125
|
+
<Repeater
|
|
126
|
+
items={blockFields}
|
|
127
|
+
setItems={setBlockFields}
|
|
128
|
+
defaultItem= {{}}
|
|
129
|
+
buttonText= "Add Field"
|
|
130
|
+
renderRow= {(item, index, updateItem) => (
|
|
131
|
+
|
|
132
|
+
<div className="flex-row">
|
|
133
|
+
<TextInput
|
|
134
|
+
label="Field Name"
|
|
135
|
+
placeholder="Enter field name"
|
|
136
|
+
value={blockFields[index].name || ''}
|
|
137
|
+
onChange={(e) => {
|
|
138
|
+
validateField(e)
|
|
139
|
+
const blockField = { ...blockFields[index], name: e.target.value };
|
|
140
|
+
setBlockFields([
|
|
141
|
+
...blockFields.slice(0, index),
|
|
142
|
+
blockField,
|
|
143
|
+
...blockFields.slice(index + 1)
|
|
144
|
+
]);
|
|
145
|
+
}}
|
|
146
|
+
errorMessage="Invalid field name"
|
|
147
|
+
isValid={true}
|
|
148
|
+
/>
|
|
149
|
+
|
|
150
|
+
<TextInput
|
|
151
|
+
label="Field Key"
|
|
152
|
+
placeholder="Enter field key"
|
|
153
|
+
value={blockFields[index].key || ''}
|
|
154
|
+
onChange={(e) => {
|
|
155
|
+
validateField(e)
|
|
156
|
+
const blockField = { ...blockFields[index], key: e.target.value };
|
|
157
|
+
setBlockFields([
|
|
158
|
+
...blockFields.slice(0, index),
|
|
159
|
+
blockField,
|
|
160
|
+
...blockFields.slice(index + 1)
|
|
161
|
+
]);
|
|
162
|
+
}}
|
|
163
|
+
errorMessage="Invalid field key"
|
|
164
|
+
isValid={true}
|
|
165
|
+
/>
|
|
166
|
+
|
|
167
|
+
<CustomSelect
|
|
168
|
+
label="Field Type"
|
|
169
|
+
placeholder="Select Field Type"
|
|
170
|
+
name={`field-type-select-${index}`}
|
|
171
|
+
options={fieldTypes}
|
|
172
|
+
labelKey="name"
|
|
173
|
+
setSelectedOption={(option) => {
|
|
174
|
+
const blockField = { ...blockFields[index], type: option.value };
|
|
175
|
+
setBlockFields([
|
|
176
|
+
...blockFields.slice(0, index),
|
|
177
|
+
blockField,
|
|
178
|
+
...blockFields.slice(index + 1)
|
|
179
|
+
]);
|
|
180
|
+
}}
|
|
181
|
+
selectedOption={fieldTypes.find(ft => ft.value === (blockFields[index].type || ''))}
|
|
182
|
+
/>
|
|
183
|
+
|
|
184
|
+
{allBlocks && blockFields[index].type === 'repeater' &&
|
|
185
|
+
<CustomSelect
|
|
186
|
+
label="Repeater Type"
|
|
187
|
+
placeholder="Select Repeater Type"
|
|
188
|
+
name={`repeater-type-select-${index}`}
|
|
189
|
+
options={allBlocks}
|
|
190
|
+
labelKey="name"
|
|
191
|
+
setSelectedOption={(option) => {
|
|
192
|
+
const blockField = { ...blockFields[index], repeaterType: option.value };
|
|
193
|
+
console.log('blockField', blockField);
|
|
194
|
+
|
|
195
|
+
setBlockFields([
|
|
196
|
+
...blockFields.slice(0, index),
|
|
197
|
+
blockField,
|
|
198
|
+
...blockFields.slice(index + 1)
|
|
199
|
+
]);
|
|
200
|
+
}}
|
|
201
|
+
selectedOption={allBlocks.find(ft => ft.value === (blockFields[index].repeaterType || ''))}
|
|
202
|
+
/>
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
/>
|
|
208
|
+
</Card>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div className="col-33">
|
|
212
|
+
<Card
|
|
213
|
+
title="Block details"
|
|
214
|
+
>
|
|
215
|
+
<div className="flex-col">
|
|
216
|
+
<TextInput
|
|
217
|
+
label="Block Name"
|
|
218
|
+
placeholder="Enter block Name"
|
|
219
|
+
value={blockDetails.blockName || ''}
|
|
220
|
+
onChange={(e) => {
|
|
221
|
+
validateField(e)
|
|
222
|
+
setBlockDetails({
|
|
223
|
+
...blockDetails,
|
|
224
|
+
blockName: e.target.value
|
|
225
|
+
})
|
|
226
|
+
}}
|
|
227
|
+
errorMessage="Invalid block name"
|
|
228
|
+
isValid={true}
|
|
229
|
+
/>
|
|
230
|
+
|
|
231
|
+
<TextInput
|
|
232
|
+
label="Block Key"
|
|
233
|
+
placeholder="Enter block key"
|
|
234
|
+
value={blockDetails.blockKey || ''}
|
|
235
|
+
onChange={(e) => {
|
|
236
|
+
validateField(e)
|
|
237
|
+
setBlockDetails({
|
|
238
|
+
...blockDetails,
|
|
239
|
+
blockKey: e.target.value
|
|
240
|
+
})
|
|
241
|
+
}}
|
|
242
|
+
errorMessage="Invalid block key"
|
|
243
|
+
isValid={true}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
</Card>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div className="flex-row bottom-bar">
|
|
251
|
+
<div className="button-group">
|
|
252
|
+
<SaveButton
|
|
253
|
+
onClick={save}
|
|
254
|
+
itemName="Block"
|
|
255
|
+
loading={loading}
|
|
256
|
+
error={errors.length > 0}
|
|
257
|
+
isNew={!blockId}
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div>
|
|
263
|
+
{errors.length > 0 && (
|
|
264
|
+
<div className="error-messages">
|
|
265
|
+
<ul>
|
|
266
|
+
{errors.map((error, index) => (
|
|
267
|
+
<li key={index}>{error.message}</li>
|
|
268
|
+
))}
|
|
269
|
+
</ul>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export default BlockSinglePage;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import CrudList from '../../components/CrudList/CrudList';
|
|
2
|
+
import { blocks } from '../../services/block.js';
|
|
3
|
+
|
|
4
|
+
function BlocksPage() {
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<div>
|
|
8
|
+
<h1>Manage Blocks</h1>
|
|
9
|
+
<CrudList
|
|
10
|
+
entityKey="blocks"
|
|
11
|
+
fetchData={blocks.getMany}
|
|
12
|
+
entitySingleName="Block"
|
|
13
|
+
entityPluralName="Blocks"
|
|
14
|
+
columns={[
|
|
15
|
+
{ key: 'id', label: 'ID' },
|
|
16
|
+
{ key: 'name', label: 'Block name' }
|
|
17
|
+
]}
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default BlocksPage;
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import LoginForm from "../../components/LoginForm/LoginForm";
|
|
2
|
+
import gnarEngineLogo from '../../assets/gnar-engine-white-logo.svg';
|
|
3
|
+
|
|
4
|
+
function LoginPage() {
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<div className="login-page flex-row">
|
|
8
|
+
<div className="login-left-panel">
|
|
9
|
+
<img src={gnarEngineLogo} className="logo" alt="Gnar Engine" />
|
|
10
|
+
</div>
|
|
11
|
+
<div className="login-right-panel">
|
|
12
|
+
<div>
|
|
13
|
+
<h1>Admin Login</h1>
|
|
14
|
+
<LoginForm />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default LoginPage;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
.login-page {
|
|
2
|
+
width: 100%;
|
|
3
|
+
max-width: 100%;
|
|
4
|
+
height: 100vh;
|
|
5
|
+
gap: 0px;
|
|
6
|
+
|
|
7
|
+
.login-left-panel {
|
|
8
|
+
min-width: 50%;
|
|
9
|
+
background: url('../assets/login-green-rad-back-1.jpg');
|
|
10
|
+
background-size: cover;
|
|
11
|
+
background-position: center center;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.login-right-panel {
|
|
18
|
+
min-width: 50%;
|
|
19
|
+
background: @dark-1;
|
|
20
|
+
padding: 135px;
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.username,
|
|
28
|
+
.password {
|
|
29
|
+
padding-left: 50px;
|
|
30
|
+
position: relative;
|
|
31
|
+
background-repeat: no-repeat;
|
|
32
|
+
background-position: 12px center;
|
|
33
|
+
background-size: 18px 18px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.username {
|
|
37
|
+
background-image: url('../assets/icon-user.svg');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.password {
|
|
41
|
+
background-image: url('../assets/icon-padlock.svg');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.login-form {
|
|
45
|
+
max-width: 450px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.forgotten-password {
|
|
49
|
+
width: 100%;
|
|
50
|
+
}
|
|
51
|
+
}
|