diff --git a/.env b/.env index feb0686..1bb2edb 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ PORT=5005 NODE_ENV=development -VERSION=1.6.9 \ No newline at end of file +VERSION=1.7.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d2320b..54c68e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### v1.7.0 (2021-10-11) +- Search bar will now redirect if valid URL or IP is provided ([#67](https://github.com/pawelmalak/flame/issues/67)) +- Users can now add their custom search providers ([#71](https://github.com/pawelmalak/flame/issues/71)) +- Fixed bug related to creating new apps/bookmarks with custom icon ([#83](https://github.com/pawelmalak/flame/issues/83)) +- URL can now be assigned to notifications. Clicking on "New version is available" popup will now redirect to changelog ([#86](https://github.com/pawelmalak/flame/issues/86)) +- Added static fonts ([#94](https://github.com/pawelmalak/flame/issues/94)) +- Fixed bug with overriding app icon created with docker labels + ### v1.6.9 (2021-10-09) - Added option for remote docker host ([#97](https://github.com/pawelmalak/flame/issues/97)) diff --git a/api.js b/api.js index 1c2d863..9eb9b9f 100644 --- a/api.js +++ b/api.js @@ -20,6 +20,7 @@ api.use('/api/config', require('./routes/config')); api.use('/api/weather', require('./routes/weather')); api.use('/api/categories', require('./routes/category')); api.use('/api/bookmarks', require('./routes/bookmark')); +api.use('/api/queries', require('./routes/queries')); // Custom error handler api.use(errorHandler); diff --git a/client/.env b/client/.env index e9f8924..6dbe18b 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.6.9 \ No newline at end of file +REACT_APP_VERSION=1.7.0 \ No newline at end of file diff --git a/client/public/index.html b/client/public/index.html index 3f43c40..c93d95e 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -4,16 +4,15 @@ - - - - + + Flame -
- - \ No newline at end of file + diff --git a/client/src/App.tsx b/client/src/App.tsx index 05db805..9311b4b 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,5 @@ import { BrowserRouter, Route, Switch } from 'react-router-dom'; -import { getConfig, setTheme } from './store/actions'; +import { fetchQueries, getConfig, setTheme } from './store/actions'; import 'external-svg-loader'; // Redux @@ -27,15 +27,18 @@ if (localStorage.theme) { // Check for updates checkVersion(); +// fetch queries +store.dispatch(fetchQueries()); + const App = (): JSX.Element => { return ( - - - - + + + + diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff b/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff new file mode 100644 index 0000000..c9eb5ca Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff2 b/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff2 new file mode 100644 index 0000000..ce795fa Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff2 differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff b/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff new file mode 100644 index 0000000..a5d98fc Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2 b/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2 new file mode 100644 index 0000000..01d05fa Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2 differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff b/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff new file mode 100644 index 0000000..c3933ba Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff2 b/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff2 new file mode 100644 index 0000000..06437ae Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff2 differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff b/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff new file mode 100644 index 0000000..86b3863 Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2 b/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2 new file mode 100644 index 0000000..ebe1795 Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2 differ diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index 5d05f0a..d44418e 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -22,7 +22,7 @@ const AppForm = (props: ComponentProps): JSX.Element => { const [formData, setFormData] = useState({ name: '', url: '', - icon: '' + icon: '', }); useEffect(() => { @@ -30,13 +30,13 @@ const AppForm = (props: ComponentProps): JSX.Element => { setFormData({ name: props.app.name, url: props.app.url, - icon: props.app.icon + icon: props.app.icon, }); } else { setFormData({ name: '', url: '', - icon: '' + icon: '', }); } }, [props.app]); @@ -44,7 +44,7 @@ const AppForm = (props: ComponentProps): JSX.Element => { const inputChangeHandler = (e: ChangeEvent): void => { setFormData({ ...formData, - [e.target.name]: e.target.value + [e.target.name]: e.target.value, }); }; @@ -59,6 +59,7 @@ const AppForm = (props: ComponentProps): JSX.Element => { const createFormData = (): FormData => { const data = new FormData(); + if (customIcon) { data.append('icon', customIcon); } @@ -88,10 +89,8 @@ const AppForm = (props: ComponentProps): JSX.Element => { setFormData({ name: '', url: '', - icon: '' + icon: '', }); - - setCustomIcon(null); }; return ( @@ -100,33 +99,33 @@ const AppForm = (props: ComponentProps): JSX.Element => { formHandler={formSubmitHandler} > - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> {' '} Check supported URL formats @@ -136,19 +135,19 @@ const AppForm = (props: ComponentProps): JSX.Element => { {!useCustomIcon ? ( // use mdi icon - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> Use icon name from MDI. - + {' '} Click here for reference @@ -163,17 +162,20 @@ const AppForm = (props: ComponentProps): JSX.Element => { ) : ( // upload custom icon - + fileChangeHandler(e)} - accept='.jpg,.jpeg,.png,.svg' + onChange={(e) => fileChangeHandler(e)} + accept=".jpg,.jpeg,.png,.svg" /> toggleUseCustomIcon(!useCustomIcon)} + onClick={() => { + setCustomIcon(null); + toggleUseCustomIcon(!useCustomIcon); + }} className={classes.Switch} > Switch to MDI diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx index 10d6de2..5162c89 100644 --- a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx +++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx @@ -1,32 +1,40 @@ +// React import { useState, SyntheticEvent, Fragment, ChangeEvent, - useEffect + useEffect, } from 'react'; -import { connect } from 'react-redux'; -import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; -import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; -import { - Bookmark, - Category, - GlobalState, - NewBookmark, - NewCategory, - NewNotification -} from '../../../interfaces'; -import { ContentType } from '../Bookmarks'; +// Redux +import { connect } from 'react-redux'; import { getCategories, addCategory, addBookmark, updateCategory, updateBookmark, - createNotification + createNotification, } from '../../../store/actions'; + +// Typescript +import { + Bookmark, + Category, + GlobalState, + NewBookmark, + NewCategory, + NewNotification, +} from '../../../interfaces'; +import { ContentType } from '../Bookmarks'; + +// UI +import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; +import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; + +// CSS import classes from './BookmarkForm.module.css'; interface ComponentProps { @@ -53,14 +61,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { const [useCustomIcon, toggleUseCustomIcon] = useState(false); const [customIcon, setCustomIcon] = useState(null); const [categoryName, setCategoryName] = useState({ - name: '' + name: '', }); const [formData, setFormData] = useState({ name: '', url: '', categoryId: -1, - icon: '' + icon: '', }); // Load category data if provided for editing @@ -79,14 +87,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { name: props.bookmark.name, url: props.bookmark.url, categoryId: props.bookmark.categoryId, - icon: props.bookmark.icon + icon: props.bookmark.icon, }); } else { setFormData({ name: '', url: '', categoryId: -1, - icon: '' + icon: '', }); } }, [props.bookmark]); @@ -117,7 +125,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { if (formData.categoryId === -1) { props.createNotification({ title: 'Error', - message: 'Please select category' + message: 'Please select category', }); return; } @@ -133,10 +141,10 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { name: '', url: '', categoryId: formData.categoryId, - icon: '' + icon: '', }); - setCustomIcon(null); + // setCustomIcon(null); } } else { // Update @@ -150,12 +158,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { const data = createFormData(); props.updateBookmark(props.bookmark.id, data, { prev: props.bookmark.categoryId, - curr: formData.categoryId + curr: formData.categoryId, }); } else { props.updateBookmark(props.bookmark.id, formData, { prev: props.bookmark.categoryId, - curr: formData.categoryId + curr: formData.categoryId, }); } @@ -163,7 +171,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { name: '', url: '', categoryId: -1, - icon: '' + icon: '', }); setCustomIcon(null); @@ -176,14 +184,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { const inputChangeHandler = (e: ChangeEvent): void => { setFormData({ ...formData, - [e.target.name]: e.target.value + [e.target.name]: e.target.value, }); }; const selectChangeHandler = (e: ChangeEvent): void => { setFormData({ ...formData, - categoryId: parseInt(e.target.value) + categoryId: parseInt(e.target.value), }); }; @@ -215,48 +223,48 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { {props.contentType === ContentType.category ? ( - + setCategoryName({ name: e.target.value })} + onChange={(e) => setCategoryName({ name: e.target.value })} /> ) : ( - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> {' '} Check supported URL formats @@ -264,12 +272,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> Use icon name from MDI. - + {' '} Click here for reference @@ -311,16 +319,19 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { ) : ( // custom - + fileChangeHandler(e)} - accept='.jpg,.jpeg,.png,.svg' + type="file" + name="icon" + id="icon" + onChange={(e) => fileChangeHandler(e)} + accept=".jpg,.jpeg,.png,.svg" /> toggleUseCustomIcon(!useCustomIcon)} + onClick={() => { + setCustomIcon(null); + toggleUseCustomIcon(!useCustomIcon); + }} className={classes.Switch} > Switch to MDI @@ -336,7 +347,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { const mapStateToProps = (state: GlobalState) => { return { - categories: state.bookmark.categories + categories: state.bookmark.categories, }; }; @@ -346,7 +357,7 @@ const dispatchMap = { addBookmark, updateCategory, updateBookmark, - createNotification + createNotification, }; export default connect(mapStateToProps, dispatchMap)(BookmarkForm); diff --git a/client/src/components/NotificationCenter/NotificationCenter.tsx b/client/src/components/NotificationCenter/NotificationCenter.tsx index 29c9cb2..733316b 100644 --- a/client/src/components/NotificationCenter/NotificationCenter.tsx +++ b/client/src/components/NotificationCenter/NotificationCenter.tsx @@ -20,19 +20,20 @@ const NotificationCenter = (props: ComponentProps): JSX.Element => { - ) + ); })} - ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { - notifications: state.notification.notifications - } -} + notifications: state.notification.notifications, + }; +}; -export default connect(mapStateToProps)(NotificationCenter); \ No newline at end of file +export default connect(mapStateToProps)(NotificationCenter); diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index f2ccdec..887a2ef 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -11,7 +11,7 @@ import { NewNotification } from '../../interfaces'; import classes from './SearchBar.module.css'; // Utils -import { searchParser } from '../../utility'; +import { searchParser, urlParser, redirectUrl } from '../../utility'; interface ComponentProps { createNotification: (notification: NewNotification) => void; @@ -28,28 +28,28 @@ const SearchBar = (props: ComponentProps): JSX.Element => { }, []); const searchHandler = (e: KeyboardEvent) => { - const searchResult = searchParser(inputRef.current.value); + const { isLocal, search, query, isURL, sameTab } = searchParser( + inputRef.current.value + ); - if (searchResult.isLocal) { - setLocalSearch(searchResult.search); + if (isLocal) { + setLocalSearch(search); } if (e.code === 'Enter') { - if (!searchResult.query.prefix) { + if (!query.prefix) { createNotification({ title: 'Error', message: 'Prefix not found', }); - } else if (searchResult.isLocal) { - setLocalSearch(searchResult.search); + } else if (isURL) { + const url = urlParser(inputRef.current.value)[1]; + redirectUrl(url, sameTab); + } else if (isLocal) { + setLocalSearch(search); } else { - if (searchResult.sameTab) { - document.location.replace( - `${searchResult.query.template}${searchResult.search}` - ); - } else { - window.open(`${searchResult.query.template}${searchResult.search}`); - } + const url = `${query.template}${search}`; + redirectUrl(url, sameTab); } } }; diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index bd9bf49..c3525f8 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -13,20 +13,16 @@ import { import { GlobalState, NewNotification, - Query, SettingsForm, } from '../../../interfaces'; // UI import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; - -// CSS -import classes from './OtherSettings.module.css'; +import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline'; // Utils import { searchConfig } from '../../../utility'; -import { queries } from '../../../utility/searchQueries.json'; interface ComponentProps { createNotification: (notification: NewNotification) => void; @@ -45,12 +41,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { hideHeader: 0, hideApps: 0, hideCategories: 0, - hideSearch: 0, - defaultSearchProvider: 'd', useOrdering: 'createdAt', appsSameTab: 0, bookmarksSameTab: 0, - searchSameTab: 0, dockerApps: 1, dockerHost: 'localhost', kubernetesApps: 1, @@ -66,12 +59,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { hideHeader: searchConfig('hideHeader', 0), hideApps: searchConfig('hideApps', 0), hideCategories: searchConfig('hideCategories', 0), - hideSearch: searchConfig('hideSearch', 0), - defaultSearchProvider: searchConfig('defaultSearchProvider', 'd'), useOrdering: searchConfig('useOrdering', 'createdAt'), appsSameTab: searchConfig('appsSameTab', 0), bookmarksSameTab: searchConfig('bookmarksSameTab', 0), - searchSameTab: searchConfig('searchSameTab', 0), dockerApps: searchConfig('dockerApps', 0), dockerHost: searchConfig('dockerHost', 'localhost'), kubernetesApps: searchConfig('kubernetesApps', 0), @@ -114,7 +104,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { return (
formSubmitHandler(e)}> {/* OTHER OPTIONS */} -

