Pārlūkot izejas kodu

Merge pull request #21 from pawelmalak/feature

Release v1.2
pawelmalak 4 gadi atpakaļ
vecāks
revīzija
91ab1c5ae4

+ 2 - 2
Dockerfile

@@ -2,9 +2,9 @@ FROM node:14-alpine
 
 WORKDIR /app
 
-COPY package*.json .
+COPY package*.json ./
 
-RUN npm install --only=production
+RUN npm install --production
 
 COPY . .
 

+ 2 - 2
Socket.js

@@ -5,11 +5,11 @@ class Socket {
     this.webSocketServer = new WebSocket.Server({ server })
 
     this.webSocketServer.on('listening', () => {
-      console.log('socket listen');
+      console.log('Socket: listen');
     })
 
     this.webSocketServer.on('connection', (webSocketClient) => {
-      console.log('new connection');
+      console.log('Socket: new connection');
     })
   }
 

+ 30 - 100
client/package-lock.json

@@ -2304,6 +2304,14 @@
       "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz",
       "integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA=="
     },
+    "@types/http-proxy": {
+      "version": "1.17.6",
+      "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.6.tgz",
+      "integrity": "sha512-+qsjqR75S/ib0ig0R9WN+CDoZeOBU6F2XLewgC4KVgdXiNHiKKHFEMRHOrs5PbYE97D5vataw5wPj4KLYfUkuQ==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
     "@types/istanbul-lib-coverage": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@@ -7449,110 +7457,21 @@
       }
     },
     "http-proxy-middleware": {
-      "version": "0.19.1",
-      "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
-      "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.0.tgz",
+      "integrity": "sha512-S+RN5njuyvYV760aiVKnyuTXqUMcSIvYOsHA891DOVQyrdZOwaXtBHpt9FUVPEDAsOvsPArZp6VXQLs44yvkow==",
       "requires": {
-        "http-proxy": "^1.17.0",
-        "is-glob": "^4.0.0",
-        "lodash": "^4.17.11",
-        "micromatch": "^3.1.10"
+        "@types/http-proxy": "^1.17.5",
+        "http-proxy": "^1.18.1",
+        "is-glob": "^4.0.1",
+        "is-plain-obj": "^3.0.0",
+        "micromatch": "^4.0.2"
       },
       "dependencies": {
-        "braces": {
-          "version": "2.3.2",
-          "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
-          "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
-          "requires": {
-            "arr-flatten": "^1.1.0",
-            "array-unique": "^0.3.2",
-            "extend-shallow": "^2.0.1",
-            "fill-range": "^4.0.0",
-            "isobject": "^3.0.1",
-            "repeat-element": "^1.1.2",
-            "snapdragon": "^0.8.1",
-            "snapdragon-node": "^2.0.1",
-            "split-string": "^3.0.2",
-            "to-regex": "^3.0.1"
-          },
-          "dependencies": {
-            "extend-shallow": {
-              "version": "2.0.1",
-              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-              "requires": {
-                "is-extendable": "^0.1.0"
-              }
-            }
-          }
-        },
-        "fill-range": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
-          "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
-          "requires": {
-            "extend-shallow": "^2.0.1",
-            "is-number": "^3.0.0",
-            "repeat-string": "^1.6.1",
-            "to-regex-range": "^2.1.0"
-          },
-          "dependencies": {
-            "extend-shallow": {
-              "version": "2.0.1",
-              "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
-              "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
-              "requires": {
-                "is-extendable": "^0.1.0"
-              }
-            }
-          }
-        },
-        "is-number": {
+        "is-plain-obj": {
           "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
-          "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
-          "requires": {
-            "kind-of": "^3.0.2"
-          },
-          "dependencies": {
-            "kind-of": {
-              "version": "3.2.2",
-              "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
-              "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
-              "requires": {
-                "is-buffer": "^1.1.5"
-              }
-            }
-          }
-        },
-        "micromatch": {
-          "version": "3.1.10",
-          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
-          "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
-          "requires": {
-            "arr-diff": "^4.0.0",
-            "array-unique": "^0.3.2",
-            "braces": "^2.3.1",
-            "define-property": "^2.0.2",
-            "extend-shallow": "^3.0.2",
-            "extglob": "^2.0.4",
-            "fragment-cache": "^0.2.1",
-            "kind-of": "^6.0.2",
-            "nanomatch": "^1.2.9",
-            "object.pick": "^1.3.0",
-            "regex-not": "^1.0.0",
-            "snapdragon": "^0.8.1",
-            "to-regex": "^3.0.2"
-          }
-        },
-        "to-regex-range": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
-          "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
-          "requires": {
-            "is-number": "^3.0.0",
-            "repeat-string": "^1.6.1"
-          }
+          "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+          "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA=="
         }
       }
     },
