Browse Source

Merge pull request #206 from pawelmalak/feature

Version 2.0.1
pawelmalak 3 years ago
parent
commit
fd7d8e65c8
44 changed files with 477 additions and 229 deletions
  1. 1 1
      .env
  2. 8 0
      CHANGELOG.md
  3. 1 1
      README.md
  4. 1 1
      client/.env
  5. 17 5
      client/src/App.tsx
  6. 1 1
      client/src/components/Apps/AppForm/AppForm.tsx
  7. 1 1
      client/src/components/Bookmarks/Form/BookmarksForm.tsx
  8. 17 5
      client/src/components/Home/Header/Header.tsx
  9. 32 12
      client/src/components/Home/Header/functions/getDateTime.ts
  10. 1 1
      client/src/components/Home/Home.tsx
  11. 1 1
      client/src/components/Settings/DockerSettings/DockerSettings.tsx
  12. 1 1
      client/src/components/Settings/SearchSettings/SearchSettings.tsx
  13. 1 1
      client/src/components/Settings/Settings.tsx
  14. 1 2
      client/src/components/Settings/Themer/ThemePreview.module.css
  15. 1 1
      client/src/components/Settings/Themer/ThemePreview.tsx
  16. 0 0
      client/src/components/Settings/Themer/Themer.module.css
  17. 101 0
      client/src/components/Settings/Themer/Themer.tsx
  18. 0 0
      client/src/components/Settings/Themer/themes.json
  19. 92 78
      client/src/components/Settings/UISettings/UISettings.tsx
  20. 24 3
      client/src/components/Settings/WeatherSettings/WeatherSettings.tsx
  21. 0 29
      client/src/components/Themer/Themer.tsx
  22. 10 16
      client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx
  23. 6 0
      client/src/interfaces/Config.ts
  24. 8 0
      client/src/interfaces/Forms.ts
  25. 4 1
      client/src/interfaces/Weather.ts
  26. 4 12
      client/src/store/action-creators/config.ts
  27. 7 3
      client/src/store/action-creators/theme.ts
  28. 14 0
      client/src/types/ConfigFormData.ts
  29. 1 0
      client/src/types/WeatherData.ts
  30. 2 0
      client/src/types/index.ts
  31. 1 1
      client/src/utility/applyAuth.ts
  32. 4 0
      client/src/utility/templateObjects/configTemplate.ts
  33. 7 0
      client/src/utility/templateObjects/settingsTemplate.ts
  34. 17 0
      client/src/utility/templateObjects/weatherTemplate.ts
  35. 8 13
      controllers/apps/createApp.js
  36. 5 5
      controllers/apps/updateApp.js
  37. 5 5
      controllers/bookmarks/createBookmark.js
  38. 5 5
      controllers/bookmarks/updateBookmark.js
  39. 4 10
      controllers/categories/createCategory.js
  40. 35 0
      db/migrations/03_weather.js
  41. 1 1
      middleware/auth.js
  42. 19 12
      models/Weather.js
  43. 3 0
      utils/getExternalWeather.js
  44. 5 1
      utils/init/initialConfig.json

+ 1 - 1
.env

@@ -1,5 +1,5 @@
 PORT=5005
 NODE_ENV=development
-VERSION=2.0.0
+VERSION=2.0.1
 PASSWORD=flame_password
 SECRET=e02eb43d69953658c6d07311d6313f2d4467672cb881f96b29368ba1f3f4da4b

+ 8 - 0
CHANGELOG.md