Miscellaneous

+ { {/* BEAHVIOR OPTIONS */} -

App Behavior

+ - - - - - - - - inputChangeHandler(e, true)} - > - - - - + { {/* KUBERNETES SETTINGS */} -

Kubernetes

+ inputChangeHandler(e)} + /> + + + + inputChangeHandler(e)} + /> + + + + inputChangeHandler(e)} + /> + + {query ? : } + + ); +}; + +export default connect(null, { addQuery, updateQuery })(QueriesForm); diff --git a/client/src/components/Settings/SearchSettings/SearchSettings.tsx b/client/src/components/Settings/SearchSettings/SearchSettings.tsx new file mode 100644 index 0000000..b2ac422 --- /dev/null +++ b/client/src/components/Settings/SearchSettings/SearchSettings.tsx @@ -0,0 +1,154 @@ +// React +import { useState, useEffect, FormEvent, ChangeEvent, Fragment } from 'react'; +import { connect } from 'react-redux'; + +// State +import { createNotification, updateConfig } from '../../../store/actions'; + +// Typescript +import { + GlobalState, + NewNotification, + Query, + SearchForm, +} from '../../../interfaces'; + +// Components +import CustomQueries from './CustomQueries/CustomQueries'; + +// UI +import Button from '../../UI/Buttons/Button/Button'; +import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline'; +import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; + +// Utils +import { searchConfig } from '../../../utility'; + +// Data +import { queries } from '../../../utility/searchQueries.json'; + +interface Props { + createNotification: (notification: NewNotification) => void; + updateConfig: (formData: SearchForm) => void; + loading: boolean; + customQueries: Query[]; +} + +const SearchSettings = (props: Props): JSX.Element => { + // Initial state + const [formData, setFormData] = useState({ + hideSearch: 0, + defaultSearchProvider: 'l', + searchSameTab: 0, + }); + + // Get config + useEffect(() => { + setFormData({ + hideSearch: searchConfig('hideSearch', 0), + defaultSearchProvider: searchConfig('defaultSearchProvider', 'l'), + searchSameTab: searchConfig('searchSameTab', 0), + }); + }, [props.loading]); + + // Form handler + const formSubmitHandler = async (e: FormEvent) => { + e.preventDefault(); + + // Save settings + await props.updateConfig(formData); + }; + + // Input handler + const inputChangeHandler = ( + e: ChangeEvent, + isNumber?: boolean + ) => { + let value: string | number = e.target.value; + + if (isNumber) { + value = parseFloat(value); + } + + setFormData({ + ...formData, + [e.target.name]: value, + }); + }; + + return ( + + {/* GENERAL SETTINGS */} + formSubmitHandler(e)} + style={{ marginBottom: '30px' }} + > + + + + + + + + + + + + + + + + + {/* CUSTOM QUERIES */} + + + + ); +}; + +const mapStateToProps = (state: GlobalState) => { + return { + loading: state.config.loading, + customQueries: state.config.customQueries, + }; +}; + +const actions = { + createNotification, + updateConfig, +}; + +export default connect(mapStateToProps, actions)(SearchSettings); diff --git a/client/src/components/Settings/Settings.tsx b/client/src/components/Settings/Settings.tsx index b1eb300..5df8ec6 100644 --- a/client/src/components/Settings/Settings.tsx +++ b/client/src/components/Settings/Settings.tsx @@ -1,73 +1,61 @@ +// import { NavLink, Link, Switch, Route } from 'react-router-dom'; +// Typescript +import { Route as SettingsRoute } from '../../interfaces'; + +// CSS import classes from './Settings.module.css'; -import { Container } from '../UI/Layout/Layout'; -import Headline from '../UI/Headlines/Headline/Headline'; - +// Components import Themer from '../Themer/Themer'; import WeatherSettings from './WeatherSettings/WeatherSettings'; import OtherSettings from './OtherSettings/OtherSettings'; import AppDetails from './AppDetails/AppDetails'; import StyleSettings from './StyleSettings/StyleSettings'; +import SearchSettings from './SearchSettings/SearchSettings'; + +// UI +import { Container } from '../UI/Layout/Layout'; +import Headline from '../UI/Headlines/Headline/Headline'; + +// Data +import { routes } from './settings.json'; const Settings = (): JSX.Element => { return ( - Go back} - /> + Go back} />
+ {/* NAVIGATION MENU */} + + {/* ROUTES */}
- - - - - + + + + + +
- ) -} + ); +}; -export default Settings; \ No newline at end of file +export default Settings; diff --git a/client/src/components/Settings/settings.json b/client/src/components/Settings/settings.json new file mode 100644 index 0000000..3cc24e9 --- /dev/null +++ b/client/src/components/Settings/settings.json @@ -0,0 +1,28 @@ +{ + "routes": [ + { + "name": "Theme", + "dest": "/settings" + }, + { + "name": "Weather", + "dest": "/settings/weather" + }, + { + "name": "Search", + "dest": "/settings/search" + }, + { + "name": "Other", + "dest": "/settings/other" + }, + { + "name": "CSS", + "dest": "/settings/css" + }, + { + "name": "App", + "dest": "/settings/app" + } + ] +} diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.module.css b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.module.css similarity index 89% rename from client/src/components/Settings/OtherSettings/OtherSettings.module.css rename to client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.module.css index 36e4deb..137667c 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.module.css +++ b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.module.css @@ -1,4 +1,4 @@ -.SettingsSection { +.SettingsHeadline { color: var(--color-primary); padding-bottom: 3px; margin-bottom: 10px; @@ -6,4 +6,4 @@ font-weight: 500; border-bottom: 2px solid var(--color-accent); display: inline-block; -} \ No newline at end of file +} diff --git a/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx new file mode 100644 index 0000000..5d14949 --- /dev/null +++ b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx @@ -0,0 +1,11 @@ +const classes = require('./SettingsHeadline.module.css'); + +interface Props { + text: string; +} + +const SettingsHeadline = (props: Props): JSX.Element => { + return

{props.text}

; +}; + +export default SettingsHeadline; diff --git a/client/src/components/UI/Notification/Notification.tsx b/client/src/components/UI/Notification/Notification.tsx index 95109e1..2bd5185 100644 --- a/client/src/components/UI/Notification/Notification.tsx +++ b/client/src/components/UI/Notification/Notification.tsx @@ -8,12 +8,16 @@ interface ComponentProps { title: string; message: string; id: number; + url: string | null; clearNotification: (id: number) => void; } const Notification = (props: ComponentProps): JSX.Element => { const [isOpen, setIsOpen] = useState(true); - const elementClasses = [classes.Notification, isOpen ? classes.NotificationOpen : classes.NotificationClose].join(' '); + const elementClasses = [ + classes.Notification, + isOpen ? classes.NotificationOpen : classes.NotificationClose, + ].join(' '); useEffect(() => { const closeNotification = setTimeout(() => { @@ -22,21 +26,27 @@ const Notification = (props: ComponentProps): JSX.Element => { const clearNotification = setTimeout(() => { props.clearNotification(props.id); - }, 3600) + }, 3600); return () => { window.clearTimeout(closeNotification); window.clearTimeout(clearNotification); + }; + }, []); + + const clickHandler = () => { + if (props.url) { + window.open(props.url, '_blank'); } - }, []) + }; return ( -
+

{props.title}

{props.message}

- ) -} + ); +}; -export default connect(null, { clearNotification })(Notification); \ No newline at end of file +export default connect(null, { clearNotification })(Notification); diff --git a/client/src/index.css b/client/src/index.css index db1d21b..8661f97 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,3 +1,39 @@ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local(''), + url('./assets/fonts/Roboto/roboto-v29-latin-regular.woff2') format('woff2'), + url('./assets/fonts/Roboto/roboto-v29-latin-regular.woff') format('woff'); +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local(''), + url('./assets/fonts/Roboto/roboto-v29-latin-500.woff2') format('woff2'), + url('./assets/fonts/Roboto/roboto-v29-latin-500.woff') format('woff'); +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 900; + src: local(''), + url('./assets/fonts/Roboto/roboto-v29-latin-900.woff2') format('woff2'), + url('./assets/fonts/Roboto/roboto-v29-latin-900.woff') format('woff'); +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + src: local(''), + url('./assets/fonts/Roboto/roboto-v29-latin-700.woff2') format('woff2'), + url('./assets/fonts/Roboto/roboto-v29-latin-700.woff') format('woff'); +} + * { margin: 0; padding: 0; @@ -5,18 +41,18 @@ } body { - --color-background: #2B2C56; - --color-primary: #EFF1FC; - --color-accent: #6677EB; + --color-background: #242b33; + --color-primary: #effbff; + --color-accent: #6ee2ff; --spacing-ui: 10px; background-color: var(--color-background); transition: background-color 0.3s; - font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, Roboto, sans-serif; + font-family: Roboto, sans-serif; font-size: 14px; } a { color: var(--color-primary); text-decoration: none; -} \ No newline at end of file +} diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index bd53e2d..9b195da 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -5,6 +5,12 @@ export interface WeatherForm { isCelsius: number; } +export interface SearchForm { + hideSearch: number; + defaultSearchProvider: string; + searchSameTab: number; +} + export interface SettingsForm { customTitle: string; pinAppsByDefault: number; @@ -12,12 +18,12 @@ export interface SettingsForm { hideHeader: number; hideApps: number; hideCategories: number; - hideSearch: number; - defaultSearchProvider: string; + // hideSearch: number; + // defaultSearchProvider: string; useOrdering: string; appsSameTab: number; bookmarksSameTab: number; - searchSameTab: number; + // searchSameTab: number; dockerApps: number; dockerHost: string; kubernetesApps: number; diff --git a/client/src/interfaces/Notification.ts b/client/src/interfaces/Notification.ts index 80a49f2..5054922 100644 --- a/client/src/interfaces/Notification.ts +++ b/client/src/interfaces/Notification.ts @@ -1,8 +1,9 @@ export interface NewNotification { title: string; message: string; + url?: string; } export interface Notification extends NewNotification { id: number; -} \ No newline at end of file +} diff --git a/client/src/interfaces/Route.ts b/client/src/interfaces/Route.ts new file mode 100644 index 0000000..9d571dd --- /dev/null +++ b/client/src/interfaces/Route.ts @@ -0,0 +1,4 @@ +export interface Route { + name: string; + dest: string; +} diff --git a/client/src/interfaces/SearchResult.ts b/client/src/interfaces/SearchResult.ts index 271bdc2..3d6c8ae 100644 --- a/client/src/interfaces/SearchResult.ts +++ b/client/src/interfaces/SearchResult.ts @@ -2,6 +2,7 @@ import { Query } from './Query'; export interface SearchResult { isLocal: boolean; + isURL: boolean; sameTab: boolean; search: string; query: Query; diff --git a/client/src/interfaces/index.ts b/client/src/interfaces/index.ts index 6892fb5..b9683dd 100644 --- a/client/src/interfaces/index.ts +++ b/client/src/interfaces/index.ts @@ -10,3 +10,4 @@ export * from './Config'; export * from './Forms'; export * from './Query'; export * from './SearchResult'; +export * from './Route'; diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts index 4324834..c670b2f 100644 --- a/client/src/store/actions/actionTypes.ts +++ b/client/src/store/actions/actionTypes.ts @@ -26,8 +26,14 @@ import { ClearNotificationAction, // Config GetConfigAction, - UpdateConfigAction + UpdateConfigAction, } from './'; +import { + AddQueryAction, + DeleteQueryAction, + FetchQueriesAction, + UpdateQueryAction, +} from './config'; export enum ActionTypes { // Theme @@ -62,35 +68,43 @@ export enum ActionTypes { clearNotification = 'CLEAR_NOTIFICATION', // Config getConfig = 'GET_CONFIG', - updateConfig = 'UPDATE_CONFIG' + updateConfig = 'UPDATE_CONFIG', + fetchQueries = 'FETCH_QUERIES', + addQuery = 'ADD_QUERY', + deleteQuery = 'DELETE_QUERY', + updateQuery = 'UPDATE_QUERY', } -export type Action = +export type Action = // Theme - SetThemeAction | + | SetThemeAction // Apps - GetAppsAction | - PinAppAction | - AddAppAction | - DeleteAppAction | - UpdateAppAction | - ReorderAppsAction | - SortAppsAction | + | GetAppsAction + | PinAppAction + | AddAppAction + | DeleteAppAction + | UpdateAppAction + | ReorderAppsAction + | SortAppsAction // Categories - GetCategoriesAction | - AddCategoryAction | - PinCategoryAction | - DeleteCategoryAction | - UpdateCategoryAction | - SortCategoriesAction | - ReorderCategoriesAction | + | GetCategoriesAction + | AddCategoryAction + | PinCategoryAction + | DeleteCategoryAction + | UpdateCategoryAction + | SortCategoriesAction + | ReorderCategoriesAction // Bookmarks - AddBookmarkAction | - DeleteBookmarkAction | - UpdateBookmarkAction | + | AddBookmarkAction + | DeleteBookmarkAction + | UpdateBookmarkAction // Notifications - CreateNotificationAction | - ClearNotificationAction | + | CreateNotificationAction + | ClearNotificationAction // Config - GetConfigAction | - UpdateConfigAction; \ No newline at end of file + | GetConfigAction + | UpdateConfigAction + | FetchQueriesAction + | AddQueryAction + | DeleteQueryAction + | UpdateQueryAction; diff --git a/client/src/store/actions/config.ts b/client/src/store/actions/config.ts index a14e21e..29c5186 100644 --- a/client/src/store/actions/config.ts +++ b/client/src/store/actions/config.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { Dispatch } from 'redux'; import { ActionTypes } from './actionTypes'; -import { Config, ApiResponse } from '../../interfaces'; +import { Config, ApiResponse, Query } from '../../interfaces'; import { CreateNotificationAction } from './notification'; import { searchConfig } from '../../utility'; @@ -13,18 +13,18 @@ export interface GetConfigAction { export const getConfig = () => async (dispatch: Dispatch) => { try { const res = await axios.get>('/api/config'); - + dispatch({ type: ActionTypes.getConfig, - payload: res.data.data - }) + payload: res.data.data, + }); // Set custom page title if set document.title = searchConfig('customTitle', 'Flame'); } catch (err) { - console.log(err) + console.log(err); } -} +}; export interface UpdateConfigAction { type: ActionTypes.updateConfig; @@ -34,19 +34,102 @@ export interface UpdateConfigAction { export const updateConfig = (formData: any) => async (dispatch: Dispatch) => { try { const res = await axios.put>('/api/config', formData); + dispatch({ type: ActionTypes.createNotification, payload: { title: 'Success', - message: 'Settings updated' - } - }) + message: 'Settings updated', + }, + }); dispatch({ type: ActionTypes.updateConfig, - payload: res.data.data - }) + payload: res.data.data, + }); } catch (err) { console.log(err); } -} \ No newline at end of file +}; + +export interface FetchQueriesAction { + type: ActionTypes.fetchQueries; + payload: Query[]; +} + +export const fetchQueries = + () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/queries'); + + dispatch({ + type: ActionTypes.fetchQueries, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export interface AddQueryAction { + type: ActionTypes.addQuery; + payload: Query; +} + +export const addQuery = + (query: Query) => async (dispatch: Dispatch) => { + try { + const res = await axios.post>('/api/queries', query); + + dispatch({ + type: ActionTypes.addQuery, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export interface DeleteQueryAction { + type: ActionTypes.deleteQuery; + payload: Query[]; +} + +export const deleteQuery = + (prefix: string) => async (dispatch: Dispatch) => { + try { + const res = await axios.delete>( + `/api/queries/${prefix}` + ); + + dispatch({ + type: ActionTypes.deleteQuery, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; + +export interface UpdateQueryAction { + type: ActionTypes.updateQuery; + payload: Query[]; +} + +export const updateQuery = + (query: Query, oldPrefix: string) => + async (dispatch: Dispatch) => { + try { + const res = await axios.put>( + `/api/queries/${oldPrefix}`, + query + ); + + dispatch({ + type: ActionTypes.updateQuery, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; diff --git a/client/src/store/reducers/config.ts b/client/src/store/reducers/config.ts index 071f461..ae2699e 100644 --- a/client/src/store/reducers/config.ts +++ b/client/src/store/reducers/config.ts @@ -1,36 +1,78 @@ import { ActionTypes, Action } from '../actions'; -import { Config } from '../../interfaces'; +import { Config, Query } from '../../interfaces'; export interface State { loading: boolean; config: Config[]; + customQueries: Query[]; } const initialState: State = { loading: true, - config: [] -} + config: [], + customQueries: [], +}; const getConfig = (state: State, action: Action): State => { return { + ...state, loading: false, - config: action.payload - } -} + config: action.payload, + }; +}; const updateConfig = (state: State, action: Action): State => { return { ...state, - config: action.payload - } -} + config: action.payload, + }; +}; + +const fetchQueries = (state: State, action: Action): State => { + return { + ...state, + customQueries: action.payload, + }; +}; + +const addQuery = (state: State, action: Action): State => { + return { + ...state, + customQueries: [...state.customQueries, action.payload], + }; +}; + +const deleteQuery = (state: State, action: Action): State => { + return { + ...state, + customQueries: action.payload, + }; +}; + +const updateQuery = (state: State, action: Action): State => { + return { + ...state, + customQueries: action.payload, + }; +}; const configReducer = (state: State = initialState, action: Action) => { - switch(action.type) { - case ActionTypes.getConfig: return getConfig(state, action); - case ActionTypes.updateConfig: return updateConfig(state, action); - default: return state; + switch (action.type) { + case ActionTypes.getConfig: + return getConfig(state, action); + case ActionTypes.updateConfig: + return updateConfig(state, action); + case ActionTypes.fetchQueries: + return fetchQueries(state, action); + case ActionTypes.addQuery: + return addQuery(state, action); + case ActionTypes.deleteQuery: + return deleteQuery(state, action); + case ActionTypes.updateQuery: + return updateQuery(state, action); + default: + return state; } -} +}; -export default configReducer; \ No newline at end of file +export default configReducer; diff --git a/client/src/utility/checkVersion.ts b/client/src/utility/checkVersion.ts index e1a0508..d4cdb9a 100644 --- a/client/src/utility/checkVersion.ts +++ b/client/src/utility/checkVersion.ts @@ -4,24 +4,31 @@ 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 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]; + .map((pair) => pair.split('='))[0][1]; if (githubVersion !== process.env.REACT_APP_VERSION) { - store.dispatch(createNotification({ - title: 'Info', - message: 'New version is available!' - })) + store.dispatch( + createNotification({ + title: 'Info', + message: 'New version is available!', + url: 'https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md', + }) + ); } else if (isForced) { - store.dispatch(createNotification({ - title: 'Info', - message: 'You are using the latest version!' - })) + 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 99f8d69..caff9c3 100644 --- a/client/src/utility/index.ts +++ b/client/src/utility/index.ts @@ -3,4 +3,5 @@ export * from './urlParser'; export * from './searchConfig'; export * from './checkVersion'; export * from './sortData'; -export * from './searchParser'; \ No newline at end of file +export * from './searchParser'; +export * from './redirectUrl'; diff --git a/client/src/utility/redirectUrl.ts b/client/src/utility/redirectUrl.ts new file mode 100644 index 0000000..81eca10 --- /dev/null +++ b/client/src/utility/redirectUrl.ts @@ -0,0 +1,7 @@ +export const redirectUrl = (url: string, sameTab: boolean) => { + if (sameTab) { + document.location.replace(url); + } else { + window.open(url); + } +}; diff --git a/client/src/utility/searchParser.ts b/client/src/utility/searchParser.ts index c477d66..2befdd2 100644 --- a/client/src/utility/searchParser.ts +++ b/client/src/utility/searchParser.ts @@ -1,11 +1,12 @@ import { queries } from './searchQueries.json'; import { Query, SearchResult } from '../interfaces'; - +import { store } from '../store/store'; import { searchConfig } from '.'; export const searchParser = (searchQuery: string): SearchResult => { const result: SearchResult = { isLocal: false, + isURL: false, sameTab: false, search: '', query: { @@ -15,6 +16,15 @@ export const searchParser = (searchQuery: string): SearchResult => { }, }; + const customQueries = store.getState().config.customQueries; + + // Check if url or ip was passed + const urlRegex = + /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/; + + result.isURL = urlRegex.test(searchQuery); + + // Match prefix and query const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i); const prefix = splitQuery @@ -25,8 +35,11 @@ export const searchParser = (searchQuery: string): SearchResult => { ? encodeURIComponent(splitQuery[2]) : encodeURIComponent(searchQuery); - const query = queries.find((q: Query) => q.prefix === prefix); + const query = [...queries, ...customQueries].find( + (q: Query) => q.prefix === prefix + ); + // If search provider was found if (query) { result.query = query; result.search = search; diff --git a/controllers/apps.js b/controllers/apps.js index 4376d15..b976d45 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -131,7 +131,10 @@ exports.getApps = asyncWrapper(async (req, res, next) => { if (apps.some((app) => app.name === item.name)) { const app = apps.filter((e) => e.name === item.name)[0]; - if (item.icon === 'custom') { + if ( + item.icon === 'custom' || + (item.icon === 'docker' && app.icon != 'docker') + ) { await app.update({ name: item.name, url: item.url, diff --git a/controllers/config.js b/controllers/config.js index a9768d2..e5290aa 100644 --- a/controllers/config.js +++ b/controllers/config.js @@ -162,7 +162,7 @@ exports.getCss = asyncWrapper(async (req, res, next) => { // @access Public exports.updateCss = asyncWrapper(async (req, res, next) => { const file = new File(join(__dirname, '../public/flame.css')); - file.write(req.body.styles); + file.write(req.body.styles, false); // Copy file to docker volume fs.copyFileSync( diff --git a/controllers/queries/index.js b/controllers/queries/index.js new file mode 100644 index 0000000..ae1ccec --- /dev/null +++ b/controllers/queries/index.js @@ -0,0 +1,81 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const File = require('../../utils/File'); +const { join } = require('path'); + +const QUERIES_PATH = join(__dirname, '../../data/customQueries.json'); + +// @desc Add custom search query +// @route POST /api/queries +// @access Public +exports.addQuery = asyncWrapper(async (req, res, next) => { + const file = new File(QUERIES_PATH); + let content = JSON.parse(file.read()); + + // Add new query + content.queries.push(req.body); + file.write(content, true); + + res.status(201).json({ + success: true, + data: req.body, + }); +}); + +// @desc Get custom queries file +// @route GET /api/queries +// @access Public +exports.getQueries = asyncWrapper(async (req, res, next) => { + const file = new File(QUERIES_PATH); + const content = JSON.parse(file.read()); + + res.status(200).json({ + success: true, + data: content.queries, + }); +}); + +// @desc Update query +// @route PUT /api/queries/:prefix +// @access Public +exports.updateQuery = asyncWrapper(async (req, res, next) => { + const file = new File(QUERIES_PATH); + let content = JSON.parse(file.read()); + + let queryIdx = content.queries.findIndex( + (q) => q.prefix == req.params.prefix + ); + + // query found + if (queryIdx > -1) { + content.queries = [ + ...content.queries.slice(0, queryIdx), + req.body, + ...content.queries.slice(queryIdx + 1), + ]; + } + + file.write(content, true); + + res.status(200).json({ + success: true, + data: content.queries, + }); +}); + +// @desc Delete query +// @route DELETE /api/queries/:prefix +// @access Public +exports.deleteQuery = asyncWrapper(async (req, res, next) => { + const file = new File(QUERIES_PATH); + let content = JSON.parse(file.read()); + + content.queries = content.queries.filter( + (q) => q.prefix != req.params.prefix + ); + file.write(content, true); + + res.status(200).json({ + success: true, + data: content.queries, + }); +}); diff --git a/package.json b/package.json index dbeb5ee..dc8b92d 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "main": "index.js", "scripts": { "start": "node server.js", - "init-server": "echo Instaling server dependencies && npm install && mkdir public && touch public/flame.css", + "init-server": "echo Instaling server dependencies && npm install", "init-client": "cd client && echo Instaling client dependencies && npm install", - "dev-init": "npm run init-server && npm run init-client", - "dev-server": "nodemon server.js", + "dir-init": "npx mkdirp data public && touch public/flame.css public/customQueries.json", + "dev-init": "npm run dir-init && npm run init-server && npm run init-client", + "dev-server": "nodemon server.js -e js", "dev-client": "npm start --prefix client", "dev": "concurrently \"npm run dev-server\" \"npm run dev-client\"", "skaffold": "concurrently \"npm run init-client\" \"npm run dev-server\"" diff --git a/routes/config.js b/routes/config.js index eebf5dd..8c9ac15 100644 --- a/routes/config.js +++ b/routes/config.js @@ -12,21 +12,10 @@ const { getCss, } = require('../controllers/config'); -router - .route('/') - .post(createPair) - .get(getAllPairs) - .put(updateValues); +router.route('/').post(createPair).get(getAllPairs).put(updateValues); -router - .route('/:key') - .get(getSinglePair) - .put(updateValue) - .delete(deletePair); +router.route('/:key').get(getSinglePair).put(updateValue).delete(deletePair); -router - .route('/0/css') - .get(getCss) - .put(updateCss); +router.route('/0/css').get(getCss).put(updateCss); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/routes/queries.js b/routes/queries.js new file mode 100644 index 0000000..afacffd --- /dev/null +++ b/routes/queries.js @@ -0,0 +1,14 @@ +const express = require('express'); +const router = express.Router(); + +const { + getQueries, + addQuery, + deleteQuery, + updateQuery, +} = require('../controllers/queries/'); + +router.route('/').post(addQuery).get(getQueries); +router.route('/:prefix').delete(deleteQuery).put(updateQuery); + +module.exports = router; diff --git a/server.js b/server.js index 8b09803..5c1d0fa 100644 --- a/server.js +++ b/server.js @@ -1,23 +1,28 @@ require('dotenv').config(); const http = require('http'); + +// Database const { connectDB } = require('./db'); +const associateModels = require('./models/associateModels'); + +// Server const api = require('./api'); const jobs = require('./utils/jobs'); const Socket = require('./Socket'); const Sockets = require('./Sockets'); -const associateModels = require('./models/associateModels'); -const initConfig = require('./utils/initConfig'); -const findCss = require('./utils/findCss'); + +// Utils +const initApp = require('./utils/init'); const Logger = require('./utils/Logger'); const logger = new Logger(); -const PORT = process.env.PORT || 5005; - (async () => { + const PORT = process.env.PORT || 5005; + + // Init app await connectDB(); await associateModels(); - await initConfig(); - findCss(); + await initApp(); // Create server for Express API and WebSockets const server = http.createServer(); @@ -28,6 +33,8 @@ const PORT = process.env.PORT || 5005; Sockets.registerSocket('weather', weatherSocket); server.listen(PORT, () => { - logger.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`); - }) -})(); \ No newline at end of file + logger.log( + `Server is running on port ${PORT} in ${process.env.NODE_ENV} mode` + ); + }); +})(); diff --git a/utils/File.js b/utils/File.js index 0b2fbdc..f135da8 100644 --- a/utils/File.js +++ b/utils/File.js @@ -3,7 +3,7 @@ const fs = require('fs'); class File { constructor(path) { this.path = path; - this.content = ''; + this.content = null; } read() { @@ -16,10 +16,13 @@ class File { } } - write(data) { + write(data, isJSON) { this.content = data; - fs.writeFileSync(this.path, this.content); + fs.writeFileSync( + this.path, + isJSON ? JSON.stringify(this.content) : this.content + ); } } -module.exports = File; \ No newline at end of file +module.exports = File; diff --git a/utils/findCss.js b/utils/findCss.js deleted file mode 100644 index af85a0a..0000000 --- a/utils/findCss.js +++ /dev/null @@ -1,22 +0,0 @@ -const fs = require('fs'); -const { join } = require('path'); -const Logger = require('./Logger'); -const logger = new Logger(); - -// Check if flame.css exists in mounted docker volume. Create new file if not -const findCss = () => { - const srcPath = join(__dirname, '../data/flame.css'); - const destPath = join(__dirname, '../public/flame.css'); - - if (fs.existsSync(srcPath)) { - fs.copyFileSync(srcPath, destPath); - logger.log('Custom CSS file found'); - return; - } - - logger.log('Creating empty CSS file'); - fs.writeFileSync(destPath, ''); - -} - -module.exports = findCss; \ No newline at end of file diff --git a/utils/init/createFile.js b/utils/init/createFile.js new file mode 100644 index 0000000..b46b4f6 --- /dev/null +++ b/utils/init/createFile.js @@ -0,0 +1,32 @@ +const fs = require('fs'); +const { join } = require('path'); + +const Logger = require('../Logger'); +const logger = new Logger(); + +const createFile = async (file) => { + const { name, msg, template, isJSON, paths } = file; + + const srcPath = join(__dirname, paths.src, name); + const destPath = join(__dirname, paths.dest, name); + + // Check if file exists + if (fs.existsSync(srcPath)) { + fs.copyFileSync(srcPath, destPath); + + if (process.env.NODE_ENV == 'development') { + logger.log(msg.found); + } + + return; + } + + // Create file if not + fs.writeFileSync(destPath, isJSON ? JSON.stringify(template) : template); + + if (process.env.NODE_ENV == 'development') { + logger.log(msg.created); + } +}; + +module.exports = createFile; diff --git a/utils/init/index.js b/utils/init/index.js new file mode 100644 index 0000000..a0e11a1 --- /dev/null +++ b/utils/init/index.js @@ -0,0 +1,9 @@ +const initConfig = require('./initConfig'); +const initFiles = require('./initFiles'); + +const initApp = async () => { + await initConfig(); + await initFiles(); +}; + +module.exports = initApp; diff --git a/utils/initConfig.js b/utils/init/initConfig.js similarity index 78% rename from utils/initConfig.js rename to utils/init/initConfig.js index b6a5a18..83ce4ea 100644 --- a/utils/initConfig.js +++ b/utils/init/initConfig.js @@ -1,7 +1,8 @@ const { Op } = require('sequelize'); -const Config = require('../models/Config'); +const Config = require('../../models/Config'); const { config } = require('./initialConfig.json'); -const Logger = require('./Logger'); + +const Logger = require('../Logger'); const logger = new Logger(); const initConfig = async () => { @@ -28,7 +29,10 @@ const initConfig = async () => { } }); - logger.log('Initial config created'); + if (process.env.NODE_ENV == 'development') { + logger.log('Initial config created'); + } + return; }; diff --git a/utils/init/initFiles.js b/utils/init/initFiles.js new file mode 100644 index 0000000..cee54ca --- /dev/null +++ b/utils/init/initFiles.js @@ -0,0 +1,8 @@ +const createFile = require('./createFile'); +const { files } = require('./initialFiles.json'); + +const initFiles = async () => { + files.forEach(async (file) => await createFile(file)); +}; + +module.exports = initFiles; diff --git a/utils/initialConfig.json b/utils/init/initialConfig.json similarity index 100% rename from utils/initialConfig.json rename to utils/init/initialConfig.json diff --git a/utils/init/initialFiles.json b/utils/init/initialFiles.json new file mode 100644 index 0000000..42354d7 --- /dev/null +++ b/utils/init/initialFiles.json @@ -0,0 +1,32 @@ +{ + "files": [ + { + "name": "flame.css", + "msg": { + "created": "Created empty CSS file", + "found": "Custom CSS file found" + }, + "paths": { + "src": "../../data", + "dest": "../../public" + }, + "template": "", + "isJSON": false + }, + { + "name": "customQueries.json", + "msg": { + "created": "Created empty queries file", + "found": "Custom queries file found" + }, + "paths": { + "src": "../../data", + "dest": "../../data" + }, + "template": { + "queries": [] + }, + "isJSON": true + } + ] +}