@@ -16033,6 +15952,17 @@
             }
           }
         },
+        "http-proxy-middleware": {
+          "version": "0.19.1",
+          "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
+          "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
+          "requires": {
+            "http-proxy": "^1.17.0",
+            "is-glob": "^4.0.0",
+            "lodash": "^4.17.11",
+            "micromatch": "^3.1.10"
+          }
+        },
         "import-local": {
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz",

+ 2 - 2
client/package.json

@@ -15,6 +15,7 @@
     "@types/react-redux": "^7.1.16",
     "@types/react-router-dom": "^5.1.7",
     "axios": "^0.21.1",
+    "http-proxy-middleware": "^2.0.0",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     "react-redux": "^7.2.4",
@@ -50,6 +51,5 @@
       "last 1 firefox version",
       "last 1 safari version"
     ]
-  },
-  "proxy": "http://localhost:5005"
+  }
 }

+ 12 - 0
client/src/components/Apps/AppCard/AppCard.module.css

@@ -27,4 +27,16 @@
   font-weight: 400;
   font-size: 0.8em;
   opacity: 1;
+}
+
+@media (min-width: 500px) {
+  .AppCard {
+    padding: 2px;
+    border-radius: 4px;
+    transition: all 0.10s;
+  }
+
+  .AppCard:hover {
+    background-color: rgba(0,0,0,0.2);
+  }
 }

+ 2 - 11
client/src/components/Apps/AppCard/AppCard.tsx

@@ -2,6 +2,7 @@ import { Link } from 'react-router-dom';
 
 import classes from './AppCard.module.css';
 import Icon from '../../UI/Icons/Icon/Icon';
+import { iconParser } from '../../../utility/iconParser';
 
 import { App } from '../../../interfaces';
 
