Sfoglia il codice sorgente

Merge pull request #35 from pawelmalak/feature

v1.3 Release
pawelmalak 4 anni fa
parent
commit
ff1d11f512
34 ha cambiato i file con 500 aggiunte e 240 eliminazioni
  1. 14 1
      README.md
  2. 0 0
      client/src/App.module.css
  3. 5 9
      client/src/App.tsx
  4. 9 8
      client/src/components/Apps/AppCard/AppCard.tsx
  5. 9 1
      client/src/components/Apps/AppForm/AppForm.tsx
  6. 12 6
      client/src/components/Apps/Apps.tsx
  7. 19 14
      client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx
  8. 9 1
      client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx
  9. 1 1
      client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx
  10. 12 6
      client/src/components/Bookmarks/Bookmarks.tsx
  11. 65 42
      client/src/components/Home/Home.tsx
  12. 8 0
      client/src/components/Home/functions/dateTime.ts
  13. 12 0
      client/src/components/Home/functions/greeter.ts
  14. 50 45
      client/src/components/Settings/OtherSettings/OtherSettings.tsx
  15. 60 70
      client/src/components/Settings/WeatherSettings/WeatherSettings.tsx
  16. 1 1
      client/src/components/UI/Modal/Modal.tsx
  17. 29 22
      client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx
  18. 13 0
      client/src/interfaces/Forms.ts
  19. 2 0
      client/src/interfaces/GlobalState.ts
  20. 2 1
      client/src/interfaces/index.ts
  21. 12 3
      client/src/store/actions/actionTypes.ts
  22. 1 1
      client/src/store/actions/app.ts
  23. 2 2
      client/src/store/actions/bookmark.ts
  24. 52 0
      client/src/store/actions/config.ts
  25. 2 1
      client/src/store/actions/index.ts
  26. 36 0
      client/src/store/reducers/config.ts
  27. 3 1
      client/src/store/reducers/index.ts
  28. 1 3
      client/src/store/store.ts
  29. 5 0
      client/src/utility/iconParser.ts
  30. 3 0
      client/src/utility/index.ts
  31. 24 0
      client/src/utility/searchConfig.ts
  32. 20 0
      client/src/utility/urlParser.ts
  33. 3 1
      controllers/config.js
  34. 4 0
      utils/initialConfig.json

+ 14 - 1
README.md

@@ -59,4 +59,17 @@ docker run -p 5005:5005 -v <host_dir>:/app/data flame
 - Themes
 - Themes
   - Customize your page by choosing from 12 color themes 
   - Customize your page by choosing from 12 color themes 
 
 
-![Homescreen screenshot](./github/_themes.png)
+![Homescreen screenshot](./github/_themes.png)
+
+## Usage
+### Supported URL formats for applications and bookmarks
+#### Rules
+- URL starts with `http://`
+  - Format: `http://www.domain.com`, `http://domain.com`
+  - Redirect: `{dest}`
+- URL starts with `https://`
+  - Format: `https://www.domain.com`, `https://domain.com`
+  - Redirect: `https://{dest}`
+- URL without protocol
+  - Format: `www.domain.com`, `domain.com`, `sub.domain.com`, `local`, `ip`, `ip:port`
+  - Redirect: `http://{dest}`

+ 0 - 0
client/src/App.module.css


+ 5 - 9
client/src/App.tsx

@@ -1,27 +1,23 @@
 import { BrowserRouter, Route, Switch } from 'react-router-dom';
 import { BrowserRouter, Route, Switch } from 'react-router-dom';
-import { setTheme } from './store/actions';
+import { getConfig, setTheme } from './store/actions';
 
 
 // Redux
 // Redux
-import store from './store/store';
+import { store } from './store/store';
 import { Provider } from 'react-redux';
 import { Provider } from 'react-redux';
 
 
-import classes from './App.module.css';
-
 import Home from './components/Home/Home';
 import Home from './components/Home/Home';
 import Apps from './components/Apps/Apps';
 import Apps from './components/Apps/Apps';
 import Settings from './components/Settings/Settings';
 import Settings from './components/Settings/Settings';
 import Bookmarks from './components/Bookmarks/Bookmarks';
 import Bookmarks from './components/Bookmarks/Bookmarks';
-
 import NotificationCenter from './components/NotificationCenter/NotificationCenter';
 import NotificationCenter from './components/NotificationCenter/NotificationCenter';
 
 
+// Get config pairs from database
+store.dispatch<any>(getConfig());
+
 if (localStorage.theme) {
 if (localStorage.theme) {
   store.dispatch<any>(setTheme(localStorage.theme));
   store.dispatch<any>(setTheme(localStorage.theme));
 }
 }
 
 
