diff --git a/._env b/._env deleted file mode 100644 index e69de29..0000000 diff --git a/.env b/.env new file mode 100644 index 0000000..f1644a5 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +PORT=5005 +NODE_ENV=development \ No newline at end of file diff --git a/.gitignore b/.gitignore index 477ae0c..ff227fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ node_modules/ -data/ -.env \ No newline at end of file +data/ \ No newline at end of file diff --git a/README.md b/README.md index 1e103ed..804ab86 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![Homescreen screenshot](./github/_home.png) ## Description -Flame is self-hosted startpage for your server. It's inspired (heavily) by [SUI](https://github.com/jeroenpardon/sui) +Flame is self-hosted startpage for your server. Its design is inspired (heavily) by [SUI](https://github.com/jeroenpardon/sui). Flame is very easy to setup and use. With built-in editors it allows you to setup your very own appliaction hub in no time - no file editing necessary. ## Technology - Backend @@ -23,6 +23,7 @@ Flame is self-hosted startpage for your server. It's inspired (heavily) by [SUI] ## Development ```sh +# clone repository git clone https://github.com/pawelmalak/flame cd flame @@ -33,13 +34,23 @@ npm run dev-init npm run dev ``` -## Deployment with Docker +## Building Docker images ```sh -# build image +# build image for amd64 only docker build -t flame . +# build multiarch image for amd64, armv7 and arm64 +# building failed multiple times with 2GB memory usage limit so you might want to increase it +docker buildx build \ + --platform linux/arm/v7,linux/arm64,linux/amd64 \ + -f Dockerfile.multiarch \ + -t flame:multiarch . +``` + +## Deployment with Docker +```sh # run container -docker run -p 5005:5005 -v :/app/data flame +docker run -p 5005:5005 -v /path/to/data:/app/data flame ``` ## Functionality @@ -73,4 +84,9 @@ docker run -p 5005:5005 -v :/app/data flame - Redirect: `https://{dest}` - URL without protocol - Format: `www.domain.com`, `domain.com`, `sub.domain.com`, `local`, `ip`, `ip:port` - - Redirect: `http://{dest}` \ No newline at end of file + - Redirect: `http://{dest}` + +## Support +If you want to support development of Flame and my upcoming self-hosted and open source projects you can use the following link: + +[![PayPal Badge](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.com/paypalme/pawelmalak) \ No newline at end of file diff --git a/client/.env b/client/.env index 314d636..5a3822f 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.3.1 +REACT_APP_VERSION=1.4.0 \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 476f8d5..66b371f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2397,6 +2397,14 @@ "csstype": "^3.0.2" } }, + "@types/react-beautiful-dnd": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz", + "integrity": "sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==", + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "17.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz", @@ -4614,6 +4622,14 @@ "postcss": "^7.0.5" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -9932,6 +9948,11 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -12300,6 +12321,11 @@ "performance-now": "^2.1.0" } }, + "raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -12362,6 +12388,20 @@ "whatwg-fetch": "^3.4.1" } }, + "react-beautiful-dnd": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", + "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", + "requires": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-dev-utils": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.4.tgz", @@ -15077,6 +15117,11 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-memo-one": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", + "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==" + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/client/package.json b/client/package.json index 5467e68..832d079 100644 --- a/client/package.json +++ b/client/package.json @@ -11,12 +11,14 @@ "@types/jest": "^26.0.23", "@types/node": "^12.20.12", "@types/react": "^17.0.5", + "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "^17.0.3", "@types/react-redux": "^7.1.16", "@types/react-router-dom": "^5.1.7", "axios": "^0.21.1", "http-proxy-middleware": "^2.0.0", "react": "^17.0.2", + "react-beautiful-dnd": "^13.1.0", "react-dom": "^17.0.2", "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index 7210f3d..157206e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,6 +5,10 @@ import { getConfig, setTheme } from './store/actions'; import { store } from './store/store'; import { Provider } from 'react-redux'; +// Utils +import { checkVersion } from './utility'; + +// Routes import Home from './components/Home/Home'; import Apps from './components/Apps/Apps'; import Settings from './components/Settings/Settings'; @@ -14,10 +18,14 @@ import NotificationCenter from './components/NotificationCenter/NotificationCent // Get config pairs from database store.dispatch(getConfig()); +// Set theme if (localStorage.theme) { store.dispatch(setTheme(localStorage.theme)); } +// Check for updates +checkVersion(); + const App = (): JSX.Element => { return ( diff --git a/client/src/components/Apps/AppTable/AppTable.module.css b/client/src/components/Apps/AppTable/AppTable.module.css index fc79b68..8b1e0ed 100644 --- a/client/src/components/Apps/AppTable/AppTable.module.css +++ b/client/src/components/Apps/AppTable/AppTable.module.css @@ -9,4 +9,21 @@ .TableAction:hover { cursor: pointer; +} + +.Message { + width: 100%; + display: flex; + justify-content: center; + align-items: baseline; + color: var(--color-primary); + margin-bottom: 20px; +} + +.Message a { + color: var(--color-accent); +} + +.Message a:hover { + cursor: pointer; } \ No newline at end of file diff --git a/client/src/components/Apps/AppTable/AppTable.tsx b/client/src/components/Apps/AppTable/AppTable.tsx index 728e079..6ef6e6c 100644 --- a/client/src/components/Apps/AppTable/AppTable.tsx +++ b/client/src/components/Apps/AppTable/AppTable.tsx @@ -1,20 +1,52 @@ -import { KeyboardEvent } from 'react'; -import { connect } from 'react-redux'; -import { App, GlobalState } from '../../../interfaces'; -import { pinApp, deleteApp } from '../../../store/actions'; +import { Fragment, KeyboardEvent, useState, useEffect } from 'react'; +import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; +import { Link } from 'react-router-dom'; +// Redux +import { connect } from 'react-redux'; +import { pinApp, deleteApp, reorderApps, updateConfig, createNotification } from '../../../store/actions'; + +// Typescript +import { App, GlobalState, NewNotification } from '../../../interfaces'; + +// CSS import classes from './AppTable.module.css'; + +// UI import Icon from '../../UI/Icons/Icon/Icon'; import Table from '../../UI/Table/Table'; +// Utils +import { searchConfig } from '../../../utility'; + interface ComponentProps { apps: App[]; pinApp: (app: App) => void; deleteApp: (id: number) => void; updateAppHandler: (app: App) => void; + reorderApps: (apps: App[]) => void; + updateConfig: (formData: any) => void; + createNotification: (notification: NewNotification) => void; } const AppTable = (props: ComponentProps): JSX.Element => { + const [localApps, setLocalApps] = useState([]); + const [isCustomOrder, setIsCustomOrder] = useState(false); + + // Copy apps array + useEffect(() => { + setLocalApps([...props.apps]); + }, [props.apps]) + + // Check ordering + useEffect(() => { + const order = searchConfig('useOrdering', ''); + + if (order === 'orderId') { + setIsCustomOrder(true); + } + }, []) + const deleteAppHandler = (app: App): void => { const proceed = window.confirm(`Are you sure you want to delete ${app.name} at ${app.url} ?`); @@ -23,55 +55,111 @@ const AppTable = (props: ComponentProps): JSX.Element => { } } + // Support keyboard navigation for actions const keyboardActionHandler = (e: KeyboardEvent, app: App, handler: Function) => { if (e.key === 'Enter') { handler(app); } } + const dragEndHanlder = (result: DropResult): void => { + if (!isCustomOrder) { + props.createNotification({ + title: 'Error', + message: 'Custom order is disabled' + }) + return; + } + + if (!result.destination) { + return; + } + + const tmpApps = [...localApps]; + const [movedApp] = tmpApps.splice(result.source.index, 1); + tmpApps.splice(result.destination.index, 0, movedApp); + + setLocalApps(tmpApps); + props.reorderApps(tmpApps); + } + return ( - - {props.apps.map((app: App): JSX.Element => { - return ( - - - - - - - ) - })} -
{app.name}{app.url}{app.icon} -
deleteAppHandler(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} - tabIndex={0}> - -
-
props.updateAppHandler(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} - tabIndex={0}> - -
-
props.pinApp(app)} - onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)} - tabIndex={0}> - {app.isPinned - ? - : - } -
-
+ +
+ {isCustomOrder + ?

You can drag and drop single rows to reorder application

+ :

Custom order is disabled. You can change it in settings

+ } +
+ + + {(provided) => ( + + {localApps.map((app: App, index): JSX.Element => { + return ( + + {(provided, snapshot) => { + const style = { + border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none', + borderRadius: '4px', + ...provided.draggableProps.style, + }; + + return ( + + + + + {!snapshot.isDragging && ( + + )} + + ) + }} + + ) + })} +
{app.name}{app.url}{app.icon} +
deleteAppHandler(app)} + onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} + tabIndex={0}> + +
+
props.updateAppHandler(app)} + onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} + tabIndex={0}> + +
+
props.pinApp(app)} + onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)} + tabIndex={0}> + {app.isPinned + ? + : + } +
+
+ )} +
+
+
) } @@ -81,4 +169,12 @@ const mapStateToProps = (state: GlobalState) => { } } -export default connect(mapStateToProps, { pinApp, deleteApp })(AppTable); \ No newline at end of file +const actions = { + pinApp, + deleteApp, + reorderApps, + updateConfig, + createNotification +} + +export default connect(mapStateToProps, actions)(AppTable); \ No newline at end of file diff --git a/client/src/components/Apps/Apps.tsx b/client/src/components/Apps/Apps.tsx index 9f3adff..88c3fff 100644 --- a/client/src/components/Apps/Apps.tsx +++ b/client/src/components/Apps/Apps.tsx @@ -44,6 +44,7 @@ const Apps = (props: ComponentProps): JSX.Element => { url: 'string', icon: 'string', isPinned: false, + orderId: 0, id: 0, createdAt: new Date(), updatedAt: new Date() diff --git a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.module.css b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.module.css index fc79b68..8b1e0ed 100644 --- a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.module.css +++ b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.module.css @@ -9,4 +9,21 @@ .TableAction:hover { cursor: pointer; +} + +.Message { + width: 100%; + display: flex; + justify-content: center; + align-items: baseline; + color: var(--color-primary); + margin-bottom: 20px; +} + +.Message a { + color: var(--color-accent); +} + +.Message a:hover { + cursor: pointer; } \ No newline at end of file diff --git a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx index 1d319fb..02779d5 100644 --- a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx +++ b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx @@ -1,13 +1,25 @@ -import { ContentType } from '../Bookmarks'; -import classes from './BookmarkTable.module.css'; -import { connect } from 'react-redux'; -import { pinCategory, deleteCategory, deleteBookmark } from '../../../store/actions'; -import { KeyboardEvent } from 'react'; +import { KeyboardEvent, useState, useEffect, Fragment } from 'react'; +import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; +import { Link } from 'react-router-dom'; +// Redux +import { connect } from 'react-redux'; +import { pinCategory, deleteCategory, deleteBookmark, createNotification, reorderCategories } from '../../../store/actions'; + +// Typescript +import { Bookmark, Category, NewNotification } from '../../../interfaces'; +import { ContentType } from '../Bookmarks'; + +// CSS +import classes from './BookmarkTable.module.css'; + +// UI import Table from '../../UI/Table/Table'; -import { Bookmark, Category } from '../../../interfaces'; import Icon from '../../UI/Icons/Icon/Icon'; +// Utils +import { searchConfig } from '../../../utility'; + interface ComponentProps { contentType: ContentType; categories: Category[]; @@ -15,9 +27,28 @@ interface ComponentProps { deleteCategory: (id: number) => void; updateHandler: (data: Category | Bookmark) => void; deleteBookmark: (bookmarkId: number, categoryId: number) => void; + createNotification: (notification: NewNotification) => void; + reorderCategories: (categories: Category[]) => void; } const BookmarkTable = (props: ComponentProps): JSX.Element => { + const [localCategories, setLocalCategories] = useState([]); + const [isCustomOrder, setIsCustomOrder] = useState(false); + + // Copy categories array + useEffect(() => { + setLocalCategories([...props.categories]); + }, [props.categories]) + + // Check ordering + useEffect(() => { + const order = searchConfig('useOrdering', ''); + + if (order === 'orderId') { + setIsCustomOrder(true); + } + }) + const deleteCategoryHandler = (category: Category): void => { const proceed = window.confirm(`Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`); @@ -40,46 +71,100 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => { } } + const dragEndHanlder = (result: DropResult): void => { + if (!isCustomOrder) { + props.createNotification({ + title: 'Error', + message: 'Custom order is disabled' + }) + return; + } + + if (!result.destination) { + return; + } + + const tmpCategories = [...localCategories]; + const [movedApp] = tmpCategories.splice(result.source.index, 1); + tmpCategories.splice(result.destination.index, 0, movedApp); + + setLocalCategories(tmpCategories); + props.reorderCategories(tmpCategories); + } + if (props.contentType === ContentType.category) { return ( - - {props.categories.map((category: Category) => { - return ( - - - - - ) - })} -
{category.name} -
deleteCategoryHandler(category)} - onKeyDown={(e) => keyboardActionHandler(e, category, deleteCategoryHandler)} - tabIndex={0}> - -
-
props.updateHandler(category)} - // onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} - tabIndex={0}> - -
-
props.pinCategory(category)} - onKeyDown={(e) => keyboardActionHandler(e, category, props.pinCategory)} - tabIndex={0}> - {category.isPinned - ? - : - } -
-
+ +
+ {isCustomOrder + ?

You can drag and drop single rows to reorder categories

+ :

Custom order is disabled. You can change it in settings

+ } +
+ + + {(provided) => ( + + {localCategories.map((category: Category, index): JSX.Element => { + return ( + + {(provided, snapshot) => { + const style = { + border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none', + borderRadius: '4px', + ...provided.draggableProps.style, + }; + + return ( + + + {!snapshot.isDragging && ( + + )} + + ) + }} + + ) + })} +
{category.name} +
deleteCategoryHandler(category)} + onKeyDown={(e) => keyboardActionHandler(e, category, deleteCategoryHandler)} + tabIndex={0}> + +
+
props.updateHandler(category)} + tabIndex={0}> + +
+
props.pinCategory(category)} + onKeyDown={(e) => keyboardActionHandler(e, category, props.pinCategory)} + tabIndex={0}> + {category.isPinned + ? + : + } +
+
+ )} +
+
+
) } else { const bookmarks: {bookmark: Bookmark, categoryName: string}[] = []; @@ -111,14 +196,12 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
deleteBookmarkHandler(bookmark.bookmark)} - // onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)} tabIndex={0}>
props.updateHandler(bookmark.bookmark)} - // onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)} tabIndex={0}>
@@ -131,4 +214,12 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => { } } -export default connect(null, { pinCategory, deleteCategory, deleteBookmark })(BookmarkTable); \ No newline at end of file +const actions = { + pinCategory, + deleteCategory, + deleteBookmark, + createNotification, + reorderCategories +} + +export default connect(null, actions)(BookmarkTable); \ No newline at end of file diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx index 21f37b3..7a2deb2 100644 --- a/client/src/components/Bookmarks/Bookmarks.tsx +++ b/client/src/components/Bookmarks/Bookmarks.tsx @@ -43,6 +43,7 @@ const Bookmarks = (props: ComponentProps): JSX.Element => { name: '', id: -1, isPinned: false, + orderId: 0, bookmarks: [], createdAt: new Date(), updatedAt: new Date() diff --git a/client/src/components/Settings/AppDetails/AppDetails.module.css b/client/src/components/Settings/AppDetails/AppDetails.module.css new file mode 100644 index 0000000..8f5fae3 --- /dev/null +++ b/client/src/components/Settings/AppDetails/AppDetails.module.css @@ -0,0 +1,8 @@ +.AppVersion { + color: var(--color-primary); + margin-bottom: 15px; +} + +.AppVersion a { + color: var(--color-accent); +} \ No newline at end of file diff --git a/client/src/components/Settings/AppDetails/AppDetails.tsx b/client/src/components/Settings/AppDetails/AppDetails.tsx new file mode 100644 index 0000000..90fe2fb --- /dev/null +++ b/client/src/components/Settings/AppDetails/AppDetails.tsx @@ -0,0 +1,25 @@ +import { Fragment } from 'react'; + +import classes from './AppDetails.module.css'; +import Button from '../../UI/Buttons/Button/Button'; +import { checkVersion } from '../../../utility'; + +const AppDetails = (): JSX.Element => { + return ( + +

+ + Flame + + {' '} + version {process.env.REACT_APP_VERSION} +

+ +
+ ) +} + +export default AppDetails; \ No newline at end of file diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index 5df8be7..bba197d 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; // Redux import { connect } from 'react-redux'; -import { createNotification, updateConfig } from '../../../store/actions'; +import { createNotification, updateConfig, sortApps, sortCategories } from '../../../store/actions'; // Typescript import { GlobalState, NewNotification, SettingsForm } from '../../../interfaces'; @@ -17,6 +17,8 @@ import { searchConfig } from '../../../utility'; interface ComponentProps { createNotification: (notification: NewNotification) => void; updateConfig: (formData: SettingsForm) => void; + sortApps: () => void; + sortCategories: () => void; loading: boolean; } @@ -26,7 +28,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { customTitle: document.title, pinAppsByDefault: 1, pinCategoriesByDefault: 1, - hideHeader: 0 + hideHeader: 0, + useOrdering: 'createdAt' }) // Get config @@ -35,7 +38,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { customTitle: searchConfig('customTitle', 'Flame'), pinAppsByDefault: searchConfig('pinAppsByDefault', 1), pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1), - hideHeader: searchConfig('hideHeader', 0) + hideHeader: searchConfig('hideHeader', 0), + useOrdering: searchConfig('useOrdering', 'createdAt') }) }, [props.loading]); @@ -46,8 +50,12 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { // Save settings await props.updateConfig(formData); - // update local page title + // Update local page title document.title = formData.customTitle; + + // Sort apps and categories with new settings + props.sortApps(); + props.sortCategories(); } // Input handler @@ -113,6 +121,19 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { + + + + ) @@ -124,4 +145,11 @@ const mapStateToProps = (state: GlobalState) => { } } -export default connect(mapStateToProps, { createNotification, updateConfig })(OtherSettings); \ No newline at end of file +const actions = { + createNotification, + updateConfig, + sortApps, + sortCategories +} + +export default connect(mapStateToProps, actions)(OtherSettings); \ No newline at end of file diff --git a/client/src/components/Settings/Settings.tsx b/client/src/components/Settings/Settings.tsx index 0abea20..49b08bd 100644 --- a/client/src/components/Settings/Settings.tsx +++ b/client/src/components/Settings/Settings.tsx @@ -1,12 +1,14 @@ -import { NavLink, Link, Switch, Route, withRouter } from 'react-router-dom'; +import { NavLink, Link, Switch, Route } from 'react-router-dom'; import classes from './Settings.module.css'; import { Container } from '../UI/Layout/Layout'; import Headline from '../UI/Headlines/Headline/Headline'; + import Themer from '../Themer/Themer'; import WeatherSettings from './WeatherSettings/WeatherSettings'; import OtherSettings from './OtherSettings/OtherSettings'; +import AppDetails from './AppDetails/AppDetails'; const Settings = (): JSX.Element => { return ( @@ -38,12 +40,20 @@ const Settings = (): JSX.Element => { to='/settings/other'> Other + + App +
+
@@ -51,4 +61,4 @@ const Settings = (): JSX.Element => { ) } -export default withRouter(Settings); \ No newline at end of file +export default Settings; \ No newline at end of file diff --git a/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx b/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx index 912aced..1378d44 100644 --- a/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx +++ b/client/src/components/Settings/WeatherSettings/WeatherSettings.tsx @@ -116,6 +116,8 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => { placeholder='52.22' value={formData.lat} onChange={(e) => inputChangeHandler(e, true)} + step='any' + lang='en-150' /> You can use @@ -135,6 +137,8 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => { placeholder='21.01' value={formData.long} onChange={(e) => inputChangeHandler(e, true)} + step='any' + lang='en-150' /> diff --git a/client/src/components/UI/Buttons/Button/Button.module.css b/client/src/components/UI/Buttons/Button/Button.module.css index b9874e1..238850b 100644 --- a/client/src/components/UI/Buttons/Button/Button.module.css +++ b/client/src/components/UI/Buttons/Button/Button.module.css @@ -6,8 +6,7 @@ border-radius: 4px; } -.Button:hover, -.Button:focus { +.Button:hover { cursor: pointer; background-color: var(--color-accent); color: var(--color-background); diff --git a/client/src/components/UI/Buttons/Button/Button.tsx b/client/src/components/UI/Buttons/Button/Button.tsx index 321e993..5f113d8 100644 --- a/client/src/components/UI/Buttons/Button/Button.tsx +++ b/client/src/components/UI/Buttons/Button/Button.tsx @@ -2,10 +2,20 @@ import classes from './Button.module.css'; interface ComponentProps { children: string; + click?: any; } const Button = (props: ComponentProps): JSX.Element => { - return + const { + children, + click + } = props; + + return ( + + ) } export default Button; \ No newline at end of file diff --git a/client/src/components/UI/Table/Table.module.css b/client/src/components/UI/Table/Table.module.css index 33b712b..9700fc8 100644 --- a/client/src/components/UI/Table/Table.module.css +++ b/client/src/components/UI/Table/Table.module.css @@ -8,15 +8,17 @@ text-align: left; font-size: 16px; color: var(--color-primary); + table-layout: fixed; } .Table th, .Table td { padding: 10px; + overflow: hidden; + text-overflow: ellipsis; } /* Head */ - .Table th { --header-radius: 4px; background-color: var(--color-primary); @@ -34,8 +36,6 @@ } /* Body */ - .Table td { - /* opacity: 0.5; */ transition: all 0.2s; } \ No newline at end of file diff --git a/client/src/components/UI/Table/Table.tsx b/client/src/components/UI/Table/Table.tsx index 882ebfb..d425dc1 100644 --- a/client/src/components/UI/Table/Table.tsx +++ b/client/src/components/UI/Table/Table.tsx @@ -3,11 +3,12 @@ import classes from './Table.module.css'; interface ComponentProps { children: JSX.Element | JSX.Element[]; headers: string[]; + innerRef?: any; } const Table = (props: ComponentProps): JSX.Element => { return ( -
+
diff --git a/client/src/interfaces/App.ts b/client/src/interfaces/App.ts index ed8842a..e4314a5 100644 --- a/client/src/interfaces/App.ts +++ b/client/src/interfaces/App.ts @@ -5,6 +5,7 @@ export interface App extends Model { url: string; icon: string; isPinned: boolean; + orderId: number; } export interface NewApp { diff --git a/client/src/interfaces/Category.ts b/client/src/interfaces/Category.ts index 926987d..0f9f8f9 100644 --- a/client/src/interfaces/Category.ts +++ b/client/src/interfaces/Category.ts @@ -3,6 +3,7 @@ import { Model, Bookmark } from '.'; export interface Category extends Model { name: string; isPinned: boolean; + orderId: number; bookmarks: Bookmark[]; } diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index 52f62c8..360dae1 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -10,4 +10,5 @@ export interface SettingsForm { pinAppsByDefault: number; pinCategoriesByDefault: number; hideHeader: number; + useOrdering: string; } \ No newline at end of file diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts index d2cc17e..4324834 100644 --- a/client/src/store/actions/actionTypes.ts +++ b/client/src/store/actions/actionTypes.ts @@ -7,12 +7,16 @@ import { AddAppAction, DeleteAppAction, UpdateAppAction, + ReorderAppsAction, + SortAppsAction, // Categories GetCategoriesAction, AddCategoryAction, PinCategoryAction, DeleteCategoryAction, UpdateCategoryAction, + SortCategoriesAction, + ReorderCategoriesAction, // Bookmarks AddBookmarkAction, DeleteBookmarkAction, @@ -37,6 +41,8 @@ export enum ActionTypes { addAppSuccess = 'ADD_APP_SUCCESS', deleteApp = 'DELETE_APP', updateApp = 'UPDATE_APP', + reorderApps = 'REORDER_APPS', + sortApps = 'SORT_APPS', // Categories getCategories = 'GET_CATEGORIES', getCategoriesSuccess = 'GET_CATEGORIES_SUCCESS', @@ -45,6 +51,8 @@ export enum ActionTypes { pinCategory = 'PIN_CATEGORY', deleteCategory = 'DELETE_CATEGORY', updateCategory = 'UPDATE_CATEGORY', + sortCategories = 'SORT_CATEGORIES', + reorderCategories = 'REORDER_CATEGORIES', // Bookmarks addBookmark = 'ADD_BOOKMARK', deleteBookmark = 'DELETE_BOOKMARK', @@ -66,12 +74,16 @@ export type Action = AddAppAction | DeleteAppAction | UpdateAppAction | + ReorderAppsAction | + SortAppsAction | // Categories GetCategoriesAction | AddCategoryAction | PinCategoryAction | DeleteCategoryAction | UpdateCategoryAction | + SortCategoriesAction | + ReorderCategoriesAction | // Bookmarks AddBookmarkAction | DeleteBookmarkAction | diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts index a651cd2..97db1c7 100644 --- a/client/src/store/actions/app.ts +++ b/client/src/store/actions/app.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { Dispatch } from 'redux'; import { ActionTypes } from './actionTypes'; -import { App, ApiResponse, NewApp } from '../../interfaces'; +import { App, ApiResponse, NewApp, Config } from '../../interfaces'; import { CreateNotificationAction } from './notification'; export interface GetAppsAction { @@ -73,10 +73,13 @@ export const addApp = (formData: NewApp) => async (dispatch: Dispatch) => { } }) - dispatch({ + await dispatch({ type: ActionTypes.addAppSuccess, payload: res.data.data }) + + // Sort apps + dispatch(sortApps()) } catch (err) { console.log(err); } @@ -125,10 +128,63 @@ export const updateApp = (id: number, formData: NewApp) => async (dispatch: Disp } }) - dispatch({ + await dispatch({ type: ActionTypes.updateApp, payload: res.data.data }) + + // Sort apps + dispatch(sortApps()) + } catch (err) { + console.log(err); + } +} + +export interface ReorderAppsAction { + type: ActionTypes.reorderApps; + payload: App[] +} + +interface ReorderQuery { + apps: { + id: number; + orderId: number; + }[] +} + +export const reorderApps = (apps: App[]) => async (dispatch: Dispatch) => { + try { + const updateQuery: ReorderQuery = { apps: [] } + + apps.forEach((app, index) => updateQuery.apps.push({ + id: app.id, + orderId: index + 1 + })) + + await axios.put>('/api/apps/0/reorder', updateQuery); + + dispatch({ + type: ActionTypes.reorderApps, + payload: apps + }) + } catch (err) { + console.log(err); + } +} + +export interface SortAppsAction { + type: ActionTypes.sortApps; + payload: string; +} + +export const sortApps = () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/config/useOrdering'); + + dispatch({ + type: ActionTypes.sortApps, + payload: res.data.data.value + }) } catch (err) { console.log(err); } diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts index 9608ebc..0398bbb 100644 --- a/client/src/store/actions/bookmark.ts +++ b/client/src/store/actions/bookmark.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { Dispatch } from 'redux'; import { ActionTypes } from './actionTypes'; -import { Category, ApiResponse, NewCategory, Bookmark, NewBookmark } from '../../interfaces'; +import { Category, ApiResponse, NewCategory, Bookmark, NewBookmark, Config } from '../../interfaces'; import { CreateNotificationAction } from './notification'; /** @@ -54,6 +54,8 @@ export const addCategory = (formData: NewCategory) => async (dispatch: Dispatch) type: ActionTypes.addCategory, payload: res.data.data }) + + dispatch(sortCategories()); } catch (err) { console.log(err); } @@ -173,6 +175,8 @@ export const updateCategory = (id: number, formData: NewCategory) => async (disp type: ActionTypes.updateCategory, payload: res.data.data }) + + dispatch(sortCategories()); } catch (err) { console.log(err); } @@ -261,4 +265,60 @@ export const updateBookmark = (bookmarkId: number, formData: NewBookmark, previo } catch (err) { console.log(err); } +} + +/** + * SORT CATEGORIES + */ +export interface SortCategoriesAction { + type: ActionTypes.sortCategories; + payload: string; +} + +export const sortCategories = () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/config/useOrdering'); + + dispatch({ + type: ActionTypes.sortCategories, + payload: res.data.data.value + }) + } catch (err) { + console.log(err); + } +} + +/** + * REORDER CATEGORIES + */ +export interface ReorderCategoriesAction { + type: ActionTypes.reorderCategories; + payload: Category[]; +} + +interface ReorderQuery { + categories: { + id: number; + orderId: number; + }[] +} + +export const reorderCategories = (categories: Category[]) => async (dispatch: Dispatch) => { + try { + const updateQuery: ReorderQuery = { categories: [] } + + categories.forEach((category, index) => updateQuery.categories.push({ + id: category.id, + orderId: index + 1 + })) + + await axios.put>('/api/categories/0/reorder', updateQuery); + + dispatch({ + type: ActionTypes.reorderCategories, + payload: categories + }) + } catch (err) { + console.log(err); + } } \ No newline at end of file diff --git a/client/src/store/reducers/app.ts b/client/src/store/reducers/app.ts index b445542..0935819 100644 --- a/client/src/store/reducers/app.ts +++ b/client/src/store/reducers/app.ts @@ -1,5 +1,6 @@ import { ActionTypes, Action } from '../actions'; import { App } from '../../interfaces/App'; +import { sortData } from '../../utility'; export interface State { loading: boolean; @@ -52,11 +53,9 @@ const pinApp = (state: State, action: Action): State => { } const addAppSuccess = (state: State, action: Action): State => { - const tmpApps = [...state.apps, action.payload]; - return { ...state, - apps: tmpApps + apps: [...state.apps, action.payload] } } @@ -85,6 +84,22 @@ const updateApp = (state: State, action: Action): State => { } } +const reorderApps = (state: State, action: Action): State => { + return { + ...state, + apps: action.payload + } +} + +const sortApps = (state: State, action: Action): State => { + const sortedApps = sortData(state.apps, action.payload); + + return { + ...state, + apps: sortedApps + } +} + const appReducer = (state = initialState, action: Action) => { switch (action.type) { case ActionTypes.getApps: return getApps(state, action); @@ -94,6 +109,8 @@ const appReducer = (state = initialState, action: Action) => { case ActionTypes.addAppSuccess: return addAppSuccess(state, action); case ActionTypes.deleteApp: return deleteApp(state, action); case ActionTypes.updateApp: return updateApp(state, action); + case ActionTypes.reorderApps: return reorderApps(state, action); + case ActionTypes.sortApps: return sortApps(state, action); default: return state; } } diff --git a/client/src/store/reducers/bookmark.ts b/client/src/store/reducers/bookmark.ts index 2c1d5f0..a554d6e 100644 --- a/client/src/store/reducers/bookmark.ts +++ b/client/src/store/reducers/bookmark.ts @@ -1,5 +1,6 @@ import { ActionTypes, Action } from '../actions'; import { Category, Bookmark } from '../../interfaces'; +import { sortData } from '../../utility'; export interface State { loading: boolean; @@ -141,6 +142,22 @@ const updateBookmark = (state: State, action: Action): State => { } } +const sortCategories = (state: State, action: Action): State => { + const sortedCategories = sortData(state.categories, action.payload); + + return { + ...state, + categories: sortedCategories + } +} + +const reorderCategories = (state: State, action: Action): State => { + return { + ...state, + categories: action.payload + } +} + const bookmarkReducer = (state = initialState, action: Action) => { switch (action.type) { case ActionTypes.getCategories: return getCategories(state, action); @@ -152,6 +169,8 @@ const bookmarkReducer = (state = initialState, action: Action) => { case ActionTypes.updateCategory: return updateCategory(state, action); case ActionTypes.deleteBookmark: return deleteBookmark(state, action); case ActionTypes.updateBookmark: return updateBookmark(state, action); + case ActionTypes.sortCategories: return sortCategories(state, action); + case ActionTypes.reorderCategories: return reorderCategories(state, action); default: return state; } } diff --git a/client/src/utility/checkVersion.ts b/client/src/utility/checkVersion.ts new file mode 100644 index 0000000..e1a0508 --- /dev/null +++ b/client/src/utility/checkVersion.ts @@ -0,0 +1,27 @@ +import axios from 'axios'; +import { store } from '../store/store'; +import { createNotification } from '../store/actions'; + +export const checkVersion = async (isForced: boolean = false) => { + try { + const res = await axios.get('https://raw.githubusercontent.com/pawelmalak/flame/master/client/.env'); + + const githubVersion = res.data + .split('\n') + .map(pair => pair.split('='))[0][1]; + + if (githubVersion !== process.env.REACT_APP_VERSION) { + store.dispatch(createNotification({ + title: 'Info', + message: 'New version is available!' + })) + } else if (isForced) { + store.dispatch(createNotification({ + title: 'Info', + message: 'You are using the latest version!' + })) + } + } catch (err) { + console.log(err); + } +} \ No newline at end of file diff --git a/client/src/utility/index.ts b/client/src/utility/index.ts index 6caa71e..a5407b2 100644 --- a/client/src/utility/index.ts +++ b/client/src/utility/index.ts @@ -1,3 +1,5 @@ export * from './iconParser'; export * from './urlParser'; -export * from './searchConfig'; \ No newline at end of file +export * from './searchConfig'; +export * from './checkVersion'; +export * from './sortData'; \ No newline at end of file diff --git a/client/src/utility/searchConfig.ts b/client/src/utility/searchConfig.ts index fe4db57..4e46091 100644 --- a/client/src/utility/searchConfig.ts +++ b/client/src/utility/searchConfig.ts @@ -18,7 +18,7 @@ export const searchConfig = (key: string, _default: any) => { } else { return pair.value; } - } else { - return _default; } + + return _default; } \ No newline at end of file diff --git a/client/src/utility/sortData.ts b/client/src/utility/sortData.ts new file mode 100644 index 0000000..c1e9803 --- /dev/null +++ b/client/src/utility/sortData.ts @@ -0,0 +1,29 @@ +interface Data { + name: string; + orderId: number; + createdAt: Date; +} + +export const sortData = (array: T[], field: string): T[] => { + const sortedData = array.slice(); + + if (field === 'name') { + sortedData.sort((a: T, b: T) => { + return a.name.localeCompare(b.name, 'en', { sensitivity: 'base' }) + }) + } else if (field === 'orderId') { + sortedData.sort((a: T, b: T) => { + if (a.orderId < b.orderId) { return -1 } + if (a.orderId > b.orderId) { return 1 } + return 0; + }) + } else { + sortedData.sort((a: T, b: T) => { + if (a.createdAt < b.createdAt) { return -1 } + if (a.createdAt > b.createdAt) { return 1 } + return 0; + }) + } + + return sortedData; +} \ No newline at end of file diff --git a/controllers/apps.js b/controllers/apps.js index 4f21394..4f50f96 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -2,6 +2,7 @@ const asyncWrapper = require('../middleware/asyncWrapper'); const ErrorResponse = require('../utils/ErrorResponse'); const App = require('../models/App'); const Config = require('../models/Config'); +const { Sequelize } = require('sequelize'); // @desc Create new app // @route POST /api/apps @@ -35,10 +36,24 @@ exports.createApp = asyncWrapper(async (req, res, next) => { // @route GET /api/apps // @access Public exports.getApps = asyncWrapper(async (req, res, next) => { - const apps = await App.findAll({ - order: [['name', 'ASC']] + // Get config from database + const useOrdering = await Config.findOne({ + where: { key: 'useOrdering' } }); + const orderType = useOrdering ? useOrdering.value : 'createdAt'; + let apps; + + if (orderType == 'name') { + apps = await App.findAll({ + order: [[ Sequelize.fn('lower', Sequelize.col('name')), 'ASC' ]] + }); + } else { + apps = await App.findAll({ + order: [[ orderType, 'ASC' ]] + }); + } + res.status(200).json({ success: true, data: apps @@ -91,6 +106,22 @@ exports.deleteApp = asyncWrapper(async (req, res, next) => { where: { id: req.params.id } }) + res.status(200).json({ + success: true, + data: {} + }) +}) + +// @desc Reorder apps +// @route PUT /api/apps/0/reorder +// @access Public +exports.reorderApps = asyncWrapper(async (req, res, next) => { + req.body.apps.forEach(async ({ id, orderId }) => { + await App.update({ orderId }, { + where: { id } + }) + }) + res.status(200).json({ success: true, data: {} diff --git a/controllers/bookmark.js b/controllers/bookmark.js index 7baa664..08b2fca 100644 --- a/controllers/bookmark.js +++ b/controllers/bookmark.js @@ -1,6 +1,7 @@ const asyncWrapper = require('../middleware/asyncWrapper'); const ErrorResponse = require('../utils/ErrorResponse'); const Bookmark = require('../models/Bookmark'); +const { Sequelize } = require('sequelize'); // @desc Create new bookmark // @route POST /api/bookmarks @@ -19,7 +20,7 @@ exports.createBookmark = asyncWrapper(async (req, res, next) => { // @access Public exports.getBookmarks = asyncWrapper(async (req, res, next) => { const bookmarks = await Bookmark.findAll({ - order: [['name', 'ASC']] + order: [[ Sequelize.fn('lower', Sequelize.col('name')), 'ASC' ]] }); res.status(200).json({ diff --git a/controllers/category.js b/controllers/category.js index a390405..15fe1eb 100644 --- a/controllers/category.js +++ b/controllers/category.js @@ -3,6 +3,7 @@ const ErrorResponse = require('../utils/ErrorResponse'); const Category = require('../models/Category'); const Bookmark = require('../models/Bookmark'); const Config = require('../models/Config'); +const { Sequelize } = require('sequelize') // @desc Create new category // @route POST /api/categories @@ -36,14 +37,32 @@ exports.createCategory = asyncWrapper(async (req, res, next) => { // @route GET /api/categories // @access Public exports.getCategories = asyncWrapper(async (req, res, next) => { - const categories = await Category.findAll({ - include: [{ - model: Bookmark, - as: 'bookmarks' - }], - order: [['name', 'ASC']] + // Get config from database + const useOrdering = await Config.findOne({ + where: { key: 'useOrdering' } }); + const orderType = useOrdering ? useOrdering.value : 'createdAt'; + let categories; + + if (orderType == 'name') { + categories = await Category.findAll({ + include: [{ + model: Bookmark, + as: 'bookmarks' + }], + order: [[ Sequelize.fn('lower', Sequelize.col('Category.name')), 'ASC' ]] + }); + } else { + categories = await Category.findAll({ + include: [{ + model: Bookmark, + as: 'bookmarks' + }], + order: [[ orderType, 'ASC' ]] + }); + } + res.status(200).json({ success: true, data: categories @@ -118,6 +137,22 @@ exports.deleteCategory = asyncWrapper(async (req, res, next) => { where: { id: req.params.id } }) + res.status(200).json({ + success: true, + data: {} + }) +}) + +// @desc Reorder categories +// @route PUT /api/categories/0/reorder +// @access Public +exports.reorderCategories = asyncWrapper(async (req, res, next) => { + req.body.categories.forEach(async ({ id, orderId }) => { + await Category.update({ orderId }, { + where: { id } + }) + }) + res.status(200).json({ success: true, data: {} diff --git a/models/App.js b/models/App.js index 8b7a5a3..f521955 100644 --- a/models/App.js +++ b/models/App.js @@ -18,6 +18,11 @@ const App = sequelize.define('App', { isPinned: { type: DataTypes.BOOLEAN, defaultValue: false + }, + orderId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null } }, { tableName: 'apps' diff --git a/models/Category.js b/models/Category.js index 5f82633..9c9eda6 100644 --- a/models/Category.js +++ b/models/Category.js @@ -9,6 +9,11 @@ const Category = sequelize.define('Category', { isPinned: { type: DataTypes.BOOLEAN, defaultValue: false + }, + orderId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null } }, { tableName: 'categories' diff --git a/routes/apps.js b/routes/apps.js index d05f988..a0b3f47 100644 --- a/routes/apps.js +++ b/routes/apps.js @@ -6,7 +6,8 @@ const { getApps, getApp, updateApp, - deleteApp + deleteApp, + reorderApps } = require('../controllers/apps'); router @@ -20,4 +21,8 @@ router .put(updateApp) .delete(deleteApp); +router + .route('/0/reorder') + .put(reorderApps); + module.exports = router; \ No newline at end of file diff --git a/routes/category.js b/routes/category.js index b18b8f6..64067d7 100644 --- a/routes/category.js +++ b/routes/category.js @@ -6,7 +6,8 @@ const { getCategories, getCategory, updateCategory, - deleteCategory + deleteCategory, + reorderCategories } = require('../controllers/category'); router @@ -20,4 +21,8 @@ router .put(updateCategory) .delete(deleteCategory); +router + .route('/0/reorder') + .put(reorderCategories); + module.exports = router; \ No newline at end of file diff --git a/utils/initialConfig.json b/utils/initialConfig.json index eface5d..09bf4b8 100644 --- a/utils/initialConfig.json +++ b/utils/initialConfig.json @@ -31,6 +31,10 @@ { "key": "hideHeader", "value": false + }, + { + "key": "useOrdering", + "value": "createdAt" } ] } \ No newline at end of file