@@ -11,22 +12,12 @@ interface ComponentProps {
 }
 
 const AppCard = (props: ComponentProps): JSX.Element => {
-  const iconParser = (mdiName: string): string => {
-    let parsedName = mdiName
-      .split('-')
-      .map((word: string) => `${word[0].toUpperCase()}${word.slice(1)}`)
-      .join('');
-    parsedName = `mdi${parsedName}`;
-
-    return parsedName;
-  }
-
   const redirectHandler = (url: string): void => {
     window.open(url);
   }
 
   return (
-    <a href={`http://${props.app.url}`} target='blank' className={classes.AppCard}>
+    <a href={`http://${props.app.url}`} target='_blank' className={classes.AppCard}>
       <div className={classes.AppCardIcon}>
         <Icon icon={iconParser(props.app.icon)} />
       </div>

+ 9 - 0
client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css

@@ -18,9 +18,18 @@
 .Bookmarks a {
   line-height: 2;
   transition: all 0.25s;
+  display: flex;
 }
 
 .BookmarkCard a:hover {
   text-decoration: underline;
   padding-left: 10px;
+}
+
+.BookmarkIcon {
+  width: 20px;
+  height: 20px;
+  display: flex;
+  margin-top: 3px;
+  margin-right: 2px;
 }

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

@@ -1,6 +1,9 @@
 import { Bookmark, Category } from '../../../interfaces';
 import classes from './BookmarkCard.module.css';
 
+import Icon from '../../UI/Icons/Icon/Icon';
+import { iconParser } from '../../../utility/iconParser';
+
 interface ComponentProps {
   category: Category;
 }
@@ -13,8 +16,13 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
         {props.category.bookmarks.map((bookmark: Bookmark) => (
           <a
             href={`http://${bookmark.url}`}
-            target='blank'
+            target='_blank'
             key={`bookmark-${bookmark.id}`}>
+            {bookmark.icon && (
+              <div className={classes.BookmarkIcon}>
+                <Icon icon={iconParser(bookmark.icon)} />
+              </div>
+            )}
             {bookmark.name}
           </a>
         ))}

+ 31 - 5
client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx

@@ -29,9 +29,11 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
   const [formData, setFormData] = useState<NewBookmark>({
     name: '',
     url: '',
-    categoryId: -1
+    categoryId: -1,
+    icon: ''
   })
 
+  // Load category data if provided for editing
   useEffect(() => {
     if (props.category) {
       setCategoryName({ name: props.category.name });
@@ -40,18 +42,21 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
     }
   }, [props.category])
 
+  // Load bookmark data if provided for editing
   useEffect(() => {
     if (props.bookmark) {
       setFormData({
         name: props.bookmark.name,
         url: props.bookmark.url,
-        categoryId: props.bookmark.categoryId
+        categoryId: props.bookmark.categoryId,
+        icon: props.bookmark.icon
       })
     } else {
       setFormData({
         name: '',
         url: '',
-        categoryId: -1
+        categoryId: -1,
+        icon: ''
       })
     }
   }, [props.bookmark])
@@ -79,7 +84,8 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
         setFormData({
           name: '',
           url: '',
-          categoryId: formData.categoryId
+          categoryId: formData.categoryId,
+          icon: ''
         })
       }
     } else {
@@ -94,7 +100,8 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
         setFormData({
           name: '',
           url: '',
-          categoryId: -1
+          categoryId: -1,
+          icon: ''
         })
       }
 
@@ -201,6 +208,25 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
                 })}
               </select>
             </InputGroup>
+            <InputGroup>
+              <label htmlFor='icon'>Bookmark Icon (optional)</label>
+              <input
+                type='text'
+                name='icon'
+                id='icon'
+                placeholder='book-open-outline'
+                value={formData.icon}
+                onChange={(e) => inputChangeHandler(e)}
+              />
+              <span>
+                Use icon name from MDI. 
+                <a
+                  href='https://materialdesignicons.com/'
+                  target='blank'>
+                  {' '}Click here for reference
+                </a>
+              </span>
+            </InputGroup>
           </Fragment>
         )
       }

+ 2 - 0
client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx

@@ -96,6 +96,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
       <Table headers={[
         'Name',
         'URL',
+        'Icon',
         'Category',
         'Actions'
       ]}>
@@ -104,6 +105,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
             <tr key={bookmark.bookmark.id}>
               <td>{bookmark.bookmark.name}</td>
               <td>{bookmark.bookmark.url}</td>
+              <td>{bookmark.bookmark.icon}</td>
               <td>{bookmark.categoryName}</td>
               <td className={classes.TableActions}>
                 <div

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

@@ -45,6 +45,7 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
     name: '',
     url: '',
     categoryId: -1,
+    icon: '',
     id: -1,
     createdAt: new Date(),
     updatedAt: new Date()

+ 38 - 4
client/src/components/Settings/OtherSettings/OtherSettings.tsx