@@ -1,3 +1,11 @@
+### v2.0.1 (2021-11-19)
+- Added option to display humidity in the weather widget ([#136](https://github.com/pawelmalak/flame/issues/136))
+- Added option to set default theme for all new users ([#165](https://github.com/pawelmalak/flame/issues/165))
+- Added option to hide header greetings and date separately ([#200](https://github.com/pawelmalak/flame/issues/200))
+- Fixed bug with broken basic auth ([#202](https://github.com/pawelmalak/flame/issues/202))
+- Fixed bug with parsing visibility value for apps and bookmarks when custom icon was used ([#203](https://github.com/pawelmalak/flame/issues/203))
+- Fixed bug with custom icons not working with apps when "pin by default" was disabled
+
 ### v2.0.0 (2021-11-15)
 - Added authentication system:
   - Only logged in user can access settings ([#33](https://github.com/pawelmalak/flame/issues/33))

+ 1 - 1
README.md

@@ -11,7 +11,7 @@ Flame is self-hosted startpage for your server. Its design is inspired (heavily)
 - 📌 Pin your favourite items to the homescreen for quick and easy access
 - 🔍 Integrated search bar with local filtering, 11 web search providers and ability to add your own
 - 🔑 Authentication system to protect your settings, apps and bookmarks
-- 🔨 Dozens of option to customize Flame interface to your needs, including support for custom CSS and 15 built-in color themes
+- 🔨 Dozens of options to customize Flame interface to your needs, including support for custom CSS and 15 built-in color themes
 - ☀️ Weather widget with current temperature, cloud coverage and animated weather status
 - 🐳 Docker integration to automatically pick and add apps based on their labels
 

+ 1 - 1
client/.env

@@ -1 +1 @@
-REACT_APP_VERSION=2.0.0
+REACT_APP_VERSION=2.0.1

+ 17 - 5
client/src/App.tsx

@@ -1,7 +1,13 @@
+import { useEffect } from 'react';
 import { BrowserRouter, Route, Switch } from 'react-router-dom';
+import 'external-svg-loader';
+
+// Redux
+import { useDispatch, useSelector } from 'react-redux';
+import { bindActionCreators } from 'redux';
 import { autoLogin, getConfig } from './store/action-creators';
 import { actionCreators, store } from './store';
-import 'external-svg-loader';
+import { State } from './store/reducers';
 
 // Utils
 import { checkVersion, decodeToken } from './utility';
@@ -12,9 +18,6 @@ import { Apps } from './components/Apps/Apps';
 import { Settings } from './components/Settings/Settings';
 import { Bookmarks } from './components/Bookmarks/Bookmarks';
 import { NotificationCenter } from './components/NotificationCenter/NotificationCenter';
-import { useDispatch } from 'react-redux';
-import { bindActionCreators } from 'redux';
-import { useEffect } from 'react';
 
 // Get config
 store.dispatch<any>(getConfig());
@@ -25,6 +28,8 @@ if (localStorage.token) {
 }
 
 export const App = (): JSX.Element => {
+  const { config, loading } = useSelector((state: State) => state.config);
+
   const dispath = useDispatch();
   const { fetchQueries, setTheme, logout, createNotification } =
     bindActionCreators(actionCreators, dispath);
@@ -46,7 +51,7 @@ export const App = (): JSX.Element => {
       }
     }, 1000);
 
-    // set theme
+    // set user theme if present
     if (localStorage.theme) {
       setTheme(localStorage.theme);
     }
@@ -60,6 +65,13 @@ export const App = (): JSX.Element => {
     return () => window.clearInterval(tokenIsValid);
   }, []);
 
+  // If there is no user theme, set the default one
+  useEffect(() => {
+    if (!loading && !localStorage.theme) {
+      setTheme(config.defaultTheme, false);
+    }
+  }, [loading]);
+
   return (
     <>
       <BrowserRouter>

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

@@ -61,7 +61,7 @@ export const AppForm = ({ app, modalHandler }: Props): JSX.Element => {
       }
       data.append('name', formData.name);
       data.append('url', formData.url);
-      data.append('isPublic', `${formData.isPublic}`);
+      data.append('isPublic', `${formData.isPublic ? 1 : 0}`);
 
       return data;
     };

+ 1 - 1
client/src/components/Bookmarks/Form/BookmarksForm.tsx

@@ -77,7 +77,7 @@ export const BookmarksForm = ({
       data.append('name', formData.name);
       data.append('url', formData.url);
       data.append('categoryId', `${formData.categoryId}`);
-      data.append('isPublic', `${formData.isPublic}`);
+      data.append('isPublic', `${formData.isPublic ? 1 : 0}`);
 
       return data;
     };

+ 17 - 5
client/src/components/Home/Header/Header.tsx

@@ -1,6 +1,10 @@
 import { useEffect, useState } from 'react';
 import { Link } from 'react-router-dom';
 
+// Redux
+import { useSelector } from 'react-redux';
+import { State } from '../../../store/reducers';
+
 // CSS
 import classes from './Header.module.css';
 
@@ -12,6 +16,10 @@ import { getDateTime } from './functions/getDateTime';
 import { greeter } from './functions/greeter';
 
 export const Header = (): JSX.Element => {
+  const { hideHeader, hideDate, showTime } = useSelector(
+    (state: State) => state.config.config
+  );
+
   const [dateTime, setDateTime] = useState<string>(getDateTime());
   const [greeting, setGreeting] = useState<string>(greeter());
 
@@ -28,14 +36,18 @@ export const Header = (): JSX.Element => {
 
   return (
     <header className={classes.Header}>
-      <p>{dateTime}</p>
+      {(!hideDate || showTime) && <p>{dateTime}</p>}
+
       <Link to="/settings" className={classes.SettingsLink}>
         Go to Settings
       </Link>
-      <span className={classes.HeaderMain}>
-        <h1>{greeting}</h1>
-        <WeatherWidget />
-      </span>
+
+      {!hideHeader && (
+        <span className={classes.HeaderMain}>
+          <h1>{greeting}</h1>
+          <WeatherWidget />
+        </span>
+      )}
     </header>
   );
 };

+ 32 - 12
client/src/components/Home/Header/functions/getDateTime.ts

@@ -30,22 +30,42 @@ export const getDateTime = (): string => {
 
   const useAmericanDate = localStorage.useAmericanDate === 'true';
   const showTime = localStorage.showTime === 'true';
+  const hideDate = localStorage.hideDate === 'true';
 
+  // Date
+  let dateEl = '';
+
+  if (!hideDate) {
+    if (!useAmericanDate) {
+      dateEl = `${days[now.getDay()]}, ${now.getDate()} ${
+        months[now.getMonth()]
+      } ${now.getFullYear()}`;
+    } else {
+      dateEl = `${days[now.getDay()]}, ${
+        months[now.getMonth()]
+      } ${now.getDate()} ${now.getFullYear()}`;
+    }
+  }
+
+  // Time
   const p = parseTime;
+  let timeEl = '';
 
-  const time = `${p(now.getHours())}:${p(now.getMinutes())}:${p(
-    now.getSeconds()
-  )}`;
+  if (showTime) {
+    const time = `${p(now.getHours())}:${p(now.getMinutes())}:${p(
+      now.getSeconds()
+    )}`;
 
-  const timeEl = showTime ? ` - ${time}` : '';
+    timeEl = time;
+  }
+
+  // Separator
+  let separator = '';
 
-  if (!useAmericanDate) {
-    return `${days[now.getDay()]}, ${now.getDate()} ${
-      months[now.getMonth()]
-    } ${now.getFullYear()}${timeEl}`;
-  } else {
-    return `${days[now.getDay()]}, ${
-      months[now.getMonth()]
-    } ${now.getDate()} ${now.getFullYear()}${timeEl}`;
+  if (!hideDate && showTime) {
+    separator = ' - ';
   }
+
+  // Output
+  return `${dateEl}${separator}${timeEl}`;
 };

+ 1 - 1
client/src/components/Home/Home.tsx

@@ -98,7 +98,7 @@ export const Home = (): JSX.Element => {
         <div></div>
       )}
 
-      {!config.hideHeader ? <Header /> : <div></div>}
+      <Header />
 
       {!config.hideApps ? (
         <Fragment>

+ 1 - 1
client/src/components/Settings/DockerSettings/DockerSettings.tsx

@@ -59,7 +59,7 @@ export const DockerSettings = (): JSX.Element => {
       <SettingsHeadline text="Docker" />
       {/* CUSTOM DOCKER SOCKET HOST */}
       <InputGroup>
-        <label htmlFor="dockerHost">Docker Host</label>
+        <label htmlFor="dockerHost">Docker host</label>
         <input
           type="text"
           id="dockerHost"

+ 1 - 1
client/src/components/Settings/SearchSettings/SearchSettings.tsx

@@ -70,7 +70,7 @@ export const SearchSettings = (): JSX.Element => {
       >
         <SettingsHeadline text="General" />
         <InputGroup>
-          <label htmlFor="defaultSearchProvider">Default Search Provider</label>
+          <label htmlFor="defaultSearchProvider">Default search provider</label>
           <select
             id="defaultSearchProvider"
             name="defaultSearchProvider"

+ 1 - 1
client/src/components/Settings/Settings.tsx

@@ -11,7 +11,7 @@ import { Route as SettingsRoute } from '../../interfaces';
 import classes from './Settings.module.css';
 
 // Components
-import { Themer } from '../Themer/Themer';
+import { Themer } from './Themer/Themer';
 import { WeatherSettings } from './WeatherSettings/WeatherSettings';
 import { UISettings } from './UISettings/UISettings';
 import { AppDetails } from './AppDetails/AppDetails';

+ 1 - 2
client/src/components/Themer/ThemePreview.module.css → client/src/components/Settings/Themer/ThemePreview.module.css

@@ -14,7 +14,6 @@
   text-transform: capitalize;
   margin: 8px 0;
   color: var(--color-primary);
-  /* align-self: flex-start; */
 }
 
 .ColorsPreview {
@@ -32,4 +31,4 @@
     width: 40px;
     height: 40px;
   }
-}
+}

+ 1 - 1
client/src/components/Themer/ThemePreview.tsx → client/src/components/Settings/Themer/ThemePreview.tsx

@@ -1,4 +1,4 @@
-import { Theme } from '../../interfaces/Theme';
+import { Theme } from '../../../interfaces/Theme';
 import classes from './ThemePreview.module.css';
 
 interface Props {

+ 0 - 0
client/src/components/Themer/Themer.module.css → client/src/components/Settings/Themer/Themer.module.css


+ 101 - 0
client/src/components/Settings/Themer/Themer.tsx

@@ -0,0 +1,101 @@
+import { ChangeEvent, FormEvent, Fragment, useEffect, useState } from 'react';
+
+// Redux
+import { useDispatch, useSelector } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import { actionCreators } from '../../../store';
+
+// Typescript
+import { Theme, ThemeSettingsForm } from '../../../interfaces';
+
+// Components
+import { ThemePreview } from './ThemePreview';
+import { Button, InputGroup, SettingsHeadline } from '../../UI';
+
+// Other
+import classes from './Themer.module.css';
+import { themes } from './themes.json';
+import { State } from '../../../store/reducers';
+import { inputHandler, themeSettingsTemplate } from '../../../utility';
+
+export const Themer = (): JSX.Element => {
+  const {
+    auth: { isAuthenticated },
+    config: { loading, config },
+  } = useSelector((state: State) => state);
+
+  const dispatch = useDispatch();
+  const { setTheme, updateConfig } = bindActionCreators(
+    actionCreators,
+    dispatch
+  );
+
+  // Initial state
+  const [formData, setFormData] = useState<ThemeSettingsForm>(
+    themeSettingsTemplate
+  );
+
+  // Get config
+  useEffect(() => {
+    setFormData({
+      ...config,
+    });
+  }, [loading]);
+
+  // Form handler
+  const formSubmitHandler = async (e: FormEvent) => {
+    e.preventDefault();
+
+    // Save settings
+    await updateConfig(formData);
+  };
+
+  // Input handler
+  const inputChangeHandler = (
+    e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
+    options?: { isNumber?: boolean; isBool?: boolean }
+  ) => {
+    inputHandler<ThemeSettingsForm>({
+      e,
+      options,
+      setStateHandler: setFormData,
+      state: formData,
+    });
+  };
+
+  return (
+    <Fragment>
+      <SettingsHeadline text="Set theme" />
+      <div className={classes.ThemerGrid}>
+        {themes.map(
+          (theme: Theme, idx: number): JSX.Element => (
+            <ThemePreview key={idx} theme={theme} applyTheme={setTheme} />
+          )
+        )}
+      </div>
+
+      {isAuthenticated && (
+        <form onSubmit={formSubmitHandler}>
+          <SettingsHeadline text="Other settings" />
+          <InputGroup>
+            <label htmlFor="defaultTheme">Default theme (for new users)</label>
+            <select
+              id="defaultTheme"
+              name="defaultTheme"
+              value={formData.defaultTheme}
+              onChange={(e) => inputChangeHandler(e)}
+            >
+              {themes.map((theme: Theme, idx) => (
+                <option key={idx} value={theme.name}>
+                  {theme.name}
+                </option>
+              ))}
+            </select>
+          </InputGroup>
+
+          <Button>Save changes</Button>
+        </form>
+      )}
+    </Fragment>
+  );
+};

+ 0 - 0
client/src/components/Themer/themes.json → client/src/components/Settings/Themer/themes.json


+ 92 - 78
client/src/components/Settings/UISettings/UISettings.tsx

@@ -66,7 +66,7 @@ export const UISettings = (): JSX.Element => {
 
   return (
     <form onSubmit={(e) => formSubmitHandler(e)}>
-      {/* OTHER OPTIONS */}
+      {/* === OTHER OPTIONS === */}
       <SettingsHeadline text="Miscellaneous" />
       {/* PAGE TITLE */}
       <InputGroup>
@@ -81,6 +81,50 @@ export const UISettings = (): JSX.Element => {
         />
       </InputGroup>
 
+      {/* === HEADER OPTIONS === */}
+      <SettingsHeadline text="Header" />
+      {/* HIDE HEADER */}
+      <InputGroup>
+        <label htmlFor="hideHeader">Hide greetings</label>
+        <select
+          id="hideHeader"
+          name="hideHeader"
+          value={formData.hideHeader ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
+        >
+          <option value={1}>True</option>
+          <option value={0}>False</option>
+        </select>
+      </InputGroup>
+
+      {/* HIDE DATE */}
+      <InputGroup>
+        <label htmlFor="hideDate">Hide date</label>
+        <select
+          id="hideDate"
+          name="hideDate"
+          value={formData.hideDate ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
+        >
+          <option value={1}>True</option>
+          <option value={0}>False</option>
+        </select>
+      </InputGroup>
+
+      {/* HIDE TIME */}
+      <InputGroup>
+        <label htmlFor="showTime">Hide time</label>
+        <select
+          id="showTime"
+          name="showTime"
+          value={formData.showTime ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
+        >
+          <option value={0}>True</option>
+          <option value={1}>False</option>
+        </select>
+      </InputGroup>
+
       {/* DATE FORMAT */}
       <InputGroup>
         <label htmlFor="useAmericanDate">Date formatting</label>
@@ -95,7 +139,52 @@ export const UISettings = (): JSX.Element => {
         </select>
       </InputGroup>
 
-      {/* BEAHVIOR OPTIONS */}
+      {/* CUSTOM GREETINGS */}
+      <InputGroup>
+        <label htmlFor="greetingsSchema">Custom greetings</label>
+        <input
+          type="text"
+          id="greetingsSchema"
+          name="greetingsSchema"
+          placeholder="Good day;Hi;Bye!"
+          value={formData.greetingsSchema}
+          onChange={(e) => inputChangeHandler(e)}
+        />
+        <span>
+          Greetings must be separated with semicolon. Only 4 messages can be
+          used
+        </span>
+      </InputGroup>
+
+      {/* CUSTOM DAYS */}
+      <InputGroup>
+        <label htmlFor="daySchema">Custom weekday names</label>
+        <input
+          type="text"
+          id="daySchema"
+          name="daySchema"
+          placeholder="Sunday;Monday;Tuesday"
+          value={formData.daySchema}
+          onChange={(e) => inputChangeHandler(e)}
+        />
+        <span>Names must be separated with semicolon</span>
+      </InputGroup>
+
+      {/* CUSTOM MONTHS */}
+      <InputGroup>
+        <label htmlFor="monthSchema">Custom month names</label>
+        <input
+          type="text"
+          id="monthSchema"
+          name="monthSchema"
+          placeholder="January;February;March"
+          value={formData.monthSchema}
+          onChange={(e) => inputChangeHandler(e)}
+        />
+        <span>Names must be separated with semicolon</span>
+      </InputGroup>
+
+      {/* === BEAHVIOR OPTIONS === */}
       <SettingsHeadline text="App Behavior" />
       {/* PIN APPS */}
       <InputGroup>
@@ -172,82 +261,7 @@ export const UISettings = (): JSX.Element => {
         </select>
       </InputGroup>
 
-      {/* HEADER OPTIONS */}
-      <SettingsHeadline text="Header" />
-      {/* HIDE HEADER */}
-      <InputGroup>
-        <label htmlFor="hideHeader">Hide greeting and date</label>
-        <select
-          id="hideHeader"
-          name="hideHeader"
-          value={formData.hideHeader ? 1 : 0}
-          onChange={(e) => inputChangeHandler(e, { isBool: true })}
-        >
-          <option value={1}>True</option>
-          <option value={0}>False</option>
-        </select>
-      </InputGroup>
-
-      {/* CUSTOM GREETINGS */}
-      <InputGroup>
-        <label htmlFor="greetingsSchema">Custom greetings</label>
-        <input
-          type="text"
-          id="greetingsSchema"
-          name="greetingsSchema"
-          placeholder="Good day;Hi;Bye!"
-          value={formData.greetingsSchema}
-          onChange={(e) => inputChangeHandler(e)}
-        />
-        <span>
-          Greetings must be separated with semicolon. Only 4 messages can be
-          used
-        </span>
-      </InputGroup>
-
-      {/* CUSTOM DAYS */}
-      <InputGroup>
-        <label htmlFor="daySchema">Custom weekday names</label>
-        <input
-          type="text"
-          id="daySchema"
-          name="daySchema"
-          placeholder="Sunday;Monday;Tuesday"
-          value={formData.daySchema}
-          onChange={(e) => inputChangeHandler(e)}
-        />
-        <span>Names must be separated with semicolon</span>
-      </InputGroup>
-
-      {/* CUSTOM MONTHS */}
-      <InputGroup>
-        <label htmlFor="monthSchema">Custom month names</label>
-        <input
-          type="text"
-          id="monthSchema"
-          name="monthSchema"
-          placeholder="January;February;March"
-          value={formData.monthSchema}
-          onChange={(e) => inputChangeHandler(e)}
-        />
-        <span>Names must be separated with semicolon</span>
-      </InputGroup>
-
-      {/* SHOW TIME */}
-      <InputGroup>
-        <label htmlFor="showTime">Show time</label>
-        <select
-          id="showTime"
-          name="showTime"
-          value={formData.showTime ? 1 : 0}
-          onChange={(e) => inputChangeHandler(e, { isBool: true })}
-        >
-          <option value={1}>True</option>
-          <option value={0}>False</option>
-        </select>
-      </InputGroup>
-
-      {/* MODULES OPTIONS */}
+      {/* === MODULES OPTIONS === */}
       <SettingsHeadline text="Modules" />
       {/* HIDE APPS */}
       <InputGroup>

+ 24 - 3
client/src/components/Settings/WeatherSettings/WeatherSettings.tsx

@@ -11,7 +11,7 @@ import { State } from '../../../store/reducers';
 import { ApiResponse, Weather, WeatherForm } from '../../../interfaces';
 
 // UI
-import { InputGroup, Button } from '../../UI';
+import { InputGroup, Button, SettingsHeadline } from '../../UI';
 
 // Utils
 import { inputHandler, weatherSettingsTemplate } from '../../../utility';
@@ -84,6 +84,8 @@ export const WeatherSettings = (): JSX.Element => {
 
   return (
     <form onSubmit={(e) => formSubmitHandler(e)}>
+      <SettingsHeadline text="API" />
+      {/* API KEY */}
       <InputGroup>
         <label htmlFor="WEATHER_API_KEY">API key</label>
         <input
@@ -104,8 +106,10 @@ export const WeatherSettings = (): JSX.Element => {
         </span>
       </InputGroup>
 
+      <SettingsHeadline text="Location" />
+      {/* LAT */}
       <InputGroup>
-        <label htmlFor="lat">Location latitude</label>
+        <label htmlFor="lat">Latitude</label>
         <input
           type="number"
           id="lat"
@@ -128,8 +132,9 @@ export const WeatherSettings = (): JSX.Element => {
         </span>
       </InputGroup>
 
+      {/* LONG */}
       <InputGroup>
-        <label htmlFor="long">Location longitude</label>
+        <label htmlFor="long">Longitude</label>
         <input
           type="number"
           id="long"
@@ -142,6 +147,8 @@ export const WeatherSettings = (): JSX.Element => {
         />
       </InputGroup>
 
+      <SettingsHeadline text="Other" />
+      {/* TEMPERATURE */}
       <InputGroup>
         <label htmlFor="isCelsius">Temperature unit</label>
         <select
@@ -155,6 +162,20 @@ export const WeatherSettings = (): JSX.Element => {
         </select>
       </InputGroup>
 
+      {/* WEATHER DATA */}
+      <InputGroup>
+        <label htmlFor="weatherData">Additional weather data</label>
+        <select
+          id="weatherData"
+          name="weatherData"
+          value={formData.weatherData}
+          onChange={(e) => inputChangeHandler(e)}
+        >
+          <option value="cloud">Cloud coverage</option>
+          <option value="humidity">Humidity</option>
+        </select>
+      </InputGroup>
+
       <Button>Save changes</Button>
     </form>
   );

+ 0 - 29
client/src/components/Themer/Themer.tsx

@@ -1,29 +0,0 @@
-import { Fragment } from 'react';
-import { useDispatch } from 'react-redux';
-import { bindActionCreators } from 'redux';
-import { actionCreators } from '../../store';
-
-import classes from './Themer.module.css';
-
-import { themes } from './themes.json';
-import { Theme } from '../../interfaces/Theme';
-import { ThemePreview } from './ThemePreview';
-
-export const Themer = (): JSX.Element => {
-  const dispatch = useDispatch();
-  const { setTheme } = bindActionCreators(actionCreators, dispatch);
-
-  return (
-    <Fragment>
-      <div>
-        <div className={classes.ThemerGrid}>
-          {themes.map(
-            (theme: Theme, idx: number): JSX.Element => (
-              <ThemePreview key={idx} theme={theme} applyTheme={setTheme} />
-            )
-          )}
-        </div>
-      </div>
-    </Fragment>
-  );
-};

+ 10 - 16
client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx

@@ -13,22 +13,14 @@ import classes from './WeatherWidget.module.css';
 // UI
 import { WeatherIcon } from '../../UI';
 import { State } from '../../../store/reducers';
+import { weatherTemplate } from '../../../utility/templateObjects/weatherTemplate';
 
 export const WeatherWidget = (): JSX.Element => {
-  const { loading, config } = useSelector((state: State) => state.config);
+  const { loading: configLoading, config } = useSelector(
+    (state: State) => state.config
+  );
 
-  const [weather, setWeather] = useState<Weather>({
-    externalLastUpdate: '',
-    tempC: 0,
-    tempF: 0,
-    isDay: 1,
-    cloud: 0,
-    conditionText: '',
-    conditionCode: 1000,
-    id: -1,
-    createdAt: new Date(),
-    updatedAt: new Date(),
-  });
+  const [weather, setWeather] = useState<Weather>(weatherTemplate);
   const [isLoading, setIsLoading] = useState(true);
 
   // Initial request to get data
@@ -65,8 +57,7 @@ export const WeatherWidget = (): JSX.Element => {
 
   return (
     <div className={classes.WeatherWidget}>
-      {isLoading ||
-        loading ||
+      {configLoading ||
         (config.WEATHER_API_KEY && weather.id > 0 && (
           <Fragment>
             <div className={classes.WeatherIcon}>
@@ -76,12 +67,15 @@ export const WeatherWidget = (): JSX.Element => {
               />
             </div>
             <div className={classes.WeatherDetails}>
+              {/* TEMPERATURE */}
               {config.isCelsius ? (
                 <span>{weather.tempC}°C</span>
               ) : (
                 <span>{weather.tempF}°F</span>
               )}
-              <span>{weather.cloud}%</span>
+
+              {/* ADDITIONAL DATA */}
+              <span>{weather[config.weatherData]}%</span>
             </div>
           </Fragment>
         ))}

+ 6 - 0
client/src/interfaces/Config.ts

@@ -1,3 +1,5 @@
+import { WeatherData } from '../types';
+
 export interface Config {
   WEATHER_API_KEY: string;
   lat: number;
@@ -25,4 +27,8 @@ export interface Config {
   daySchema: string;
   monthSchema: string;
   showTime: boolean;
+  defaultTheme: string;
+  isKilometer: boolean;
+  weatherData: WeatherData;
+  hideDate: boolean;
 }

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

@@ -1,8 +1,11 @@
+import { WeatherData } from '../types';
+
 export interface WeatherForm {
   WEATHER_API_KEY: string;
   lat: number;
   long: number;
   isCelsius: boolean;
+  weatherData: WeatherData;
 }
 
 export interface SearchForm {
@@ -27,6 +30,7 @@ export interface OtherSettingsForm {
   daySchema: string;
   monthSchema: string;
   showTime: boolean;
+  hideDate: boolean;
 }
 
 export interface DockerSettingsForm {
@@ -35,3 +39,7 @@ export interface DockerSettingsForm {
   kubernetesApps: boolean;
   unpinStoppedApps: boolean;
 }
+
+export interface ThemeSettingsForm {
+  defaultTheme: string;
+}

+ 4 - 1
client/src/interfaces/Weather.ts

@@ -8,4 +8,7 @@ export interface Weather extends Model {
   cloud: number;
   conditionText: string;
   conditionCode: number;
-}
+  humidity: number;
+  windK: number;
+  windM: number;
+}

+ 4 - 12
client/src/store/action-creators/config.ts

@@ -8,17 +8,10 @@ import {
   UpdateQueryAction,
 } from '../actions/config';
 import axios from 'axios';
-import {
-  ApiResponse,
-  Config,
-  DockerSettingsForm,
-  OtherSettingsForm,
-  Query,
-  SearchForm,
-  WeatherForm,
-} from '../../interfaces';
+import { ApiResponse, Config, Query } from '../../interfaces';
 import { ActionType } from '../action-types';
 import { storeUIConfig, applyAuth } from '../../utility';
+import { ConfigFormData } from '../../types';
 
 const keys: (keyof Config)[] = [
   'useAmericanDate',
@@ -26,6 +19,7 @@ const keys: (keyof Config)[] = [
   'daySchema',
   'monthSchema',
   'showTime',
+  'hideDate',
 ];
 
 export const getConfig = () => async (dispatch: Dispatch<GetConfigAction>) => {
@@ -50,9 +44,7 @@ export const getConfig = () => async (dispatch: Dispatch<GetConfigAction>) => {
 };
 
 export const updateConfig =
-  (
-    formData: WeatherForm | OtherSettingsForm | SearchForm | DockerSettingsForm
-  ) =>
+  (formData: ConfigFormData) =>
   async (dispatch: Dispatch<UpdateConfigAction>) => {
     try {
       const res = await axios.put<ApiResponse<Config>>(

+ 7 - 3
client/src/store/action-creators/theme.ts

@@ -2,14 +2,18 @@ import { Dispatch } from 'redux';
 import { SetThemeAction } from '../actions/theme';
 import { ActionType } from '../action-types';
 import { Theme } from '../../interfaces/Theme';
-import { themes } from '../../components/Themer/themes.json';
+import { themes } from '../../components/Settings/Themer/themes.json';
 
 export const setTheme =
-  (name: string) => (dispatch: Dispatch<SetThemeAction>) => {
+  (name: string, remeberTheme: boolean = true) =>
+  (dispatch: Dispatch<SetThemeAction>) => {
     const theme = themes.find((theme) => theme.name === name);
 
     if (theme) {
-      localStorage.setItem('theme', name);
+      if (remeberTheme) {
+        localStorage.setItem('theme', name);
+      }
+
       loadTheme(theme);
 
       dispatch({

+ 14 - 0
client/src/types/ConfigFormData.ts

@@ -0,0 +1,14 @@
+import {
+  DockerSettingsForm,
+  OtherSettingsForm,
+  SearchForm,
+  ThemeSettingsForm,
+  WeatherForm,
+} from '../interfaces';
+
+export type ConfigFormData =
+  | WeatherForm
+  | SearchForm
+  | DockerSettingsForm
+  | OtherSettingsForm
+  | ThemeSettingsForm;

+ 1 - 0
client/src/types/WeatherData.ts

@@ -0,0 +1 @@
+export type WeatherData = 'cloud' | 'humidity';

+ 2 - 0
client/src/types/index.ts

@@ -0,0 +1,2 @@
+export * from './ConfigFormData';
+export * from './WeatherData';

+ 1 - 1
client/src/utility/applyAuth.ts

@@ -1,4 +1,4 @@
 export const applyAuth = () => {
   const token = localStorage.getItem('token') || '';
-  return { Authorization: `Bearer ${token}` };
+  return { 'Authorization-Flame': `Bearer ${token}` };
 };

+ 4 - 0
client/src/utility/templateObjects/configTemplate.ts

@@ -28,4 +28,8 @@ export const configTemplate: Config = {
   monthSchema:
     'January;February;March;April;May;June;July;August;September;October;November;December',
   showTime: false,
+  defaultTheme: 'tron',
+  isKilometer: true,
+  weatherData: 'cloud',
+  hideDate: false,
 };

+ 7 - 0
client/src/utility/templateObjects/settingsTemplate.ts

@@ -2,6 +2,7 @@ import {
   DockerSettingsForm,
   OtherSettingsForm,
   SearchForm,
+  ThemeSettingsForm,
   WeatherForm,
 } from '../../interfaces';
 
@@ -21,6 +22,7 @@ export const otherSettingsTemplate: OtherSettingsForm = {
   monthSchema:
     'January;February;March;April;May;June;July;August;September;October;November;December',
   showTime: false,
+  hideDate: false,
 };
 
 export const weatherSettingsTemplate: WeatherForm = {
@@ -28,6 +30,7 @@ export const weatherSettingsTemplate: WeatherForm = {
   lat: 0,
   long: 0,
   isCelsius: true,
+  weatherData: 'cloud',
 };
 
 export const searchSettingsTemplate: SearchForm = {
@@ -43,3 +46,7 @@ export const dockerSettingsTemplate: DockerSettingsForm = {
   kubernetesApps: true,
   unpinStoppedApps: true,
 };
+
+export const themeSettingsTemplate: ThemeSettingsForm = {
+  defaultTheme: 'tron',
+};

+ 17 - 0
client/src/utility/templateObjects/weatherTemplate.ts

@@ -0,0 +1,17 @@
+import { Weather } from '../../interfaces';
+
+export const weatherTemplate: Weather = {
+  externalLastUpdate: '',
+  tempC: 0,
+  tempF: 0,
+  isDay: 1,
+  cloud: 0,
+  conditionText: '',
+  conditionCode: 1000,
+  id: -1,
+  createdAt: new Date(),
+  updatedAt: new Date(),
+  humidity: 0,
+  windK: 0,
+  windM: 0,
+};

+ 8 - 13
controllers/apps/createApp.js

@@ -8,25 +8,20 @@ const loadConfig = require('../../utils/loadConfig');
 const createApp = asyncWrapper(async (req, res, next) => {
   const { pinAppsByDefault } = await loadConfig();
 
-  let app;
-  let _body = { ...req.body };
+  let body = { ...req.body };
 
-  if (_body.icon) {
-    _body.icon = _body.icon.trim();
+  if (body.icon) {
+    body.icon = body.icon.trim();
   }
 
   if (req.file) {
-    _body.icon = req.file.filename;
+    body.icon = req.file.filename;
   }
 
-  if (pinAppsByDefault) {
-    app = await App.create({
-      ..._body,
-      isPinned: true,
-    });
-  } else {
-    app = await App.create(req.body);
-  }
+  const app = await App.create({
+    ...body,
+    isPinned: pinAppsByDefault,
+  });
 
   res.status(201).json({
     success: true,

+ 5 - 5
controllers/apps/updateApp.js

@@ -18,17 +18,17 @@ const updateApp = asyncWrapper(async (req, res, next) => {
     );
   }
 
-  let _body = { ...req.body };
+  let body = { ...req.body };
 
-  if (_body.icon) {
-    _body.icon = _body.icon.trim();
+  if (body.icon) {
+    body.icon = body.icon.trim();
   }
 
   if (req.file) {
-    _body.icon = req.file.filename;
+    body.icon = req.file.filename;
   }
 
-  app = await app.update(_body);
+  app = await app.update(body);
 
   res.status(200).json({
     success: true,

+ 5 - 5
controllers/bookmarks/createBookmark.js

@@ -7,20 +7,20 @@ const Bookmark = require('../../models/Bookmark');
 const createBookmark = asyncWrapper(async (req, res, next) => {
   let bookmark;
 
-  let _body = {
+  let body = {
     ...req.body,
     categoryId: parseInt(req.body.categoryId),
   };
 
-  if (_body.icon) {
-    _body.icon = _body.icon.trim();
+  if (body.icon) {
+    body.icon = body.icon.trim();
   }
 
   if (req.file) {
-    _body.icon = req.file.filename;
+    body.icon = req.file.filename;
   }
 
-  bookmark = await Bookmark.create(_body);
+  bookmark = await Bookmark.create(body);
 
   res.status(201).json({
     success: true,

+ 5 - 5
controllers/bookmarks/updateBookmark.js

@@ -19,20 +19,20 @@ const updateBookmark = asyncWrapper(async (req, res, next) => {
     );
   }
 
-  let _body = {
+  let body = {
     ...req.body,
     categoryId: parseInt(req.body.categoryId),
   };
 
-  if (_body.icon) {
-    _body.icon = _body.icon.trim();
+  if (body.icon) {
+    body.icon = body.icon.trim();
   }
 
   if (req.file) {
-    _body.icon = req.file.filename;
+    body.icon = req.file.filename;
   }
 
-  bookmark = await bookmark.update(_body);
+  bookmark = await bookmark.update(body);
 
   res.status(200).json({
     success: true,

+ 4 - 10
controllers/categories/createCategory.js

@@ -8,16 +8,10 @@ const loadConfig = require('../../utils/loadConfig');
 const createCategory = asyncWrapper(async (req, res, next) => {
   const { pinCategoriesByDefault: pinCategories } = await loadConfig();
 
-  let category;
-
-  if (pinCategories) {
-    category = await Category.create({
-      ...req.body,
-      isPinned: true,
-    });
-  } else {
-    category = await Category.create(req.body);
-  }
+  const category = await Category.create({
+    ...req.body,
+    isPinned: pinCategories,
+  });
 
   res.status(201).json({
     success: true,

+ 35 - 0
db/migrations/03_weather.js

@@ -0,0 +1,35 @@
+const { DataTypes } = require('sequelize');
+const { INTEGER, FLOAT } = DataTypes;
+const loadConfig = require('../../utils/loadConfig');
+const getExternalWeather = require('../../utils/getExternalWeather');
+
+const up = async (query) => {
+  await query.addColumn('weather', 'humidity', {
+    type: INTEGER,
+  });
+
+  await query.addColumn('weather', 'windK', {
+    type: FLOAT,
+  });
+
+  await query.addColumn('weather', 'windM', {
+    type: FLOAT,
+  });
+
+  const { WEATHER_API_KEY: secret } = await loadConfig();
+
+  if (secret) {
+    await getExternalWeather();
+  }
+};
+
+const down = async (query) => {
+  await query.removeColumn('weather', 'humidity');
+  await query.removeColumn('weather', 'windK');
+  await query.removeColumn('weather', 'windM');
+};
+
+module.exports = {
+  up,
+  down,
+};

+ 1 - 1
middleware/auth.js

@@ -1,7 +1,7 @@
 const jwt = require('jsonwebtoken');
 
 const auth = (req, res, next) => {
-  const authHeader = req.header('Authorization');
+  const authHeader = req.header('Authorization-Flame');
   let token;
   let tokenIsValid = false;
 

+ 19 - 12
models/Weather.js

@@ -1,16 +1,23 @@
 const { DataTypes } = require('sequelize');
 const { sequelize } = require('../db');
 
-const Weather = sequelize.define('Weather', {
-  externalLastUpdate: DataTypes.STRING,
-  tempC: DataTypes.FLOAT,
-  tempF: DataTypes.FLOAT,
-  isDay: DataTypes.INTEGER,
-  cloud: DataTypes.INTEGER,
-  conditionText: DataTypes.TEXT,
-  conditionCode: DataTypes.INTEGER
-}, {
-  tableName: 'weather'
-});
+const Weather = sequelize.define(
+  'Weather',
+  {
+    externalLastUpdate: DataTypes.STRING,
+    tempC: DataTypes.FLOAT,
+    tempF: DataTypes.FLOAT,
+    isDay: DataTypes.INTEGER,
+    cloud: DataTypes.INTEGER,
+    conditionText: DataTypes.TEXT,
+    conditionCode: DataTypes.INTEGER,
+    humidity: DataTypes.INTEGER,
+    windK: DataTypes.FLOAT,
+    windM: DataTypes.FLOAT,
+  },
+  {
+    tableName: 'weather',
+  }
+);
 
-module.exports = Weather;
+module.exports = Weather;

+ 3 - 0
utils/getExternalWeather.js

@@ -21,6 +21,9 @@ const getExternalWeather = async () => {
       cloud: cursor.cloud,
       conditionText: cursor.condition.text,
       conditionCode: cursor.condition.code,
+      humidity: cursor.humidity,
+      windK: cursor.wind_kph,
+      windM: cursor.wind_mph,
     });
     return weatherData;
   } catch (err) {

+ 5 - 1
utils/init/initialConfig.json

@@ -24,5 +24,9 @@
   "greetingsSchema": "Good evening!;Good afternoon!;Good morning!;Good night!",
   "daySchema": "Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday",
   "monthSchema": "January;February;March;April;May;June;July;August;September;October;November;December",
-  "showTime": false
+  "showTime": false,
+  "defaultTheme": "tron",
+  "isKilometer": true,
+  "weatherData": "cloud",
+  "hideDate": false
 }