-if (localStorage.customTitle) {
-  document.title = localStorage.customTitle;
-}
-
 const App = (): JSX.Element => {
 const App = (): JSX.Element => {
   return (
   return (
     <Provider store={store}>
     <Provider store={store}>

+ 9 - 8
client/src/components/Apps/AppCard/AppCard.tsx

@@ -1,8 +1,6 @@
-import { Link } from 'react-router-dom';
-
 import classes from './AppCard.module.css';
 import classes from './AppCard.module.css';
 import Icon from '../../UI/Icons/Icon/Icon';
 import Icon from '../../UI/Icons/Icon/Icon';
-import { iconParser } from '../../../utility/iconParser';
+import { iconParser, urlParser } from '../../../utility';
 
 
 import { App } from '../../../interfaces';
 import { App } from '../../../interfaces';
 
 
@@ -12,18 +10,21 @@ interface ComponentProps {
 }
 }
 
 
 const AppCard = (props: ComponentProps): JSX.Element => {
 const AppCard = (props: ComponentProps): JSX.Element => {
-  const redirectHandler = (url: string): void => {
-    window.open(url);
-  }
+  const [displayUrl, redirectUrl] = urlParser(props.app.url);
 
 
   return (
   return (
-    <a href={`http://${props.app.url}`} target='_blank' className={classes.AppCard}>
+    <a
+      href={redirectUrl}
+      target='_blank'
+      rel='noreferrer'
+      className={classes.AppCard}
+    >
       <div className={classes.AppCardIcon}>
       <div className={classes.AppCardIcon}>
         <Icon icon={iconParser(props.app.icon)} />
         <Icon icon={iconParser(props.app.icon)} />
       </div>
       </div>
       <div className={classes.AppCardDetails}>
       <div className={classes.AppCardDetails}>
         <h5>{props.app.name}</h5>
         <h5>{props.app.name}</h5>
-        <span>{props.app.url}</span>
+        <span>{displayUrl}</span>
       </div>
       </div>
     </a>
     </a>
   )
   )

+ 9 - 1
client/src/components/Apps/AppForm/AppForm.tsx

@@ -98,7 +98,15 @@ const AppForm = (props: ComponentProps): JSX.Element => {
           value={formData.url}
           value={formData.url}
           onChange={(e) => inputChangeHandler(e)}
           onChange={(e) => inputChangeHandler(e)}
         />
         />
-        <span>Only urls without http[s]:// are supported</span>
+        <span>
+          <a
+            href='https://github.com/pawelmalak/flame#supported-url-formats-for-applications-and-bookmarks'
+            target='_blank'
+            rel='noreferrer'
+          >
+            {' '}Check supported URL formats
+          </a>
+        </span>
       </InputGroup>
       </InputGroup>
       <InputGroup>
       <InputGroup>
         <label htmlFor='icon'>App Icon</label>
         <label htmlFor='icon'>App Icon</label>

+ 12 - 6
client/src/components/Apps/Apps.tsx

@@ -1,4 +1,4 @@
-import { Fragment, useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
 import { Link } from 'react-router-dom';
 import { Link } from 'react-router-dom';
 
 
 // Redux
 // Redux
@@ -30,6 +30,12 @@ interface ComponentProps {
 }
 }
 
 
 const Apps = (props: ComponentProps): JSX.Element => {
 const Apps = (props: ComponentProps): JSX.Element => {
+  const {
+    getApps,
+    apps,
+    loading
+  } = props;
+
   const [modalIsOpen, setModalIsOpen] = useState(false);
   const [modalIsOpen, setModalIsOpen] = useState(false);
   const [isInEdit, setIsInEdit] = useState(false);
   const [isInEdit, setIsInEdit] = useState(false);
   const [isInUpdate, setIsInUpdate] = useState(false);
   const [isInUpdate, setIsInUpdate] = useState(false);
@@ -44,10 +50,10 @@ const Apps = (props: ComponentProps): JSX.Element => {
   })
   })
 
 
   useEffect(() => {
   useEffect(() => {
-    if (props.apps.length === 0) {
-      props.getApps();
+    if (apps.length === 0) {
+      getApps();
     }
     }
-  }, [props.getApps]);
+  }, [getApps, apps]);
 
 
   const toggleModal = (): void => {
   const toggleModal = (): void => {
     setModalIsOpen(!modalIsOpen);
     setModalIsOpen(!modalIsOpen);
@@ -93,10 +99,10 @@ const Apps = (props: ComponentProps): JSX.Element => {
       </div>
       </div>
 
 
       <div className={classes.Apps}>
       <div className={classes.Apps}>
-        {props.loading
+        {loading
           ? <Spinner />
           ? <Spinner />
           : (!isInEdit
           : (!isInEdit
-              ? <AppGrid apps={props.apps} />
+              ? <AppGrid apps={apps} />
               : <AppTable updateAppHandler={toggleUpdate} />)
               : <AppTable updateAppHandler={toggleUpdate} />)
         }
         }
       </div>
       </div>

+ 19 - 14
client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx

@@ -2,7 +2,7 @@ import { Bookmark, Category } from '../../../interfaces';
 import classes from './BookmarkCard.module.css';
 import classes from './BookmarkCard.module.css';
 
 
 import Icon from '../../UI/Icons/Icon/Icon';
 import Icon from '../../UI/Icons/Icon/Icon';
-import { iconParser } from '../../../utility/iconParser';
+import { iconParser, urlParser } from '../../../utility';
 
 
 interface ComponentProps {
 interface ComponentProps {
   category: Category;
   category: Category;
@@ -13,19 +13,24 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
     <div className={classes.BookmarkCard}>
     <div className={classes.BookmarkCard}>
       <h3>{props.category.name}</h3>
       <h3>{props.category.name}</h3>
       <div className={classes.Bookmarks}>
       <div className={classes.Bookmarks}>
-        {props.category.bookmarks.map((bookmark: Bookmark) => (
-          <a
-            href={`http://${bookmark.url}`}
-            target='_blank'
-            key={`bookmark-${bookmark.id}`}>
-            {bookmark.icon && (
-              <div className={classes.BookmarkIcon}>
-                <Icon icon={iconParser(bookmark.icon)} />
-              </div>
-            )}
-            {bookmark.name}
-          </a>
-        ))}
+        {props.category.bookmarks.map((bookmark: Bookmark) => {
+          const redirectUrl = urlParser(bookmark.url)[1];
+
+          return (
+            <a
+              href={redirectUrl}
+              target='_blank'
+              rel='noreferrer'
+              key={`bookmark-${bookmark.id}`}>
+              {bookmark.icon && (
+                <div className={classes.BookmarkIcon}>
+                  <Icon icon={iconParser(bookmark.icon)} />
+                </div>
+              )}
+              {bookmark.name}
+            </a>
+          )
+        })}
       </div>
       </div>
     </div>
     </div>
   )
   )

+ 9 - 1
client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx

@@ -184,7 +184,15 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
                 value={formData.url}
                 value={formData.url}
                 onChange={(e) => inputChangeHandler(e)}
                 onChange={(e) => inputChangeHandler(e)}
               />
               />
-              <span>Only urls without http[s]:// are supported</span>
+              <span>
+                <a
+                  href='https://github.com/pawelmalak/flame#supported-url-formats-for-applications-and-bookmarks'
+                  target='_blank'
+                  rel='noreferrer'
+                >
+                  {' '}Check supported URL formats
+                </a>
+              </span>
             </InputGroup>
             </InputGroup>
             <InputGroup>
             <InputGroup>
               <label htmlFor='categoryId'>Bookmark Category</label>
               <label htmlFor='categoryId'>Bookmark Category</label>

+ 1 - 1
client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx

@@ -2,7 +2,7 @@ import { Link } from 'react-router-dom';
 
 
 import classes from './BookmarkGrid.module.css';
 import classes from './BookmarkGrid.module.css';
 
 
-import { Bookmark, Category } from '../../../interfaces';
+import { Category } from '../../../interfaces';
 
 
 import BookmarkCard from '../BookmarkCard/BookmarkCard';
 import BookmarkCard from '../BookmarkCard/BookmarkCard';
 
 

+ 12 - 6
client/src/components/Bookmarks/Bookmarks.tsx

@@ -28,6 +28,12 @@ export enum ContentType {
 }
 }
 
 
 const Bookmarks = (props: ComponentProps): JSX.Element => {
 const Bookmarks = (props: ComponentProps): JSX.Element => {
+  const {
+    getCategories,
+    categories,
+    loading
+  } = props;
+
   const [modalIsOpen, setModalIsOpen] = useState(false);
   const [modalIsOpen, setModalIsOpen] = useState(false);
   const [formContentType, setFormContentType] = useState(ContentType.category);
   const [formContentType, setFormContentType] = useState(ContentType.category);
   const [isInEdit, setIsInEdit] = useState(false);
   const [isInEdit, setIsInEdit] = useState(false);
@@ -52,10 +58,10 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
   })
   })
 
 
   useEffect(() => {
   useEffect(() => {
-    if (props.categories.length === 0) {
-      props.getCategories();
+    if (categories.length === 0) {
+      getCategories();
     }
     }
-  }, [props.getCategories])
+  }, [getCategories, categories])
 
 
   const toggleModal = (): void => {
   const toggleModal = (): void => {
     setModalIsOpen(!modalIsOpen);
     setModalIsOpen(!modalIsOpen);
@@ -132,13 +138,13 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
         />
         />
       </div>
       </div>
 
 
-      {props.loading
+      {loading
         ? <Spinner />
         ? <Spinner />
         : (!isInEdit
         : (!isInEdit
-          ? <BookmarkGrid categories={props.categories} />
+          ? <BookmarkGrid categories={categories} />
           : <BookmarkTable
           : <BookmarkTable
               contentType={tableContentType}
               contentType={tableContentType}
-              categories={props.categories}
+              categories={categories}
               updateHandler={goToUpdateMode}
               updateHandler={goToUpdateMode}
             />
             />
           )
           )

+ 65 - 42
client/src/components/Home/Home.tsx

@@ -1,4 +1,4 @@
-import { useEffect } from 'react';
+import { useState, useEffect } from 'react';
 import { Link } from 'react-router-dom';
 import { Link } from 'react-router-dom';
 
 
 // Redux
 // Redux
@@ -23,6 +23,13 @@ import AppGrid from '../Apps/AppGrid/AppGrid';
 import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid';
 import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid';
 import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget';
 import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget';
 
 
+// Functions
+import { greeter } from './functions/greeter';
+import { dateTime } from './functions/dateTime';
+
+// Utils
+import { searchConfig } from '../../utility';
+
 interface ComponentProps {
 interface ComponentProps {
   getApps: Function;
   getApps: Function;
   getCategories: Function;
   getCategories: Function;
@@ -33,68 +40,84 @@ interface ComponentProps {
 }
 }
 
 
 const Home = (props: ComponentProps): JSX.Element => {
 const Home = (props: ComponentProps): JSX.Element => {
+  const {
+    getApps,
+    apps,
+    appsLoading,
+    getCategories,
+    categories,
+    categoriesLoading
+  } = props;
+
+  const [header, setHeader] = useState({
+    dateTime: dateTime(),
+    greeting: greeter()
+  })
+
+  // Load applications
   useEffect(() => {
   useEffect(() => {
-    if (props.apps.length === 0) {
-      props.getApps();
+    if (apps.length === 0) {
+      getApps();
     }
     }
-  }, [props.getApps]);
+  }, [getApps, apps]);
 
 
+  // Load bookmark categories
   useEffect(() => {
   useEffect(() => {
-    if (props.categories.length === 0) {
-      props.getCategories();
+    if (categories.length === 0) {
+      getCategories();
     }
     }
-  }, [props.getCategories]);
-
-  const dateAndTime = (): string => {
-    const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
-    const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
-
-    const now = new Date();
-
-    return `${days[now.getDay()]}, ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`;
-  }
+  }, [getCategories, categories]);
 
 
-  const greeter = (): string => {
-    const now = new Date().getHours();
-    let msg: string;
-
-    if (now >= 18) msg = 'Good evening!';
-    else if (now >= 12) msg = 'Good afternoon!';
-    else if (now >= 6) msg = 'Good morning!';
-    else if (now >= 0) msg = 'Good night!';
-    else msg = 'Hello!';
-
-    return msg;
-  }
+  // Refresh greeter and time
+  useEffect(() => {
+    let interval: any;
+
+    // Start interval only when hideHeader is false
+    if (searchConfig('hideHeader', 0) !== 1) {
+      interval = setInterval(() => {
+        setHeader({
+          dateTime: dateTime(),
+          greeting: greeter()
+        })
+      }, 1000);
+    }
 
 
+    return () => clearInterval(interval);
+  }, [])
+  
   return (
   return (
     <Container>
     <Container>
-      <header className={classes.Header}>
-        <p>{dateAndTime()}</p>
-        <Link to='/settings' className={classes.SettingsLink}>Go to Settings</Link>
-        <span className={classes.HeaderMain}>
-          <h1>{greeter()}</h1>
-          <WeatherWidget />
-        </span>
-      </header>
+      {searchConfig('hideHeader', 0) !== 1
+        ? (
+          <header className={classes.Header}>
+            <p>{header.dateTime}</p>
+            <Link to='/settings' className={classes.SettingsLink}>Go to Settings</Link>
+            <span className={classes.HeaderMain}>
+              <h1>{header.greeting}</h1>
+              <WeatherWidget />
+            </span>
+          </header>
+          )
+        : <div></div>
+      }
       
       
       <SectionHeadline title='Applications' link='/applications' />
       <SectionHeadline title='Applications' link='/applications' />
-      {props.appsLoading
+      {appsLoading
         ? <Spinner />
         ? <Spinner />
         : <AppGrid
         : <AppGrid
-          apps={props.apps.filter((app: App) => app.isPinned)}
-          totalApps={props.apps.length}
+          apps={apps.filter((app: App) => app.isPinned)}
+          totalApps={apps.length}
         />
         />
       }
       }
 
 
       <div className={classes.HomeSpace}></div>
       <div className={classes.HomeSpace}></div>
 
 
       <SectionHeadline title='Bookmarks' link='/bookmarks' />
       <SectionHeadline title='Bookmarks' link='/bookmarks' />
-      {props.categoriesLoading
+      {categoriesLoading
         ? <Spinner />
         ? <Spinner />
         : <BookmarkGrid
         : <BookmarkGrid
-            categories={props.categories.filter((category: Category) => category.isPinned)}
-            totalCategories={props.categories.length}
+            categories={categories.filter((category: Category) => category.isPinned)}
+            totalCategories={categories.length}
         />
         />
       }
       }
 
 

+ 8 - 0
client/src/components/Home/functions/dateTime.ts

@@ -0,0 +1,8 @@
+export const dateTime = (): string => {
+  const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+  const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+
+  const now = new Date();
+
+  return `${days[now.getDay()]}, ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`;
+}

+ 12 - 0
client/src/components/Home/functions/greeter.ts

@@ -0,0 +1,12 @@
+export const greeter = (): string => {
+  const now = new Date().getHours();
+  let msg: string;
+
+  if (now >= 18) msg = 'Good evening!';
+  else if (now >= 12) msg = 'Good afternoon!';
+  else if (now >= 6) msg = 'Good morning!';
+  else if (now >= 0) msg = 'Good night!';
+  else msg = 'Hello!';
+
+  return msg;
+}

+ 50 - 45
client/src/components/Settings/OtherSettings/OtherSettings.tsx

@@ -1,69 +1,56 @@
 import { useState, useEffect, ChangeEvent, FormEvent } from 'react';
 import { useState, useEffect, ChangeEvent, FormEvent } from 'react';
-import axios from 'axios';
+
+// Redux
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
+import { createNotification, updateConfig } from '../../../store/actions';
+
+// Typescript
+import { GlobalState, NewNotification, SettingsForm } from '../../../interfaces';
 
 
+// UI
 import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
 import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
 import Button from '../../UI/Buttons/Button/Button';
 import Button from '../../UI/Buttons/Button/Button';
-import { createNotification } from '../../../store/actions';
-import { ApiResponse, Config, NewNotification } from '../../../interfaces';
 
 
-interface FormState {
-  customTitle: string;
-  pinAppsByDefault: number;
-  pinCategoriesByDefault: number;
-}
+// Utils
+import { searchConfig } from '../../../utility';
 
 
 interface ComponentProps {
 interface ComponentProps {
   createNotification: (notification: NewNotification) => void;
   createNotification: (notification: NewNotification) => void;
+  updateConfig: (formData: SettingsForm) => void;
+  loading: boolean;
 }
 }
 
 
 const OtherSettings = (props: ComponentProps): JSX.Element => {
 const OtherSettings = (props: ComponentProps): JSX.Element => {
-  const [formData, setFormData] = useState<FormState>({
+  // Initial state
+  const [formData, setFormData] = useState<SettingsForm>({
     customTitle: document.title,
     customTitle: document.title,
-    pinAppsByDefault: 0,
-    pinCategoriesByDefault: 0
+    pinAppsByDefault: 1,
+    pinCategoriesByDefault: 1,
+    hideHeader: 0
   })
   })
 
 
-  // get initial config
+  // Get config
   useEffect(() => {
   useEffect(() => {
-    axios.get<ApiResponse<Config[]>>('/api/config?keys=customTitle,pinAppsByDefault,pinCategoriesByDefault')
-      .then(data => {
-        let tmpFormData = { ...formData };
-
-        data.data.data.forEach((config: Config) => {
-          let value: string | number = config.value;
-          if (config.valueType === 'number') {
-            value = parseFloat(value);
-          }
-
-          tmpFormData = {
-            ...tmpFormData,
-            [config.key]: value
-          }
-        })
-
-        setFormData(tmpFormData);
-      })
-      .catch(err => console.log(err));
-  }, [])
-
-  const formSubmitHandler = (e: FormEvent) => {
+    setFormData({
+      customTitle: searchConfig('customTitle', 'Flame'),
+      pinAppsByDefault: searchConfig('pinAppsByDefault', 1),
+      pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1),
+      hideHeader: searchConfig('hideHeader', 0)
+    })
+  }, [props.loading]);
+
+  // Form handler
+  const formSubmitHandler = async (e: FormEvent) => {
     e.preventDefault();
     e.preventDefault();
 
 
-    axios.put<ApiResponse<{}>>('/api/config', formData)
-      .then(() => {
-        props.createNotification({
-          title: 'Success',
-          message: 'Settings updated'
-        })
-      })
-      .catch((err) => console.log(err));
+    // Save settings
+    await props.updateConfig(formData);
 
 
     // update local page title
     // update local page title
-    localStorage.setItem('customTitle', formData.customTitle);
     document.title = formData.customTitle;
     document.title = formData.customTitle;
   }
   }
 
 
+  // Input handler
   const inputChangeHandler = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, isNumber?: boolean) => {
   const inputChangeHandler = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, isNumber?: boolean) => {
     let value: string | number = e.target.value;
     let value: string | number = e.target.value;
 
 
@@ -80,7 +67,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
   return (
   return (
     <form onSubmit={(e) => formSubmitHandler(e)}>
     <form onSubmit={(e) => formSubmitHandler(e)}>
       <InputGroup>
       <InputGroup>
-        <label htmlFor='customTitle'>Custom Page Title</label>
+        <label htmlFor='customTitle'>Custom page title</label>
         <input
         <input
           type='text'
           type='text'
           id='customTitle'
           id='customTitle'
@@ -114,9 +101,27 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
           <option value={0}>False</option>
           <option value={0}>False</option>
         </select>
         </select>
       </InputGroup>
       </InputGroup>
+      <InputGroup>
+        <label htmlFor='hideHeader'>Hide greeting and date</label>
+        <select
+          id='hideHeader'
+          name='hideHeader'
+          value={formData.hideHeader}
+          onChange={(e) => inputChangeHandler(e, true)}
+        >
+          <option value={1}>True</option>
+          <option value={0}>False</option>
+        </select>
+      </InputGroup>
     <Button>Save changes</Button>
     <Button>Save changes</Button>
     </form>
     </form>
   )
   )
 }
 }
 
 
-export default connect(null, { createNotification })(OtherSettings);
+const mapStateToProps = (state: GlobalState) => {
+  return {
+    loading: state.config.loading
+  }
+}
+
+export default connect(mapStateToProps, { createNotification, updateConfig })(OtherSettings);

+ 60 - 70
client/src/components/Settings/WeatherSettings/WeatherSettings.tsx

@@ -1,110 +1,94 @@
 import { useState, ChangeEvent, useEffect, FormEvent } from 'react';
 import { useState, ChangeEvent, useEffect, FormEvent } from 'react';
-import { connect } from 'react-redux';
 import axios from 'axios';
 import axios from 'axios';
-import { ApiResponse, Config, NewNotification, Weather } from '../../../interfaces';
 
 
+// Redux
+import { connect } from 'react-redux';
+import { createNotification, updateConfig } from '../../../store/actions';
+
+// Typescript
+import { ApiResponse, GlobalState, NewNotification, Weather, WeatherForm } from '../../../interfaces';
+
+// UI
 import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
 import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
 import Button from '../../UI/Buttons/Button/Button';
 import Button from '../../UI/Buttons/Button/Button';
-import { createNotification } from '../../../store/actions';
 
 
-interface FormState {
-  WEATHER_API_KEY: string;
-  lat: number;
-  long: number;
-  isCelsius: number;
-}
+// Utils
+import { searchConfig } from '../../../utility';
 
 
 interface ComponentProps {
 interface ComponentProps {
   createNotification: (notification: NewNotification) => void;
   createNotification: (notification: NewNotification) => void;
+  updateConfig: (formData: WeatherForm) => void;
+  loading: boolean;
 }
 }
 
 
 const WeatherSettings = (props: ComponentProps): JSX.Element => {
 const WeatherSettings = (props: ComponentProps): JSX.Element => {
-  const [formData, setFormData] = useState<FormState>({
+  // Initial state
+  const [formData, setFormData] = useState<WeatherForm>({
     WEATHER_API_KEY: '',
     WEATHER_API_KEY: '',
     lat: 0,
     lat: 0,
     long: 0,
     long: 0,
     isCelsius: 1
     isCelsius: 1
   })
   })
 
 
-  const inputChangeHandler = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, isNumber?: boolean) => {
-    let value: string | number = e.target.value;
-
-    if (isNumber) {
-      value = parseFloat(value);
-    }
-
+  // Get config
+  useEffect(() => {
     setFormData({
     setFormData({
-      ...formData,
-      [e.target.name]: value
+      WEATHER_API_KEY: searchConfig('WEATHER_API_KEY', ''),
+      lat: searchConfig('lat', 0),
+      long: searchConfig('long', 0),
+      isCelsius: searchConfig('isCelsius', 1)
     })
     })
-  }
-
-  useEffect(() => {
-    axios.get<ApiResponse<Config[]>>('/api/config?keys=WEATHER_API_KEY,lat,long,isCelsius')
-      .then(data => {
-        let tmpFormData = { ...formData };
-
-        data.data.data.forEach((config: Config) => {
-          let value: string | number = config.value;
-          if (config.valueType === 'number') {
-            value = parseFloat(value);
-          }
-
-          tmpFormData = {
-            ...tmpFormData,
-            [config.key]: value
-          }
-        })
-
-        setFormData(tmpFormData);
-      })
-      .catch(err => console.log(err));
-  }, []);
+  }, [props.loading]);
 
 
-  const formSubmitHandler = (e: FormEvent) => {
+  // Form handler
+  const formSubmitHandler = async (e: FormEvent) => {
     e.preventDefault();
     e.preventDefault();
 
 
     // Check for api key input
     // Check for api key input
     if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) {
     if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) {
       props.createNotification({
       props.createNotification({
         title: 'Warning',
         title: 'Warning',
-        message: 'API Key is missing. Weather Module will NOT work'
+        message: 'API key is missing. Weather Module will NOT work'
       })
       })
     }
     }
 
 
     // Save settings
     // Save settings
-    axios.put<ApiResponse<{}>>('/api/config', formData)
+    await props.updateConfig(formData);
+    
+    // Update weather
+    axios.get<ApiResponse<Weather>>('/api/weather/update')
       .then(() => {
       .then(() => {
         props.createNotification({
         props.createNotification({
           title: 'Success',
           title: 'Success',
-          message: 'Settings updated'
+          message: 'Weather updated'
         })
         })
-
-        // Update weather with new settings
-        axios.get<ApiResponse<Weather>>('/api/weather/update')
-          .then(() => {
-            props.createNotification({
-              title: 'Success',
-              message: 'Weather updated'
-            })
-          })
-          .catch((err) => {
-            props.createNotification({
-              title: 'Error',
-              message: err.response.data.error
-            })
-          });
       })
       })
-      .catch(err => console.log(err));
-    
-    // set localStorage
-    localStorage.setItem('isCelsius', JSON.stringify(parseInt(`${formData.isCelsius}`) === 1))
+      .catch((err) => {
+        props.createNotification({
+          title: 'Error',
+          message: err.response.data.error
+        })
+      });
+  }
+
+  // Input handler
+  const inputChangeHandler = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, isNumber?: boolean) => {
+    let value: string | number = e.target.value;
+
+    if (isNumber) {
+      value = parseFloat(value);
+    }
+
+    setFormData({
+      ...formData,
+      [e.target.name]: value
+    })
   }
   }
 
 
   return (
   return (
     <form onSubmit={(e) => formSubmitHandler(e)}>
     <form onSubmit={(e) => formSubmitHandler(e)}>
       <InputGroup>
       <InputGroup>
-        <label htmlFor='WEATHER_API_KEY'>API Key</label>
+        <label htmlFor='WEATHER_API_KEY'>API key</label>
         <input
         <input
           type='text'
           type='text'
           id='WEATHER_API_KEY'
           id='WEATHER_API_KEY'
@@ -124,7 +108,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
         </span>
         </span>
       </InputGroup>
       </InputGroup>
       <InputGroup>
       <InputGroup>
-        <label htmlFor='lat'>Location Latitude</label>
+        <label htmlFor='lat'>Location latitude</label>
         <input
         <input
           type='number'
           type='number'
           id='lat'
           id='lat'
@@ -143,7 +127,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
         </span>
         </span>
       </InputGroup>
       </InputGroup>
       <InputGroup>
       <InputGroup>
-        <label htmlFor='long'>Location Longitude</label>
+        <label htmlFor='long'>Location longitude</label>
         <input
         <input
           type='number'
           type='number'
           id='long'
           id='long'
@@ -154,7 +138,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
         />
         />
       </InputGroup>
       </InputGroup>
       <InputGroup>
       <InputGroup>
-        <label htmlFor='isCelsius'>Temperature Unit</label>
+        <label htmlFor='isCelsius'>Temperature unit</label>
         <select
         <select
           id='isCelsius'
           id='isCelsius'
           name='isCelsius'
           name='isCelsius'
@@ -170,4 +154,10 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
   )
   )
 }
 }
 
 
-export default connect(null, { createNotification })(WeatherSettings);
+const mapStateToProps = (state: GlobalState) => {
+  return {
+    loading: state.config.loading
+  }
+}
+
+export default connect(mapStateToProps, { createNotification, updateConfig })(WeatherSettings);

+ 1 - 1
client/src/components/UI/Modal/Modal.tsx

@@ -1,4 +1,4 @@
-import { MouseEvent, useRef, useEffect } from 'react';
+import { MouseEvent, useRef } from 'react';
 
 
 import classes from './Modal.module.css';
 import classes from './Modal.module.css';
 
 

+ 29 - 22
client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx

@@ -1,12 +1,27 @@
 import { useState, useEffect, Fragment } from 'react';
 import { useState, useEffect, Fragment } from 'react';
-import { Weather, ApiResponse, Config } from '../../../interfaces';
 import axios from 'axios';
 import axios from 'axios';
 
 
-import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon';
+// Redux
+import { connect } from 'react-redux';
+
+// Typescript
+import { Weather, ApiResponse, Config, GlobalState } from '../../../interfaces';
 
 
+// CSS
 import classes from './WeatherWidget.module.css';
 import classes from './WeatherWidget.module.css';
 
 
-const WeatherWidget = (): JSX.Element => {
+// UI
+import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon';
+
+// Utils
+import { searchConfig } from '../../../utility';
+
+interface ComponentProps {
+  configLoading: boolean;
+  config: Config[];
+}
+
+const WeatherWidget = (props: ComponentProps): JSX.Element => {
   const [weather, setWeather] = useState<Weather>({
   const [weather, setWeather] = useState<Weather>({
     externalLastUpdate: '',
     externalLastUpdate: '',
     tempC: 0,
     tempC: 0,
@@ -20,11 +35,9 @@ const WeatherWidget = (): JSX.Element => {
     updatedAt: new Date()
     updatedAt: new Date()
   });
   });
   const [isLoading, setIsLoading] = useState(true);
   const [isLoading, setIsLoading] = useState(true);
-  const [isCelsius, setIsCelsius] = useState(true);
 
 
   // Initial request to get data
   // Initial request to get data
   useEffect(() => {
   useEffect(() => {
-    // get weather
     axios.get<ApiResponse<Weather[]>>('/api/weather')
     axios.get<ApiResponse<Weather[]>>('/api/weather')
       .then(data => {
       .then(data => {
         const weatherData = data.data.data[0];
         const weatherData = data.data.data[0];
@@ -34,18 +47,6 @@ const WeatherWidget = (): JSX.Element => {
         setIsLoading(false);
         setIsLoading(false);
       })
       })
       .catch(err => console.log(err));
       .catch(err => console.log(err));
-    
-    // get config
-    if (!localStorage.isCelsius) {
-      axios.get<ApiResponse<Config>>('/api/config/isCelsius')
-        .then((data) => {
-          setIsCelsius(parseInt(data.data.data.value) === 1);
-          localStorage.setItem('isCelsius', JSON.stringify(isCelsius));
-        })
-        .catch((err) => console.log(err));
-    } else {
-      setIsCelsius(JSON.parse(localStorage.isCelsius));
-    }
   }, []);
   }, []);
 
 
   // Open socket for data updates
   // Open socket for data updates
@@ -67,9 +68,8 @@ const WeatherWidget = (): JSX.Element => {
 
 
   return (
   return (
     <div className={classes.WeatherWidget}>
     <div className={classes.WeatherWidget}>
-      {isLoading
-        ? 'loading'
-        : (weather.id > 0 && 
+      {(isLoading || props.configLoading || searchConfig('WEATHER_API_KEY', '')) && 
+         (weather.id > 0 && 
             (<Fragment>
             (<Fragment>
               <div className={classes.WeatherIcon}>
               <div className={classes.WeatherIcon}>
                 <WeatherIcon
                 <WeatherIcon
@@ -78,7 +78,7 @@ const WeatherWidget = (): JSX.Element => {
                 />
                 />
               </div>
               </div>
               <div className={classes.WeatherDetails}>
               <div className={classes.WeatherDetails}>
-                {isCelsius
+                {searchConfig('isCelsius', true)
                   ? <span>{weather.tempC}°C</span>
                   ? <span>{weather.tempC}°C</span>
                   : <span>{weather.tempF}°F</span>
                   : <span>{weather.tempF}°F</span>
                 }
                 }
@@ -91,4 +91,11 @@ const WeatherWidget = (): JSX.Element => {
   )
   )
 }
 }
 
 
-export default WeatherWidget;
+const mapStateToProps = (state: GlobalState) => {
+  return {
+    configLoading: state.config.loading,
+    config: state.config.config
+  }
+}
+
+export default connect(mapStateToProps)(WeatherWidget);

+ 13 - 0
client/src/interfaces/Forms.ts

@@ -0,0 +1,13 @@
+export interface WeatherForm {
+  WEATHER_API_KEY: string;
+  lat: number;
+  long: number;
+  isCelsius: number;
+}
+
+export interface SettingsForm {
+  customTitle: string;
+  pinAppsByDefault: number;
+  pinCategoriesByDefault: number;
+  hideHeader: number;
+}

+ 2 - 0
client/src/interfaces/GlobalState.ts

@@ -2,10 +2,12 @@ import { State as AppState } from '../store/reducers/app';
 import { State as ThemeState } from '../store/reducers/theme';
 import { State as ThemeState } from '../store/reducers/theme';
 import { State as BookmarkState } from '../store/reducers/bookmark';
 import { State as BookmarkState } from '../store/reducers/bookmark';
 import { State as NotificationState } from '../store/reducers/notification';
 import { State as NotificationState } from '../store/reducers/notification';
+import { State as ConfigState } from '../store/reducers/config';
 
 
 export interface GlobalState {
 export interface GlobalState {
   theme: ThemeState;
   theme: ThemeState;
   app: AppState;
   app: AppState;
   bookmark: BookmarkState;
   bookmark: BookmarkState;
   notification: NotificationState;
   notification: NotificationState;
+  config: ConfigState;
 }
 }

+ 2 - 1
client/src/interfaces/index.ts

@@ -6,4 +6,5 @@ export * from './Weather';
 export * from './Bookmark';
 export * from './Bookmark';
 export * from './Category';
 export * from './Category';
 export * from './Notification';
 export * from './Notification';
-export * from './Config';
+export * from './Config';
+export * from './Forms';

+ 12 - 3
client/src/store/actions/actionTypes.ts

@@ -19,7 +19,10 @@ import {
   UpdateBookmarkAction,
   UpdateBookmarkAction,
   // Notifications
   // Notifications
   CreateNotificationAction,
   CreateNotificationAction,
-  ClearNotificationAction
+  ClearNotificationAction,
+  // Config
+  GetConfigAction,
+  UpdateConfigAction
 } from './';
 } from './';
 
 
 export enum ActionTypes {
 export enum ActionTypes {
@@ -48,7 +51,10 @@ export enum ActionTypes {
   updateBookmark = 'UPDATE_BOOKMARK',
   updateBookmark = 'UPDATE_BOOKMARK',
   // Notifications
   // Notifications
   createNotification = 'CREATE_NOTIFICATION',
   createNotification = 'CREATE_NOTIFICATION',
-  clearNotification = 'CLEAR_NOTIFICATION'
+  clearNotification = 'CLEAR_NOTIFICATION',
+  // Config
+  getConfig = 'GET_CONFIG',
+  updateConfig = 'UPDATE_CONFIG'
 }
 }
 
 
 export type Action = 
 export type Action = 
@@ -72,4 +78,7 @@ export type Action =
   UpdateBookmarkAction |
   UpdateBookmarkAction |
   // Notifications
   // Notifications
   CreateNotificationAction |
   CreateNotificationAction |
-  ClearNotificationAction;
+  ClearNotificationAction |
+  // Config
+  GetConfigAction |
+  UpdateConfigAction;

+ 1 - 1
client/src/store/actions/app.ts

@@ -89,7 +89,7 @@ export interface DeleteAppAction {
 
 
 export const deleteApp = (id: number) => async (dispatch: Dispatch) => {
 export const deleteApp = (id: number) => async (dispatch: Dispatch) => {
   try {
   try {
-    const res = await axios.delete<ApiResponse<{}>>(`/api/apps/${id}`);
+    await axios.delete<ApiResponse<{}>>(`/api/apps/${id}`);
 
 
     dispatch<CreateNotificationAction>({
     dispatch<CreateNotificationAction>({
       type: ActionTypes.createNotification,
       type: ActionTypes.createNotification,

+ 2 - 2
client/src/store/actions/bookmark.ts

@@ -130,7 +130,7 @@ export interface DeleteCategoryAction {
 
 
 export const deleteCategory = (id: number) => async (dispatch: Dispatch) => {
 export const deleteCategory = (id: number) => async (dispatch: Dispatch) => {
   try {
   try {
-    const res = await axios.delete<ApiResponse<{}>>(`/api/categories/${id}`);
+    await axios.delete<ApiResponse<{}>>(`/api/categories/${id}`);
 
 
     dispatch<CreateNotificationAction>({
     dispatch<CreateNotificationAction>({
       type: ActionTypes.createNotification,
       type: ActionTypes.createNotification,
@@ -191,7 +191,7 @@ export interface DeleteBookmarkAction {
 
 
 export const deleteBookmark = (bookmarkId: number, categoryId: number) => async (dispatch: Dispatch) => {
 export const deleteBookmark = (bookmarkId: number, categoryId: number) => async (dispatch: Dispatch) => {
   try {
   try {
-    const res = await axios.delete<ApiResponse<{}>>(`/api/bookmarks/${bookmarkId}`);
+    await axios.delete<ApiResponse<{}>>(`/api/bookmarks/${bookmarkId}`);
 
 
     dispatch<CreateNotificationAction>({
     dispatch<CreateNotificationAction>({
       type: ActionTypes.createNotification,
       type: ActionTypes.createNotification,

+ 52 - 0
client/src/store/actions/config.ts

@@ -0,0 +1,52 @@
+import axios from 'axios';
+import { Dispatch } from 'redux';
+import { ActionTypes } from './actionTypes';
+import { Config, ApiResponse } from '../../interfaces';
+import { CreateNotificationAction } from './notification';
+import { searchConfig } from '../../utility';
+
+export interface GetConfigAction {
+  type: ActionTypes.getConfig;
+  payload: Config[];
+}
+
+export const getConfig = () => async (dispatch: Dispatch) => {
+  try {
+    const res = await axios.get<ApiResponse<Config[]>>('/api/config');
+   
+    dispatch<GetConfigAction>({
+      type: ActionTypes.getConfig,
+      payload: res.data.data
+    })
+
+    // Set custom page title if set
+    document.title = searchConfig('customTitle', 'Flame');
+  } catch (err) {
+    console.log(err)
+  }
+}
+
+export interface UpdateConfigAction {
+  type: ActionTypes.updateConfig;
+  payload: Config[];
+}
+
+export const updateConfig = (formData: any) => async (dispatch: Dispatch) => {
+  try {
+    const res = await axios.put<ApiResponse<Config[]>>('/api/config', formData);
+    dispatch<CreateNotificationAction>({
+      type: ActionTypes.createNotification,
+      payload: {
+        title: 'Success',
+        message: 'Settings updated'
+      }
+    })
+
+    dispatch<UpdateConfigAction>({
+      type: ActionTypes.updateConfig,
+      payload: res.data.data
+    })
+  } catch (err) {
+    console.log(err);
+  }
+}

+ 2 - 1
client/src/store/actions/index.ts

@@ -2,4 +2,5 @@ export * from './theme';
 export * from './app';
 export * from './app';
 export * from './actionTypes';
 export * from './actionTypes';
 export * from './bookmark';
 export * from './bookmark';
-export * from './notification';
+export * from './notification';
+export * from './config';

+ 36 - 0
client/src/store/reducers/config.ts

@@ -0,0 +1,36 @@
+import { ActionTypes, Action } from '../actions';
+import { Config } from '../../interfaces';
+
+export interface State {
+  loading: boolean;
+  config: Config[];
+}
+
+const initialState: State = {
+  loading: true,
+  config: []
+}
+
+const getConfig = (state: State, action: Action): State => {
+  return {
+    loading: false,
+    config: action.payload
+  }
+}
+
+const updateConfig = (state: State, action: Action): State => {
+  return {
+    ...state,
+    config: 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;
+  }
+}
+
+export default configReducer;

+ 3 - 1
client/src/store/reducers/index.ts

@@ -6,12 +6,14 @@ import themeReducer from './theme';
 import appReducer from './app';
 import appReducer from './app';
 import bookmarkReducer from './bookmark';
 import bookmarkReducer from './bookmark';
 import notificationReducer from './notification';
 import notificationReducer from './notification';
+import configReducer from './config';
 
 
 const rootReducer = combineReducers<GlobalState>({
 const rootReducer = combineReducers<GlobalState>({
   theme: themeReducer,
   theme: themeReducer,
   app: appReducer,
   app: appReducer,
   bookmark: bookmarkReducer,
   bookmark: bookmarkReducer,
-  notification: notificationReducer
+  notification: notificationReducer,
+  config: configReducer
 })
 })
 
 
 export default rootReducer;
 export default rootReducer;

+ 1 - 3
client/src/store/store.ts

@@ -4,6 +4,4 @@ import thunk from 'redux-thunk';
 import rootReducer from './reducers';
 import rootReducer from './reducers';
 const initialState = {};
 const initialState = {};
 
 
-const store = createStore(rootReducer, initialState, composeWithDevTools(applyMiddleware(thunk)));
-
-export default store;
+export const store = createStore(rootReducer, initialState, composeWithDevTools(applyMiddleware(thunk)));

+ 5 - 0
client/src/utility/iconParser.ts

@@ -1,3 +1,8 @@
+/**
+ * Parse Material Desgin icon name to be used with mdi/js
+ * @param mdiName Dash separated icon name from MDI, e.g. alert-box-outline
+ * @returns Parsed icon name to be used with mdi/js, e.g mdiAlertBoxOutline
+ */
 export const iconParser = (mdiName: string): string => {
 export const iconParser = (mdiName: string): string => {
   let parsedName = mdiName
   let parsedName = mdiName
     .split('-')
     .split('-')

+ 3 - 0
client/src/utility/index.ts

@@ -0,0 +1,3 @@
+export * from './iconParser';
+export * from './urlParser';
+export * from './searchConfig';

+ 24 - 0
client/src/utility/searchConfig.ts

@@ -0,0 +1,24 @@
+import { store } from '../store/store';
+
+/**
+ * Search config store with given key
+ * @param key Config pair key to search
+ * @param _default Value to return if key is not found
+ */
+export const searchConfig = (key: string, _default: any) => {
+  const state = store.getState();
+
+  const pair = state.config.config.find(p => p.key === key);
+
+  if (pair) {
+    if (pair.valueType === 'number') {
+      return parseFloat(pair.value);
+    } else if (pair.valueType === 'boolean') {
+      return parseInt(pair.value);
+    } else {
+      return pair.value;
+    }
+  } else {
+    return _default;
+  }
+}

+ 20 - 0
client/src/utility/urlParser.ts

@@ -0,0 +1,20 @@
+export const urlParser = (url: string): string[] => {
+  let parsedUrl: string;
+  let displayUrl: string;
+
+  if (/https?:\/\//.test(url)) {
+    // Url starts with http[s]:// -> leave it as it is
+    parsedUrl = url;
+  } else {
+    // No protocol -> apply http:// prefix
+    parsedUrl = `http://${url}`;
+  }
+
+  // Create simplified url to display as text
+  displayUrl = url
+    .replace(/https?:\/\//, '')
+    .replace('www.', '')
+    .replace(/\/$/, '');
+
+  return [displayUrl, parsedUrl]
+}

+ 3 - 1
controllers/config.js

@@ -96,9 +96,11 @@ exports.updateValues = asyncWrapper(async (req, res, next) => {
     })
     })
   })
   })
   
   
+  const config = await Config.findAll();
+
   res.status(200).send({
   res.status(200).send({
     success: true,
     success: true,
-    data: {}
+    data: config
   })
   })
 })
 })
 
 

+ 4 - 0
utils/initialConfig.json

@@ -27,6 +27,10 @@
     {
     {
       "key": "pinCategoriesByDefault",
       "key": "pinCategoriesByDefault",
       "value": true
       "value": true
+    },
+    {
+      "key": "hideHeader",
+      "value": false
     }
     }
   ]
   ]
 }
 }