@@ -9,6 +9,8 @@ import { ApiResponse, Config, NewNotification } from '../../../interfaces';
 
 interface FormState {
   customTitle: string;
+  pinAppsByDefault: number;
+  pinCategoriesByDefault: number;
 }
 
 interface ComponentProps {
@@ -17,12 +19,14 @@ interface ComponentProps {
 
 const OtherSettings = (props: ComponentProps): JSX.Element => {
   const [formData, setFormData] = useState<FormState>({
-    customTitle: document.title
+    customTitle: document.title,
+    pinAppsByDefault: 0,
+    pinCategoriesByDefault: 0
   })
 
   // get initial config
   useEffect(() => {
-    axios.get<ApiResponse<Config[]>>('/api/config?keys=customTitle')
+    axios.get<ApiResponse<Config[]>>('/api/config?keys=customTitle,pinAppsByDefault,pinCategoriesByDefault')
       .then(data => {
         let tmpFormData = { ...formData };
 
@@ -60,10 +64,16 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
     document.title = formData.customTitle;
   }
 
-  const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
+  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]: e.target.value
+      [e.target.name]: value
     })
   }
 
@@ -80,6 +90,30 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
           onChange={(e) => inputChangeHandler(e)}
         />
       </InputGroup>
+      <InputGroup>
+        <label htmlFor='pinAppsByDefault'>Pin new applications by default</label>
+        <select
+          id='pinAppsByDefault'
+          name='pinAppsByDefault'
+          value={formData.pinAppsByDefault}
+          onChange={(e) => inputChangeHandler(e, true)}
+        >
+          <option value={1}>True</option>
+          <option value={0}>False</option>
+        </select>
+      </InputGroup>
+      <InputGroup>
+        <label htmlFor='pinCategoriesByDefault'>Pin new categories by default</label>
+        <select
+          id='pinCategoriesByDefault'
+          name='pinCategoriesByDefault'
+          value={formData.pinCategoriesByDefault}
+          onChange={(e) => inputChangeHandler(e, true)}
+        >
+          <option value={1}>True</option>
+          <option value={0}>False</option>
+        </select>
+      </InputGroup>
     <Button>Save changes</Button>
     </form>
   )

+ 10 - 0
client/src/components/Settings/WeatherSettings/WeatherSettings.tsx

@@ -64,6 +64,15 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
   const formSubmitHandler = (e: FormEvent) => {
     e.preventDefault();
 
+    // Check for api key input
+    if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) {
+      props.createNotification({
+        title: 'Warning',
+        message: 'API Key is missing. Weather Module will NOT work'
+      })
+    }
+
+    // Save settings
     axios.put<ApiResponse<{}>>('/api/config', formData)
       .then(() => {
         props.createNotification({
@@ -111,6 +120,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
             target='blank'>
             {' '}Weather API
           </a>
+          . Key is required for weather module to work.
         </span>
       </InputGroup>
       <InputGroup>

+ 3 - 3
client/src/components/UI/Icons/WeatherIcon/WeatherIcon.tsx

@@ -12,8 +12,8 @@ interface ComponentProps {
 
 const WeatherIcon = (props: ComponentProps): JSX.Element => {
   const icon = props.isDay
-    ? (new IconMapping).mapIcon(props.weatherStatusCode, TimeOfDay.day)
-    : (new IconMapping).mapIcon(props.weatherStatusCode, TimeOfDay.night);
+    ? new IconMapping().mapIcon(props.weatherStatusCode, TimeOfDay.day)
+    : new IconMapping().mapIcon(props.weatherStatusCode, TimeOfDay.night);
 
   useEffect(() => {
     const delay = setTimeout(() => {
@@ -25,7 +25,7 @@ const WeatherIcon = (props: ComponentProps): JSX.Element => {
     return () => {
       clearTimeout(delay);
     }
-  }, [props.weatherStatusCode]);
+  }, [props.weatherStatusCode, icon, props.theme.colors.accent]);
 
   return <canvas id={`weather-icon`} width='50' height='50'></canvas>
 }

+ 5 - 1
client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx

@@ -50,7 +50,11 @@ const WeatherWidget = (): JSX.Element => {
 
   // Open socket for data updates
   useEffect(() => {
-    const webSocketClient = new WebSocket('ws://localhost:5005');
+    const webSocketClient = new WebSocket(`ws://${window.location.host}/socket`);
+
+    webSocketClient.onopen = () => {
+      console.log('Socket: listen')
+    }
 
     webSocketClient.onmessage = (e) => {
       const data = JSON.parse(e.data);

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

@@ -4,10 +4,12 @@ export interface Bookmark extends Model {
   name: string;
   url: string;
   categoryId: number;
+  icon: string;
 }
 
 export interface NewBookmark {
   name: string;
   url: string;
   categoryId: number;
+  icon: string;
 }

+ 15 - 0
client/src/setupProxy.js

@@ -0,0 +1,15 @@
+const { createProxyMiddleware } = require('http-proxy-middleware');
+
+module.exports = function (app) {
+  const apiProxy = createProxyMiddleware('/api', {
+    target: 'http://localhost:5005'
+  })
+
+  const wsProxy = createProxyMiddleware('/socket', {
+    target: 'http://localhost:5005',
+    ws: true
+  })
+
+  app.use(apiProxy);
+  app.use(wsProxy);
+};

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

@@ -23,10 +23,7 @@ export const getApps = () => async (dispatch: Dispatch) => {
       payload: res.data.data
     })
   } catch (err) {
-    dispatch<GetAppsAction<string>>({
-      type: ActionTypes.getAppsError,
-      payload: err.data.data
-    })
+    console.log(err);
   }
 }
 

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

@@ -0,0 +1,9 @@
+export const iconParser = (mdiName: string): string => {
+  let parsedName = mdiName
+    .split('-')
+    .map((word: string) => `${word[0].toUpperCase()}${word.slice(1)}`)
+    .join('');
+  parsedName = `mdi${parsedName}`;
+
+  return parsedName;
+}

+ 18 - 1
controllers/apps.js

@@ -1,12 +1,29 @@
 const asyncWrapper = require('../middleware/asyncWrapper');
 const ErrorResponse = require('../utils/ErrorResponse');
 const App = require('../models/App');
+const Config = require('../models/Config');
 
 // @desc      Create new app
 // @route     POST /api/apps
 // @access    Public
 exports.createApp = asyncWrapper(async (req, res, next) => {
-  const app = await App.create(req.body);
+  // Get config from database
+  const pinApps = await Config.findOne({
+    where: { key: 'pinAppsByDefault' }
+  });
+
+  let app;
+
+  if (pinApps) {
+    if (parseInt(pinApps.value)) {
+      app = await App.create({
+        ...req.body,
+        isPinned: true
+      })
+    } else {
+      app = await App.create(req.body);
+    }
+  }
 
   res.status(201).json({
     success: true,

+ 18 - 1
controllers/category.js

@@ -2,12 +2,29 @@ const asyncWrapper = require('../middleware/asyncWrapper');
 const ErrorResponse = require('../utils/ErrorResponse');
 const Category = require('../models/Category');
 const Bookmark = require('../models/Bookmark');
+const Config = require('../models/Config');
 
 // @desc      Create new category
 // @route     POST /api/categories
 // @access    Public
 exports.createCategory = asyncWrapper(async (req, res, next) => {
-  const category = await Category.create(req.body);
+  // Get config from database
+  const pinCategories = await Config.findOne({
+    where: { key: 'pinCategoriesByDefault' }
+  });
+
+  let category;
+
+  if (pinCategories) {
+    if (parseInt(pinCategories.value)) {
+      category = await Category.create({
+        ...req.body,
+        isPinned: true
+      })
+    } else {
+      category = await Category.create(req.body);
+    }
+  }
 
   res.status(201).json({
     success: true,

+ 2 - 5
db.js

@@ -8,13 +8,10 @@ const sequelize = new Sequelize({
 
 const connectDB = async () => {
   try {
-    await sequelize.authenticate({ logging: false });
+    await sequelize.authenticate();
     console.log('Connected to database');
     
-    await sequelize.sync({
-      // alter: true,
-      logging: false
-    });
+    await sequelize.sync({ alter: true });
     console.log('All models were synced');
   } catch (error) {
     console.error('Unable to connect to the database:', error);

+ 4 - 0
models/Bookmark.js

@@ -13,6 +13,10 @@ const Bookmark = sequelize.define('Bookmark', {
   categoryId: {
     type: DataTypes.INTEGER,
     allowNull: false
+  },
+  icon: {
+    type: DataTypes.STRING,
+    defaultValue: ''
   }
 }, {
   tableName: 'bookmarks'

+ 22 - 0
utils/clearWeatherData.js

@@ -0,0 +1,22 @@
+const { Op } = require('sequelize');
+const Weather = require('../models/Weather');
+
+const clearWeatherData = async () => {
+  const weather = await Weather.findOne({
+    order: [[ 'createdAt', 'DESC' ]]
+  });
+
+  if (weather) {
+    await Weather.destroy({
+      where: {
+        id: {
+          [Op.lt]: weather.id
+        }
+      }
+    })
+  }
+
+  console.log('Old weather data was deleted');
+}
+
+module.exports = clearWeatherData;

+ 5 - 8
utils/initConfig.js

@@ -1,16 +1,13 @@
 const { Op } = require('sequelize');
 const Config = require('../models/Config');
+const { config } = require('./initialConfig.json');
 
 const initConfig = async () => {
-  // Config keys
-  const keys = ['WEATHER_API_KEY', 'lat', 'long', 'isCelsius', 'customTitle'];
-  const values = ['', 0, 0, true, 'Flame'];
-
   // Get config values
   const configPairs = await Config.findAll({
     where: {
       key: {
-        [Op.or]: keys
+        [Op.or]: config.map(pair => pair.key)
       }
     }
   })
@@ -19,12 +16,12 @@ const initConfig = async () => {
   const configKeys = configPairs.map((pair) => pair.key);
 
   // Create missing pairs
-  keys.forEach(async (key, idx) => {
+  config.forEach(async ({ key, value}) => {
     if (!configKeys.includes(key)) {
       await Config.create({
         key,
-        value: values[idx],
-        valueType: typeof values[idx]
+        value,
+        valueType: typeof value
       })
     }
   })

+ 32 - 0
utils/initialConfig.json

@@ -0,0 +1,32 @@
+{
+  "config": [
+    {
+      "key": "WEATHER_API_KEY",
+      "value": ""
+    },
+    {
+      "key": "lat",
+      "value": 0
+    },
+    {
+      "key": "long",
+      "value": 0
+    },
+    {
+      "key": "isCelsius",
+      "value": true
+    },
+    {
+      "key": "customTitle",
+      "value": "Flame"
+    },
+    {
+      "key": "pinAppsByDefault",
+      "value": true
+    },
+    {
+      "key": "pinCategoriesByDefault",
+      "value": true
+    }
+  ]
+}

+ 3 - 2
utils/jobs.js

@@ -1,5 +1,6 @@
 const schedule = require('node-schedule');
 const getExternalWeather = require('./getExternalWeather');
+const clearWeatherData = require('./clearWeatherData');
 const Sockets = require('../Sockets');
 
 // Update weather data every 15 minutes
@@ -14,6 +15,6 @@ const weatherJob = schedule.scheduleJob('updateWeather', '0 */15 * * * *', async
 })
 
 // Clear old weather data every 4 hours
-const weatherCleanerJob = schedule.scheduleJob('clearWeather', '0 0 */4 * * *', async () => {
-  console.log('clean')
+const weatherCleanerJob = schedule.scheduleJob('clearWeather', '0 5 */4 * * *', async () => {
+  clearWeatherData();
 })