浏览代码

Client: Implemented new config system

Paweł Malak 3 年之前
父节点
当前提交
76e50624e7

+ 13 - 6
client/src/components/Apps/AppCard/AppCard.tsx

@@ -2,12 +2,13 @@ import classes from './AppCard.module.css';
 import Icon from '../../UI/Icons/Icon/Icon';
 import Icon from '../../UI/Icons/Icon/Icon';
 import { iconParser, urlParser } from '../../../utility';
 import { iconParser, urlParser } from '../../../utility';
 
 
-import { App } from '../../../interfaces';
-import { searchConfig } from '../../../utility';
+import { App, Config, GlobalState } from '../../../interfaces';
+import { connect } from 'react-redux';
 
 
 interface ComponentProps {
 interface ComponentProps {
   app: App;
   app: App;
   pinHandler?: Function;
   pinHandler?: Function;
+  config: Config;
 }
 }
 
 
 const AppCard = (props: ComponentProps): JSX.Element => {
 const AppCard = (props: ComponentProps): JSX.Element => {
@@ -29,7 +30,7 @@ const AppCard = (props: ComponentProps): JSX.Element => {
       <div className={classes.CustomIcon}>
       <div className={classes.CustomIcon}>
         <svg
         <svg
           data-src={`/uploads/${icon}`}
           data-src={`/uploads/${icon}`}
-          fill='var(--color-primary)'
+          fill="var(--color-primary)"
           className={classes.CustomIcon}
           className={classes.CustomIcon}
         ></svg>
         ></svg>
       </div>
       </div>
@@ -41,8 +42,8 @@ const AppCard = (props: ComponentProps): JSX.Element => {
   return (
   return (
     <a
     <a
       href={redirectUrl}
       href={redirectUrl}
-      target={searchConfig('appsSameTab', false) ? '' : '_blank'}
-      rel='noreferrer'
+      target={props.config.appsSameTab ? '' : '_blank'}
+      rel="noreferrer"
       className={classes.AppCard}
       className={classes.AppCard}
     >
     >
       <div className={classes.AppCardIcon}>{iconEl}</div>
       <div className={classes.AppCardIcon}>{iconEl}</div>
@@ -54,4 +55,10 @@ const AppCard = (props: ComponentProps): JSX.Element => {
   );
   );
 };
 };
 
 
-export default AppCard;
+const mapStateToProps = (state: GlobalState) => {
+  return {
+    config: state.config.config,
+  };
+};
+
+export default connect(mapStateToProps)(AppCard);

+ 98 - 54
client/src/components/Apps/AppTable/AppTable.tsx

@@ -1,13 +1,24 @@
 import { Fragment, KeyboardEvent, useState, useEffect } from 'react';
 import { Fragment, KeyboardEvent, useState, useEffect } from 'react';
-import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
+import {
+  DragDropContext,
+  Droppable,
+  Draggable,
+  DropResult,
+} from 'react-beautiful-dnd';
 import { Link } from 'react-router-dom';
 import { Link } from 'react-router-dom';
 
 
 // Redux
 // Redux
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
-import { pinApp, deleteApp, reorderApps, updateConfig, createNotification } from '../../../store/actions';
+import {
+  pinApp,
+  deleteApp,
+  reorderApps,
+  updateConfig,
+  createNotification,
+} from '../../../store/actions';
 
 
 // Typescript
 // Typescript
-import { App, GlobalState, NewNotification } from '../../../interfaces';
+import { App, Config, GlobalState, NewNotification } from '../../../interfaces';
 
 
 // CSS
 // CSS
 import classes from './AppTable.module.css';
 import classes from './AppTable.module.css';
@@ -16,11 +27,9 @@ import classes from './AppTable.module.css';
 import Icon from '../../UI/Icons/Icon/Icon';
 import Icon from '../../UI/Icons/Icon/Icon';
 import Table from '../../UI/Table/Table';
 import Table from '../../UI/Table/Table';
 
 
-// Utils
-import { searchConfig } from '../../../utility';
-
 interface ComponentProps {
 interface ComponentProps {
   apps: App[];
   apps: App[];
+  config: Config;
   pinApp: (app: App) => void;
   pinApp: (app: App) => void;
   deleteApp: (id: number) => void;
   deleteApp: (id: number) => void;
   updateAppHandler: (app: App) => void;
   updateAppHandler: (app: App) => void;
@@ -36,38 +45,44 @@ const AppTable = (props: ComponentProps): JSX.Element => {
   // Copy apps array
   // Copy apps array
   useEffect(() => {
   useEffect(() => {
     setLocalApps([...props.apps]);
     setLocalApps([...props.apps]);
-  }, [props.apps])
+  }, [props.apps]);
 
 
   // Check ordering
   // Check ordering
   useEffect(() => {
   useEffect(() => {
-    const order = searchConfig('useOrdering', '');
+    const order = props.config.useOrdering;
 
 
     if (order === 'orderId') {
     if (order === 'orderId') {
       setIsCustomOrder(true);
       setIsCustomOrder(true);
     }
     }
-  }, [])
+  }, []);
 
 
   const deleteAppHandler = (app: App): void => {
   const deleteAppHandler = (app: App): void => {
-    const proceed = window.confirm(`Are you sure you want to delete ${app.name} at ${app.url} ?`);
+    const proceed = window.confirm(
+      `Are you sure you want to delete ${app.name} at ${app.url} ?`
+    );
 
 
     if (proceed) {
     if (proceed) {
       props.deleteApp(app.id);
       props.deleteApp(app.id);
     }
     }
-  }
+  };
 
 
   // Support keyboard navigation for actions
   // Support keyboard navigation for actions
-  const keyboardActionHandler = (e: KeyboardEvent, app: App, handler: Function) => {
+  const keyboardActionHandler = (
+    e: KeyboardEvent,
+    app: App,
+    handler: Function
+  ) => {
     if (e.key === 'Enter') {
     if (e.key === 'Enter') {
       handler(app);
       handler(app);
     }
     }
-  }
+  };
 
 
   const dragEndHanlder = (result: DropResult): void => {
   const dragEndHanlder = (result: DropResult): void => {
     if (!isCustomOrder) {
     if (!isCustomOrder) {
       props.createNotification({
       props.createNotification({
         title: 'Error',
         title: 'Error',
-        message: 'Custom order is disabled'
-      })
+        message: 'Custom order is disabled',
+      });
       return;
       return;
     }
     }
 
 
@@ -81,32 +96,39 @@ const AppTable = (props: ComponentProps): JSX.Element => {
 
 
     setLocalApps(tmpApps);
     setLocalApps(tmpApps);
     props.reorderApps(tmpApps);
     props.reorderApps(tmpApps);
-  }
+  };
 
 
   return (
   return (
     <Fragment>
     <Fragment>
       <div className={classes.Message}>
       <div className={classes.Message}>
-        {isCustomOrder
-          ? <p>You can drag and drop single rows to reorder application</p>
-          : <p>Custom order is disabled. You can change it in <Link to='/settings/other'>settings</Link></p>
-        }
+        {isCustomOrder ? (
+          <p>You can drag and drop single rows to reorder application</p>
+        ) : (
+          <p>
+            Custom order is disabled. You can change it in{' '}
+            <Link to="/settings/other">settings</Link>
+          </p>
+        )}
       </div>
       </div>
       <DragDropContext onDragEnd={dragEndHanlder}>
       <DragDropContext onDragEnd={dragEndHanlder}>
-        <Droppable droppableId='apps'>
+        <Droppable droppableId="apps">
           {(provided) => (
           {(provided) => (
-            <Table headers={[
-              'Name',
-              'URL',
-              'Icon',
-              'Actions'
-            ]}
-            innerRef={provided.innerRef}>
+            <Table
+              headers={['Name', 'URL', 'Icon', 'Actions']}
+              innerRef={provided.innerRef}
+            >
               {localApps.map((app: App, index): JSX.Element => {
               {localApps.map((app: App, index): JSX.Element => {
                 return (
                 return (
-                  <Draggable key={app.id} draggableId={app.id.toString()} index={index}>
+                  <Draggable
+                    key={app.id}
+                    draggableId={app.id.toString()}
+                    index={index}
+                  >
                     {(provided, snapshot) => {
                     {(provided, snapshot) => {
                       const style = {
                       const style = {
-                        border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none',
+                        border: snapshot.isDragging
+                          ? '1px solid var(--color-accent)'
+                          : 'none',
                         borderRadius: '4px',
                         borderRadius: '4px',
                         ...provided.draggableProps.style,
                         ...provided.draggableProps.style,
                       };
                       };
@@ -118,63 +140,85 @@ const AppTable = (props: ComponentProps): JSX.Element => {
                           ref={provided.innerRef}
                           ref={provided.innerRef}
                           style={style}
                           style={style}
                         >
                         >
-                          <td style={{ width:'200px' }}>{app.name}</td>
-                          <td style={{ width:'200px' }}>{app.url}</td>
-                          <td style={{ width:'200px' }}>{app.icon}</td>
+                          <td style={{ width: '200px' }}>{app.name}</td>
+                          <td style={{ width: '200px' }}>{app.url}</td>
+                          <td style={{ width: '200px' }}>{app.icon}</td>
                           {!snapshot.isDragging && (
                           {!snapshot.isDragging && (
                             <td className={classes.TableActions}>
                             <td className={classes.TableActions}>
                               <div
                               <div
                                 className={classes.TableAction}
                                 className={classes.TableAction}
                                 onClick={() => deleteAppHandler(app)}
                                 onClick={() => deleteAppHandler(app)}
-                                onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)}
-                                tabIndex={0}>
-                                <Icon icon='mdiDelete' />
+                                onKeyDown={(e) =>
+                                  keyboardActionHandler(
+                                    e,
+                                    app,
+                                    deleteAppHandler
+                                  )
+                                }
+                                tabIndex={0}
+                              >
+                                <Icon icon="mdiDelete" />
                               </div>
                               </div>
                               <div
                               <div
                                 className={classes.TableAction}
                                 className={classes.TableAction}
                                 onClick={() => props.updateAppHandler(app)}
                                 onClick={() => props.updateAppHandler(app)}
-                                onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)}
-                                tabIndex={0}>
-                                <Icon icon='mdiPencil' />
+                                onKeyDown={(e) =>
+                                  keyboardActionHandler(
+                                    e,
+                                    app,
+                                    props.updateAppHandler
+                                  )
+                                }
+                                tabIndex={0}
+                              >
+                                <Icon icon="mdiPencil" />
                               </div>
                               </div>
                               <div
                               <div
                                 className={classes.TableAction}
                                 className={classes.TableAction}
                                 onClick={() => props.pinApp(app)}
                                 onClick={() => props.pinApp(app)}
-                                onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)}
-                                tabIndex={0}>
-                                {app.isPinned
-                                  ? <Icon icon='mdiPinOff' color='var(--color-accent)' />
-                                  : <Icon icon='mdiPin' />
+                                onKeyDown={(e) =>
+                                  keyboardActionHandler(e, app, props.pinApp)
                                 }
                                 }
+                                tabIndex={0}
+                              >
+                                {app.isPinned ? (
+                                  <Icon
+                                    icon="mdiPinOff"
+                                    color="var(--color-accent)"
+                                  />
+                                ) : (
+                                  <Icon icon="mdiPin" />
+                                )}
                               </div>
                               </div>
                             </td>
                             </td>
                           )}
                           )}
                         </tr>
                         </tr>
-                      )
+                      );
                     }}
                     }}
                   </Draggable>
                   </Draggable>
-                )
+                );
               })}
               })}
             </Table>
             </Table>
           )}
           )}
         </Droppable>
         </Droppable>
       </DragDropContext>
       </DragDropContext>
     </Fragment>
     </Fragment>
-  )
-}
+  );
+};
 
 
 const mapStateToProps = (state: GlobalState) => {
 const mapStateToProps = (state: GlobalState) => {
   return {
   return {
-    apps: state.app.apps
-  }
-}
+    apps: state.app.apps,
+    config: state.config.config,
+  };
+};
 
 
 const actions = {
 const actions = {
   pinApp,
   pinApp,
   deleteApp,
   deleteApp,
   reorderApps,
   reorderApps,
   updateConfig,
   updateConfig,
-  createNotification
-}
+  createNotification,
+};
 
 
-export default connect(mapStateToProps, actions)(AppTable);
+export default connect(mapStateToProps, actions)(AppTable);

+ 12 - 4
client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx

@@ -1,12 +1,14 @@
-import { Bookmark, Category } from '../../../interfaces';
+import { Bookmark, Category, Config, GlobalState } 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, urlParser, searchConfig } from '../../../utility';
+import { iconParser, urlParser } from '../../../utility';
 import { Fragment } from 'react';
 import { Fragment } from 'react';
+import { connect } from 'react-redux';
 
 
 interface ComponentProps {
 interface ComponentProps {
   category: Category;
   category: Category;
+  config: Config;
 }
 }
 
 
 const BookmarkCard = (props: ComponentProps): JSX.Element => {
 const BookmarkCard = (props: ComponentProps): JSX.Element => {
@@ -54,7 +56,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
           return (
           return (
             <a
             <a
               href={redirectUrl}
               href={redirectUrl}
-              target={searchConfig('bookmarksSameTab', false) ? '' : '_blank'}
+              target={props.config.bookmarksSameTab ? '' : '_blank'}
               rel="noreferrer"
               rel="noreferrer"
               key={`bookmark-${bookmark.id}`}
               key={`bookmark-${bookmark.id}`}
             >
             >
@@ -68,4 +70,10 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
   );
   );
 };
 };
 
 
-export default BookmarkCard;
+const mapStateToProps = (state: GlobalState) => {
+  return {
+    config: state.config.config,
+  };
+};
+
+export default connect(mapStateToProps)(BookmarkCard);

+ 178 - 120
client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx

@@ -1,13 +1,30 @@
 import { KeyboardEvent, useState, useEffect, Fragment } from 'react';
 import { KeyboardEvent, useState, useEffect, Fragment } from 'react';
-import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
+import {
+  DragDropContext,
+  Droppable,
+  Draggable,
+  DropResult,
+} from 'react-beautiful-dnd';
 import { Link } from 'react-router-dom';
 import { Link } from 'react-router-dom';
 
 
 // Redux
 // Redux
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
-import { pinCategory, deleteCategory, deleteBookmark, createNotification, reorderCategories } from '../../../store/actions';
+import {
+  pinCategory,
+  deleteCategory,
+  deleteBookmark,
+  createNotification,
+  reorderCategories,
+} from '../../../store/actions';
 
 
 // Typescript
 // Typescript
-import { Bookmark, Category, NewNotification } from '../../../interfaces';
+import {
+  Bookmark,
+  Category,
+  Config,
+  GlobalState,
+  NewNotification,
+} from '../../../interfaces';
 import { ContentType } from '../Bookmarks';
 import { ContentType } from '../Bookmarks';
 
 
 // CSS
 // CSS
@@ -17,12 +34,10 @@ import classes from './BookmarkTable.module.css';
 import Table from '../../UI/Table/Table';
 import Table from '../../UI/Table/Table';
 import Icon from '../../UI/Icons/Icon/Icon';
 import Icon from '../../UI/Icons/Icon/Icon';
 
 
-// Utils
-import { searchConfig } from '../../../utility';
-
 interface ComponentProps {
 interface ComponentProps {
   contentType: ContentType;
   contentType: ContentType;
   categories: Category[];
   categories: Category[];
+  config: Config;
   pinCategory: (category: Category) => void;
   pinCategory: (category: Category) => void;
   deleteCategory: (id: number) => void;
   deleteCategory: (id: number) => void;
   updateHandler: (data: Category | Bookmark) => void;
   updateHandler: (data: Category | Bookmark) => void;
@@ -38,45 +53,53 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
   // Copy categories array
   // Copy categories array
   useEffect(() => {
   useEffect(() => {
     setLocalCategories([...props.categories]);
     setLocalCategories([...props.categories]);
-  }, [props.categories])
+  }, [props.categories]);
 
 
   // Check ordering
   // Check ordering
   useEffect(() => {
   useEffect(() => {
-    const order = searchConfig('useOrdering', '');
+    const order = props.config.useOrdering;
 
 
     if (order === 'orderId') {
     if (order === 'orderId') {
       setIsCustomOrder(true);
       setIsCustomOrder(true);
     }
     }
-  })
+  });
 
 
   const deleteCategoryHandler = (category: Category): void => {
   const deleteCategoryHandler = (category: Category): void => {
-    const proceed = window.confirm(`Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`);
+    const proceed = window.confirm(
+      `Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`
+    );
 
 
     if (proceed) {
     if (proceed) {
       props.deleteCategory(category.id);
       props.deleteCategory(category.id);
     }
     }
-  }
+  };
 
 
   const deleteBookmarkHandler = (bookmark: Bookmark): void => {
   const deleteBookmarkHandler = (bookmark: Bookmark): void => {
-    const proceed = window.confirm(`Are you sure you want to delete ${bookmark.name}?`);
+    const proceed = window.confirm(
+      `Are you sure you want to delete ${bookmark.name}?`
+    );
 
 
     if (proceed) {
     if (proceed) {
       props.deleteBookmark(bookmark.id, bookmark.categoryId);
       props.deleteBookmark(bookmark.id, bookmark.categoryId);
     }
     }
-  }
+  };
 
 
-  const keyboardActionHandler = (e: KeyboardEvent, category: Category, handler: Function) => {
+  const keyboardActionHandler = (
+    e: KeyboardEvent,
+    category: Category,
+    handler: Function
+  ) => {
     if (e.key === 'Enter') {
     if (e.key === 'Enter') {
       handler(category);
       handler(category);
     }
     }
-  }
+  };
 
 
   const dragEndHanlder = (result: DropResult): void => {
   const dragEndHanlder = (result: DropResult): void => {
     if (!isCustomOrder) {
     if (!isCustomOrder) {
       props.createNotification({
       props.createNotification({
         title: 'Error',
         title: 'Error',
-        message: 'Custom order is disabled'
-      })
+        message: 'Custom order is disabled',
+      });
       return;
       return;
     }
     }
 
 
@@ -90,136 +113,171 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
 
 
     setLocalCategories(tmpCategories);
     setLocalCategories(tmpCategories);
     props.reorderCategories(tmpCategories);
     props.reorderCategories(tmpCategories);
-  }
+  };
 
 
   if (props.contentType === ContentType.category) {
   if (props.contentType === ContentType.category) {
     return (
     return (
       <Fragment>
       <Fragment>
         <div className={classes.Message}>
         <div className={classes.Message}>
-          {isCustomOrder
-            ? <p>You can drag and drop single rows to reorder categories</p>
-            : <p>Custom order is disabled. You can change it in <Link to='/settings/other'>settings</Link></p>
-          }
+          {isCustomOrder ? (
+            <p>You can drag and drop single rows to reorder categories</p>
+          ) : (
+            <p>
+              Custom order is disabled. You can change it in{' '}
+              <Link to="/settings/other">settings</Link>
+            </p>
+          )}
         </div>
         </div>
         <DragDropContext onDragEnd={dragEndHanlder}>
         <DragDropContext onDragEnd={dragEndHanlder}>
-          <Droppable droppableId='categories'>
+          <Droppable droppableId="categories">
             {(provided) => (
             {(provided) => (
-              <Table headers={[
-                'Name',
-                'Actions'
-              ]}
-              innerRef={provided.innerRef}>
-                {localCategories.map((category: Category, index): JSX.Element => {
-                  return (
-                    <Draggable key={category.id} draggableId={category.id.toString()} index={index}>
-                      {(provided, snapshot) => {
-                        const style = {
-                          border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none',
-                          borderRadius: '4px',
-                          ...provided.draggableProps.style,
-                        };
-
-                        return (
-                          <tr
-                            {...provided.draggableProps}
-                            {...provided.dragHandleProps}
-                            ref={provided.innerRef}
-                            style={style}  
-                          >
-                            <td>{category.name}</td>
-                            {!snapshot.isDragging && (
-                              <td className={classes.TableActions}>
-                                <div
-                                  className={classes.TableAction}
-                                  onClick={() => deleteCategoryHandler(category)}
-                                  onKeyDown={(e) => keyboardActionHandler(e, category, deleteCategoryHandler)}
-                                  tabIndex={0}>
-                                  <Icon icon='mdiDelete' />
-                                </div>
-                                <div
-                                  className={classes.TableAction}
-                                  onClick={() => props.updateHandler(category)}
-                                  tabIndex={0}>
-                                  <Icon icon='mdiPencil' />
-                                </div>
-                                <div
-                                  className={classes.TableAction}
-                                  onClick={() => props.pinCategory(category)}
-                                  onKeyDown={(e) => keyboardActionHandler(e, category, props.pinCategory)}
-                                  tabIndex={0}>
-                                  {category.isPinned
-                                    ? <Icon icon='mdiPinOff' color='var(--color-accent)' />
-                                    : <Icon icon='mdiPin' />
-                                  }
-                                </div>
-                              </td>
-                            )}
-                          </tr>
-                        )
-                      }}
-                    </Draggable>
-                  )
-                })}
+              <Table headers={['Name', 'Actions']} innerRef={provided.innerRef}>
+                {localCategories.map(
+                  (category: Category, index): JSX.Element => {
+                    return (
+                      <Draggable
+                        key={category.id}
+                        draggableId={category.id.toString()}
+                        index={index}
+                      >
+                        {(provided, snapshot) => {
+                          const style = {
+                            border: snapshot.isDragging
+                              ? '1px solid var(--color-accent)'
+                              : 'none',
+                            borderRadius: '4px',
+                            ...provided.draggableProps.style,
+                          };
+
+                          return (
+                            <tr
+                              {...provided.draggableProps}
+                              {...provided.dragHandleProps}
+                              ref={provided.innerRef}
+                              style={style}
+                            >
+                              <td>{category.name}</td>
+                              {!snapshot.isDragging && (
+                                <td className={classes.TableActions}>
+                                  <div
+                                    className={classes.TableAction}
+                                    onClick={() =>
+                                      deleteCategoryHandler(category)
+                                    }
+                                    onKeyDown={(e) =>
+                                      keyboardActionHandler(
+                                        e,
+                                        category,
+                                        deleteCategoryHandler
+                                      )
+                                    }
+                                    tabIndex={0}
+                                  >
+                                    <Icon icon="mdiDelete" />
+                                  </div>
+                                  <div
+                                    className={classes.TableAction}
+                                    onClick={() =>
+                                      props.updateHandler(category)
+                                    }
+                                    tabIndex={0}
+                                  >
+                                    <Icon icon="mdiPencil" />
+                                  </div>
+                                  <div
+                                    className={classes.TableAction}
+                                    onClick={() => props.pinCategory(category)}
+                                    onKeyDown={(e) =>
+                                      keyboardActionHandler(
+                                        e,
+                                        category,
+                                        props.pinCategory
+                                      )
+                                    }
+                                    tabIndex={0}
+                                  >
+                                    {category.isPinned ? (
+                                      <Icon
+                                        icon="mdiPinOff"
+                                        color="var(--color-accent)"
+                                      />
+                                    ) : (
+                                      <Icon icon="mdiPin" />
+                                    )}
+                                  </div>
+                                </td>
+                              )}
+                            </tr>
+                          );
+                        }}
+                      </Draggable>
+                    );
+                  }
+                )}
               </Table>
               </Table>
             )}
             )}
           </Droppable>
           </Droppable>
         </DragDropContext>
         </DragDropContext>
       </Fragment>
       </Fragment>
-    )
+    );
   } else {
   } else {
-    const bookmarks: {bookmark: Bookmark, categoryName: string}[] = [];
+    const bookmarks: { bookmark: Bookmark; categoryName: string }[] = [];
     props.categories.forEach((category: Category) => {
     props.categories.forEach((category: Category) => {
       category.bookmarks.forEach((bookmark: Bookmark) => {
       category.bookmarks.forEach((bookmark: Bookmark) => {
         bookmarks.push({
         bookmarks.push({
           bookmark,
           bookmark,
-          categoryName: category.name
+          categoryName: category.name,
         });
         });
-      })
-    })
+      });
+    });
 
 
     return (
     return (
-      <Table headers={[
-        'Name',
-        'URL',
-        'Icon',
-        'Category',
-        'Actions'
-      ]}>
-        {bookmarks.map((bookmark: {bookmark: Bookmark, categoryName: string}) => {
-          return (
-            <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
-                  className={classes.TableAction}
-                  onClick={() => deleteBookmarkHandler(bookmark.bookmark)}
-                  tabIndex={0}>
-                  <Icon icon='mdiDelete' />
-                </div>
-                <div
-                  className={classes.TableAction}
-                  onClick={() => props.updateHandler(bookmark.bookmark)}
-                  tabIndex={0}>
-                  <Icon icon='mdiPencil' />
-                </div>
-              </td>
-            </tr>
-          )
-        })}
+      <Table headers={['Name', 'URL', 'Icon', 'Category', 'Actions']}>
+        {bookmarks.map(
+          (bookmark: { bookmark: Bookmark; categoryName: string }) => {
+            return (
+              <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
+                    className={classes.TableAction}
+                    onClick={() => deleteBookmarkHandler(bookmark.bookmark)}
+                    tabIndex={0}
+                  >
+                    <Icon icon="mdiDelete" />
+                  </div>
+                  <div
+                    className={classes.TableAction}
+                    onClick={() => props.updateHandler(bookmark.bookmark)}
+                    tabIndex={0}
+                  >
+                    <Icon icon="mdiPencil" />
+                  </div>
+                </td>
+              </tr>
+            );
+          }
+        )}
       </Table>
       </Table>
-    )
+    );
   }
   }
-}
+};
+
+const mapStateToProps = (state: GlobalState) => {
+  return {
+    config: state.config.config,
+  };
+};
 
 
 const actions = {
 const actions = {
   pinCategory,
   pinCategory,
   deleteCategory,
   deleteCategory,
   deleteBookmark,
   deleteBookmark,
   createNotification,
   createNotification,
-  reorderCategories
-}
+  reorderCategories,
+};
 
 
-export default connect(null, actions)(BookmarkTable);
+export default connect(mapStateToProps, actions)(BookmarkTable);

+ 8 - 9
client/src/components/Home/Home.tsx

@@ -7,7 +7,7 @@ import { getApps, getCategories } from '../../store/actions';
 
 
 // Typescript
 // Typescript
 import { GlobalState } from '../../interfaces/GlobalState';
 import { GlobalState } from '../../interfaces/GlobalState';
-import { App, Category } from '../../interfaces';
+import { App, Category, Config } from '../../interfaces';
 
 
 // UI
 // UI
 import Icon from '../UI/Icons/Icon/Icon';
 import Icon from '../UI/Icons/Icon/Icon';
@@ -28,9 +28,6 @@ import SearchBar from '../SearchBar/SearchBar';
 import { greeter } from './functions/greeter';
 import { greeter } from './functions/greeter';
 import { dateTime } from './functions/dateTime';
 import { dateTime } from './functions/dateTime';
 
 
-// Utils
-import { searchConfig } from '../../utility';
-
 interface ComponentProps {
 interface ComponentProps {
   getApps: Function;
   getApps: Function;
   getCategories: Function;
   getCategories: Function;
@@ -38,6 +35,7 @@ interface ComponentProps {
   apps: App[];
   apps: App[];
   categoriesLoading: boolean;
   categoriesLoading: boolean;
   categories: Category[];
   categories: Category[];
+  config: Config;
 }
 }
 
 
 const Home = (props: ComponentProps): JSX.Element => {
 const Home = (props: ComponentProps): JSX.Element => {
@@ -77,7 +75,7 @@ const Home = (props: ComponentProps): JSX.Element => {
     let interval: any;
     let interval: any;
 
 
     // Start interval only when hideHeader is false
     // Start interval only when hideHeader is false
-    if (searchConfig('hideHeader', 0) !== 1) {
+    if (!props.config.hideHeader) {
       interval = setInterval(() => {
       interval = setInterval(() => {
         setHeader({
         setHeader({
           dateTime: dateTime(),
           dateTime: dateTime(),
@@ -103,13 +101,13 @@ const Home = (props: ComponentProps): JSX.Element => {
 
 
   return (
   return (
     <Container>
     <Container>
-      {searchConfig('hideSearch', 0) !== 1 ? (
+      {!props.config.hideSearch ? (
         <SearchBar setLocalSearch={setLocalSearch} />
         <SearchBar setLocalSearch={setLocalSearch} />
       ) : (
       ) : (
         <div></div>
         <div></div>
       )}
       )}
 
 
-      {searchConfig('hideHeader', 0) !== 1 ? (
+      {!props.config.hideHeader ? (
         <header className={classes.Header}>
         <header className={classes.Header}>
           <p>{header.dateTime}</p>
           <p>{header.dateTime}</p>
           <Link to="/settings" className={classes.SettingsLink}>
           <Link to="/settings" className={classes.SettingsLink}>
@@ -124,7 +122,7 @@ const Home = (props: ComponentProps): JSX.Element => {
         <div></div>
         <div></div>
       )}
       )}
 
 
-      {searchConfig('hideApps', 0) !== 1 ? (
+      {!props.config.hideApps ? (
         <Fragment>
         <Fragment>
           <SectionHeadline title="Applications" link="/applications" />
           <SectionHeadline title="Applications" link="/applications" />
           {appsLoading ? (
           {appsLoading ? (
@@ -148,7 +146,7 @@ const Home = (props: ComponentProps): JSX.Element => {
         <div></div>
         <div></div>
       )}
       )}
 
 
-      {searchConfig('hideCategories', 0) !== 1 ? (
+      {!props.config.hideCategories ? (
         <Fragment>
         <Fragment>
           <SectionHeadline title="Bookmarks" link="/bookmarks" />
           <SectionHeadline title="Bookmarks" link="/bookmarks" />
           {categoriesLoading ? (
           {categoriesLoading ? (
@@ -182,6 +180,7 @@ const mapStateToProps = (state: GlobalState) => {
     apps: state.app.apps,
     apps: state.app.apps,
     categoriesLoading: state.bookmark.loading,
     categoriesLoading: state.bookmark.loading,
     categories: state.bookmark.categories,
     categories: state.bookmark.categories,
+    config: state.config.config,
   };
   };
 };
 };
 
 

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

@@ -11,9 +11,10 @@ import {
 
 
 // Typescript
 // Typescript
 import {
 import {
+  Config,
   GlobalState,
   GlobalState,
   NewNotification,
   NewNotification,
-  SettingsForm,
+  OtherSettingsForm,
 } from '../../../interfaces';
 } from '../../../interfaces';
 
 
 // UI
 // UI
@@ -22,50 +23,29 @@ import Button from '../../UI/Buttons/Button/Button';
 import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
 import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
 
 
 // Utils
 // Utils
-import { searchConfig } from '../../../utility';
+import { otherSettingsTemplate, inputHandler } from '../../../utility';
 
 
 interface ComponentProps {
 interface ComponentProps {
   createNotification: (notification: NewNotification) => void;
   createNotification: (notification: NewNotification) => void;
-  updateConfig: (formData: SettingsForm) => void;
+  updateConfig: (formData: OtherSettingsForm) => void;
   sortApps: () => void;
   sortApps: () => void;
   sortCategories: () => void;
   sortCategories: () => void;
   loading: boolean;
   loading: boolean;
+  config: Config;
 }
 }
 
 
 const OtherSettings = (props: ComponentProps): JSX.Element => {
 const OtherSettings = (props: ComponentProps): JSX.Element => {
+  const { config } = props;
+
   // Initial state
   // Initial state
-  const [formData, setFormData] = useState<SettingsForm>({
-    customTitle: document.title,
-    pinAppsByDefault: 1,
-    pinCategoriesByDefault: 1,
-    hideHeader: 0,
-    hideApps: 0,
-    hideCategories: 0,
-    useOrdering: 'createdAt',
-    appsSameTab: 0,
-    bookmarksSameTab: 0,
-    dockerApps: 1,
-    dockerHost: 'localhost',
-    kubernetesApps: 1,
-    unpinStoppedApps: 1,
-  });
+  const [formData, setFormData] = useState<OtherSettingsForm>(
+    otherSettingsTemplate
+  );
 
 
   // Get config
   // Get config
   useEffect(() => {
   useEffect(() => {
     setFormData({
     setFormData({
-      customTitle: searchConfig('customTitle', 'Flame'),
-      pinAppsByDefault: searchConfig('pinAppsByDefault', 1),
-      pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1),
-      hideHeader: searchConfig('hideHeader', 0),
-      hideApps: searchConfig('hideApps', 0),
-      hideCategories: searchConfig('hideCategories', 0),
-      useOrdering: searchConfig('useOrdering', 'createdAt'),
-      appsSameTab: searchConfig('appsSameTab', 0),
-      bookmarksSameTab: searchConfig('bookmarksSameTab', 0),
-      dockerApps: searchConfig('dockerApps', 0),
-      dockerHost: searchConfig('dockerHost', 'localhost'),
-      kubernetesApps: searchConfig('kubernetesApps', 0),
-      unpinStoppedApps: searchConfig('unpinStoppedApps', 0),
+      ...config,
     });
     });
   }, [props.loading]);
   }, [props.loading]);
 
 
@@ -87,17 +67,13 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
   // Input handler
   // Input handler
   const inputChangeHandler = (
   const inputChangeHandler = (
     e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
     e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
-    isNumber?: boolean
+    options?: { isNumber?: boolean; isBool?: boolean }
   ) => {
   ) => {
-    let value: string | number = e.target.value;
-
-    if (isNumber) {
-      value = parseFloat(value);
-    }
-
-    setFormData({
-      ...formData,
-      [e.target.name]: value,
+    inputHandler<OtherSettingsForm>({
+      e,
+      options,
+      setStateHandler: setFormData,
+      state: formData,
     });
     });
   };
   };
 
 
@@ -126,8 +102,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
         <select
         <select
           id="pinAppsByDefault"
           id="pinAppsByDefault"
           name="pinAppsByDefault"
           name="pinAppsByDefault"
-          value={formData.pinAppsByDefault}
-          onChange={(e) => inputChangeHandler(e, true)}
+          value={formData.pinAppsByDefault ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
         >
         >
           <option value={1}>True</option>
           <option value={1}>True</option>
           <option value={0}>False</option>
           <option value={0}>False</option>
@@ -140,8 +116,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
         <select
         <select
           id="pinCategoriesByDefault"
           id="pinCategoriesByDefault"
           name="pinCategoriesByDefault"
           name="pinCategoriesByDefault"
-          value={formData.pinCategoriesByDefault}
-          onChange={(e) => inputChangeHandler(e, true)}
+          value={formData.pinCategoriesByDefault ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
         >
         >
           <option value={1}>True</option>
           <option value={1}>True</option>
           <option value={0}>False</option>
           <option value={0}>False</option>
@@ -165,8 +141,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
         <select
         <select
           id="appsSameTab"
           id="appsSameTab"
           name="appsSameTab"
           name="appsSameTab"
-          value={formData.appsSameTab}
-          onChange={(e) => inputChangeHandler(e, true)}
+          value={formData.appsSameTab ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
         >
         >
           <option value={1}>True</option>
           <option value={1}>True</option>
           <option value={0}>False</option>
           <option value={0}>False</option>
@@ -177,8 +153,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
         <select
         <select
           id="bookmarksSameTab"
           id="bookmarksSameTab"
           name="bookmarksSameTab"
           name="bookmarksSameTab"
-          value={formData.bookmarksSameTab}
-          onChange={(e) => inputChangeHandler(e, true)}
+          value={formData.bookmarksSameTab ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
         >
         >
           <option value={1}>True</option>
           <option value={1}>True</option>
           <option value={0}>False</option>
           <option value={0}>False</option>
@@ -192,8 +168,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
         <select
         <select
           id="hideHeader"
           id="hideHeader"
           name="hideHeader"
           name="hideHeader"
-          value={formData.hideHeader}
-          onChange={(e) => inputChangeHandler(e, true)}
+          value={formData.hideHeader ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
         >
         >
           <option value={1}>True</option>
           <option value={1}>True</option>
           <option value={0}>False</option>
           <option value={0}>False</option>
@@ -204,8 +180,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
         <select
         <select
           id="hideApps"
           id="hideApps"
           name="hideApps"
           name="hideApps"
-          value={formData.hideApps}
-          onChange={(e) => inputChangeHandler(e, true)}
+          value={formData.hideApps ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
         >
         >
           <option value={1}>True</option>
           <option value={1}>True</option>
           <option value={0}>False</option>
           <option value={0}>False</option>
@@ -216,8 +192,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
         <select
         <select
           id="hideCategories"
           id="hideCategories"
           name="hideCategories"
           name="hideCategories"
-          value={formData.hideCategories}
-          onChange={(e) => inputChangeHandler(e, true)}
+          value={formData.hideCategories ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
         >
         >
           <option value={1}>True</option>
           <option value={1}>True</option>
           <option value={0}>False</option>
           <option value={0}>False</option>
@@ -242,8 +218,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
         <select
         <select
           id="dockerApps"
           id="dockerApps"
           name="dockerApps"
           name="dockerApps"
-          value={formData.dockerApps}
-          onChange={(e) => inputChangeHandler(e, true)}
+          value={formData.dockerApps ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
         >
         >
           <option value={1}>True</option>
           <option value={1}>True</option>
           <option value={0}>False</option>
           <option value={0}>False</option>
@@ -256,8 +232,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
         <select
         <select
           id="unpinStoppedApps"
           id="unpinStoppedApps"
           name="unpinStoppedApps"
           name="unpinStoppedApps"
-          value={formData.unpinStoppedApps}
-          onChange={(e) => inputChangeHandler(e, true)}
+          value={formData.unpinStoppedApps ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
         >
         >
           <option value={1}>True</option>
           <option value={1}>True</option>
           <option value={0}>False</option>
           <option value={0}>False</option>
@@ -271,8 +247,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
         <select
         <select
           id="kubernetesApps"
           id="kubernetesApps"
           name="kubernetesApps"
           name="kubernetesApps"
-          value={formData.kubernetesApps}
-          onChange={(e) => inputChangeHandler(e, true)}
+          value={formData.kubernetesApps ? 1 : 0}
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
         >
         >
           <option value={1}>True</option>
           <option value={1}>True</option>
           <option value={0}>False</option>
           <option value={0}>False</option>
@@ -286,6 +262,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
 const mapStateToProps = (state: GlobalState) => {
 const mapStateToProps = (state: GlobalState) => {
   return {
   return {
     loading: state.config.loading,
     loading: state.config.loading,
+    config: state.config.config,
   };
   };
 };
 };
 
 

+ 9 - 3
client/src/components/Settings/SearchSettings/CustomQueries/CustomQueries.tsx

@@ -5,16 +5,21 @@ import classes from './CustomQueries.module.css';
 
 
 import Modal from '../../../UI/Modal/Modal';
 import Modal from '../../../UI/Modal/Modal';
 import Icon from '../../../UI/Icons/Icon/Icon';
 import Icon from '../../../UI/Icons/Icon/Icon';
-import { GlobalState, NewNotification, Query } from '../../../../interfaces';
+import {
+  Config,
+  GlobalState,
+  NewNotification,
+  Query,
+} from '../../../../interfaces';
 import QueriesForm from './QueriesForm';
 import QueriesForm from './QueriesForm';
 import { deleteQuery, createNotification } from '../../../../store/actions';
 import { deleteQuery, createNotification } from '../../../../store/actions';
 import Button from '../../../UI/Buttons/Button/Button';
 import Button from '../../../UI/Buttons/Button/Button';
-import { searchConfig } from '../../../../utility';
 
 
 interface Props {
 interface Props {
   customQueries: Query[];
   customQueries: Query[];
   deleteQuery: (prefix: string) => {};
   deleteQuery: (prefix: string) => {};
   createNotification: (notification: NewNotification) => void;
   createNotification: (notification: NewNotification) => void;
+  config: Config;
 }
 }
 
 
 const CustomQueries = (props: Props): JSX.Element => {
 const CustomQueries = (props: Props): JSX.Element => {
@@ -29,7 +34,7 @@ const CustomQueries = (props: Props): JSX.Element => {
   };
   };
 
 
   const deleteHandler = (query: Query) => {
   const deleteHandler = (query: Query) => {
-    const currentProvider = searchConfig('defaultSearchProvider', 'l');
+    const currentProvider = props.config.defaultSearchProvider;
     const isCurrent = currentProvider === query.prefix;
     const isCurrent = currentProvider === query.prefix;
 
 
     if (isCurrent) {
     if (isCurrent) {
@@ -104,6 +109,7 @@ const CustomQueries = (props: Props): JSX.Element => {
 const mapStateToProps = (state: GlobalState) => {
 const mapStateToProps = (state: GlobalState) => {
   return {
   return {
     customQueries: state.config.customQueries,
     customQueries: state.config.customQueries,
+    config: state.config.config,
   };
   };
 };
 };
 
 

+ 16 - 23
client/src/components/Settings/SearchSettings/SearchSettings.tsx

@@ -7,6 +7,7 @@ import { createNotification, updateConfig } from '../../../store/actions';
 
 
 // Typescript
 // Typescript
 import {
 import {
+  Config,
   GlobalState,
   GlobalState,
   NewNotification,
   NewNotification,
   Query,
   Query,
@@ -22,7 +23,7 @@ import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadli
 import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
 import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
 
 
 // Utils
 // Utils
-import { searchConfig } from '../../../utility';
+import { inputHandler, searchSettingsTemplate } from '../../../utility';
 
 
 // Data
 // Data
 import { queries } from '../../../utility/searchQueries.json';
 import { queries } from '../../../utility/searchQueries.json';
@@ -32,22 +33,17 @@ interface Props {
   updateConfig: (formData: SearchForm) => void;
   updateConfig: (formData: SearchForm) => void;
   loading: boolean;
   loading: boolean;
   customQueries: Query[];
   customQueries: Query[];
+  config: Config;
 }
 }
 
 
 const SearchSettings = (props: Props): JSX.Element => {
 const SearchSettings = (props: Props): JSX.Element => {
   // Initial state
   // Initial state
-  const [formData, setFormData] = useState<SearchForm>({
-    hideSearch: 0,
-    defaultSearchProvider: 'l',
-    searchSameTab: 0,
-  });
+  const [formData, setFormData] = useState<SearchForm>(searchSettingsTemplate);
 
 
   // Get config
   // Get config
   useEffect(() => {
   useEffect(() => {
     setFormData({
     setFormData({
-      hideSearch: searchConfig('hideSearch', 0),
-      defaultSearchProvider: searchConfig('defaultSearchProvider', 'l'),
-      searchSameTab: searchConfig('searchSameTab', 0),
+      ...props.config,
     });
     });
   }, [props.loading]);
   }, [props.loading]);
 
 
@@ -62,17 +58,13 @@ const SearchSettings = (props: Props): JSX.Element => {
   // Input handler
   // Input handler
   const inputChangeHandler = (
   const inputChangeHandler = (
     e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
     e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
-    isNumber?: boolean
+    options?: { isNumber?: boolean; isBool?: boolean }
   ) => {
   ) => {
-    let value: string | number = e.target.value;
-
-    if (isNumber) {
-      value = parseFloat(value);
-    }
-
-    setFormData({
-      ...formData,
-      [e.target.name]: value,
+    inputHandler<SearchForm>({
+      e,
+      options,
+      setStateHandler: setFormData,
+      state: formData,
     });
     });
   };
   };
 
 
@@ -110,8 +102,8 @@ const SearchSettings = (props: Props): JSX.Element => {
           <select
           <select
             id="searchSameTab"
             id="searchSameTab"
             name="searchSameTab"
             name="searchSameTab"
-            value={formData.searchSameTab}
-            onChange={(e) => inputChangeHandler(e, true)}
+            value={formData.searchSameTab ? 1 : 0}
+            onChange={(e) => inputChangeHandler(e, { isBool: true })}
           >
           >
             <option value={1}>True</option>
             <option value={1}>True</option>
             <option value={0}>False</option>
             <option value={0}>False</option>
@@ -122,8 +114,8 @@ const SearchSettings = (props: Props): JSX.Element => {
           <select
           <select
             id="hideSearch"
             id="hideSearch"
             name="hideSearch"
             name="hideSearch"
-            value={formData.hideSearch}
-            onChange={(e) => inputChangeHandler(e, true)}
+            value={formData.hideSearch ? 1 : 0}
+            onChange={(e) => inputChangeHandler(e, { isBool: true })}
           >
           >
             <option value={1}>True</option>
             <option value={1}>True</option>
             <option value={0}>False</option>
             <option value={0}>False</option>
@@ -143,6 +135,7 @@ const mapStateToProps = (state: GlobalState) => {
   return {
   return {
     loading: state.config.loading,
     loading: state.config.loading,
     customQueries: state.config.customQueries,
     customQueries: state.config.customQueries,
+    config: state.config.config,
   };
   };
 };
 };
 
 

+ 81 - 75
client/src/components/Settings/WeatherSettings/WeatherSettings.tsx

@@ -6,38 +6,40 @@ import { connect } from 'react-redux';
 import { createNotification, updateConfig } from '../../../store/actions';
 import { createNotification, updateConfig } from '../../../store/actions';
 
 
 // Typescript
 // Typescript
-import { ApiResponse, GlobalState, NewNotification, Weather, WeatherForm } from '../../../interfaces';
+import {
+  ApiResponse,
+  Config,
+  GlobalState,
+  NewNotification,
+  Weather,
+  WeatherForm,
+} from '../../../interfaces';
 
 
 // UI
 // 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';
 
 
 // Utils
 // Utils
-import { searchConfig } from '../../../utility';
+import { inputHandler, weatherSettingsTemplate } from '../../../utility';
 
 
 interface ComponentProps {
 interface ComponentProps {
   createNotification: (notification: NewNotification) => void;
   createNotification: (notification: NewNotification) => void;
   updateConfig: (formData: WeatherForm) => void;
   updateConfig: (formData: WeatherForm) => void;
   loading: boolean;
   loading: boolean;
+  config: Config;
 }
 }
 
 
 const WeatherSettings = (props: ComponentProps): JSX.Element => {
 const WeatherSettings = (props: ComponentProps): JSX.Element => {
   // Initial state
   // Initial state
-  const [formData, setFormData] = useState<WeatherForm>({
-    WEATHER_API_KEY: '',
-    lat: 0,
-    long: 0,
-    isCelsius: 1
-  })
+  const [formData, setFormData] = useState<WeatherForm>(
+    weatherSettingsTemplate
+  );
 
 
   // Get config
   // Get config
   useEffect(() => {
   useEffect(() => {
     setFormData({
     setFormData({
-      WEATHER_API_KEY: searchConfig('WEATHER_API_KEY', ''),
-      lat: searchConfig('lat', 0),
-      long: searchConfig('long', 0),
-      isCelsius: searchConfig('isCelsius', 1)
-    })
+      ...props.config,
+    });
   }, [props.loading]);
   }, [props.loading]);
 
 
   // Form handler
   // Form handler
@@ -48,120 +50,124 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
     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
     await props.updateConfig(formData);
     await props.updateConfig(formData);
-    
+
     // Update weather
     // Update weather
-    axios.get<ApiResponse<Weather>>('/api/weather/update')
+    axios
+      .get<ApiResponse<Weather>>('/api/weather/update')
       .then(() => {
       .then(() => {
         props.createNotification({
         props.createNotification({
           title: 'Success',
           title: 'Success',
-          message: 'Weather updated'
-        })
+          message: 'Weather updated',
+        });
       })
       })
       .catch((err) => {
       .catch((err) => {
         props.createNotification({
         props.createNotification({
           title: 'Error',
           title: 'Error',
-          message: err.response.data.error
-        })
+          message: err.response.data.error,
+        });
       });
       });
-  }
+  };
 
 
   // Input handler
   // 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
-    })
-  }
+  const inputChangeHandler = (
+    e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
+    options?: { isNumber?: boolean; isBool?: boolean }
+  ) => {
+    inputHandler<WeatherForm>({
+      e,
+      options,
+      setStateHandler: setFormData,
+      state: formData,
+    });
+  };
 
 
   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'
-          id='WEATHER_API_KEY'
-          name='WEATHER_API_KEY'
-          placeholder='secret'
+          type="text"
+          id="WEATHER_API_KEY"
+          name="WEATHER_API_KEY"
+          placeholder="secret"
           value={formData.WEATHER_API_KEY}
           value={formData.WEATHER_API_KEY}
           onChange={(e) => inputChangeHandler(e)}
           onChange={(e) => inputChangeHandler(e)}
         />
         />
         <span>
         <span>
           Using
           Using
-          <a
-            href='https://www.weatherapi.com/pricing.aspx'
-            target='blank'>
-            {' '}Weather API
+          <a href="https://www.weatherapi.com/pricing.aspx" target="blank">
+            {' '}
+            Weather API
           </a>
           </a>
           . Key is required for weather module to work.
           . Key is required for weather module to work.
         </span>
         </span>
       </InputGroup>
       </InputGroup>
       <InputGroup>
       <InputGroup>
-        <label htmlFor='lat'>Location latitude</label>
+        <label htmlFor="lat">Location latitude</label>
         <input
         <input
-          type='number'
-          id='lat'
-          name='lat'
-          placeholder='52.22'
+          type="number"
+          id="lat"
+          name="lat"
+          placeholder="52.22"
           value={formData.lat}
           value={formData.lat}
-          onChange={(e) => inputChangeHandler(e, true)}
-          step='any'
-          lang='en-150'
+          onChange={(e) => inputChangeHandler(e, { isNumber: true })}
+          step="any"
+          lang="en-150"
         />
         />
         <span>
         <span>
           You can use
           You can use
           <a
           <a
-            href='https://www.latlong.net/convert-address-to-lat-long.html'
-            target='blank'>
-            {' '}latlong.net
+            href="https://www.latlong.net/convert-address-to-lat-long.html"
+            target="blank"
+          >
+            {' '}
+            latlong.net
           </a>
           </a>
         </span>
         </span>
       </InputGroup>
       </InputGroup>
       <InputGroup>
       <InputGroup>
-        <label htmlFor='long'>Location longitude</label>
+        <label htmlFor="long">Location longitude</label>
         <input
         <input
-          type='number'
-          id='long'
-          name='long'
-          placeholder='21.01'
+          type="number"
+          id="long"
+          name="long"
+          placeholder="21.01"
           value={formData.long}
           value={formData.long}
-          onChange={(e) => inputChangeHandler(e, true)}
-          step='any'
-          lang='en-150'
+          onChange={(e) => inputChangeHandler(e, { isNumber: true })}
+          step="any"
+          lang="en-150"
         />
         />
       </InputGroup>
       </InputGroup>
       <InputGroup>
       <InputGroup>
-        <label htmlFor='isCelsius'>Temperature unit</label>
+        <label htmlFor="isCelsius">Temperature unit</label>
         <select
         <select
-          id='isCelsius'
-          name='isCelsius'
-          onChange={(e) => inputChangeHandler(e, true)}
-          value={formData.isCelsius}
+          id="isCelsius"
+          name="isCelsius"
+          onChange={(e) => inputChangeHandler(e, { isBool: true })}
+          value={formData.isCelsius ? 1 : 0}
         >
         >
           <option value={1}>Celsius</option>
           <option value={1}>Celsius</option>
           <option value={0}>Fahrenheit</option>
           <option value={0}>Fahrenheit</option>
         </select>
         </select>
       </InputGroup>
       </InputGroup>
-    <Button>Save changes</Button>
+      <Button>Save changes</Button>
     </form>
     </form>
-  )
-}
+  );
+};
 
 
 const mapStateToProps = (state: GlobalState) => {
 const mapStateToProps = (state: GlobalState) => {
   return {
   return {
-    loading: state.config.loading
-  }
-}
-
-export default connect(mapStateToProps, { createNotification, updateConfig })(WeatherSettings);
+    loading: state.config.loading,
+    config: state.config.config,
+  };
+};
+
+export default connect(mapStateToProps, { createNotification, updateConfig })(
+  WeatherSettings
+);

+ 38 - 38
client/src/components/Widgets/WeatherWidget/WeatherWidget.tsx

@@ -5,7 +5,7 @@ import axios from 'axios';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 
 
 // Typescript
 // Typescript
-import { Weather, ApiResponse, Config, GlobalState } from '../../../interfaces';
+import { Weather, ApiResponse, GlobalState, Config } from '../../../interfaces';
 
 
 // CSS
 // CSS
 import classes from './WeatherWidget.module.css';
 import classes from './WeatherWidget.module.css';
@@ -13,12 +13,9 @@ import classes from './WeatherWidget.module.css';
 // UI
 // UI
 import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon';
 import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon';
 
 
-// Utils
-import { searchConfig } from '../../../utility';
-
 interface ComponentProps {
 interface ComponentProps {
   configLoading: boolean;
   configLoading: boolean;
-  config: Config[];
+  config: Config;
 }
 }
 
 
 const WeatherWidget = (props: ComponentProps): JSX.Element => {
 const WeatherWidget = (props: ComponentProps): JSX.Element => {
@@ -32,26 +29,28 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
     conditionCode: 1000,
     conditionCode: 1000,
     id: -1,
     id: -1,
     createdAt: new Date(),
     createdAt: new Date(),
-    updatedAt: new Date()
+    updatedAt: new Date(),
   });
   });
   const [isLoading, setIsLoading] = useState(true);
   const [isLoading, setIsLoading] = useState(true);
 
 
   // Initial request to get data
   // Initial request to get data
   useEffect(() => {
   useEffect(() => {
-    axios.get<ApiResponse<Weather[]>>('/api/weather')
-      .then(data => {
+    axios
+      .get<ApiResponse<Weather[]>>('/api/weather')
+      .then((data) => {
         const weatherData = data.data.data[0];
         const weatherData = data.data.data[0];
         if (weatherData) {
         if (weatherData) {
           setWeather(weatherData);
           setWeather(weatherData);
         }
         }
         setIsLoading(false);
         setIsLoading(false);
       })
       })
-      .catch(err => console.log(err));
+      .catch((err) => console.log(err));
   }, []);
   }, []);
 
 
   // Open socket for data updates
   // Open socket for data updates
   useEffect(() => {
   useEffect(() => {
-    const socketProtocol = document.location.protocol === 'http:' ? 'ws:' : 'wss:';
+    const socketProtocol =
+      document.location.protocol === 'http:' ? 'ws:' : 'wss:';
     const socketAddress = `${socketProtocol}//${window.location.host}/socket`;
     const socketAddress = `${socketProtocol}//${window.location.host}/socket`;
     const webSocketClient = new WebSocket(socketAddress);
     const webSocketClient = new WebSocket(socketAddress);
 
 
@@ -59,43 +58,44 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
       const data = JSON.parse(e.data);
       const data = JSON.parse(e.data);
       setWeather({
       setWeather({
         ...weather,
         ...weather,
-        ...data
-      })
-    }
+        ...data,
+      });
+    };
 
 
     return () => webSocketClient.close();
     return () => webSocketClient.close();
   }, []);
   }, []);
 
 
   return (
   return (
     <div className={classes.WeatherWidget}>
     <div className={classes.WeatherWidget}>
-      {(isLoading || props.configLoading || searchConfig('WEATHER_API_KEY', '')) && 
-         (weather.id > 0 && 
-            (<Fragment>
-              <div className={classes.WeatherIcon}>
-                <WeatherIcon
-                  weatherStatusCode={weather.conditionCode}
-                  isDay={weather.isDay}
-                />
-              </div>
-              <div className={classes.WeatherDetails}>
-                {searchConfig('isCelsius', true)
-                  ? <span>{weather.tempC}°C</span>
-                  : <span>{weather.tempF}°F</span>
-                }
-                <span>{weather.cloud}%</span>
-              </div>
-            </Fragment>)
-        )
-      }
+      {isLoading ||
+        props.configLoading ||
+        (props.config.WEATHER_API_KEY && weather.id > 0 && (
+          <Fragment>
+            <div className={classes.WeatherIcon}>
+              <WeatherIcon
+                weatherStatusCode={weather.conditionCode}
+                isDay={weather.isDay}
+              />
+            </div>
+            <div className={classes.WeatherDetails}>
+              {props.config.isCelsius ? (
+                <span>{weather.tempC}°C</span>
+              ) : (
+                <span>{weather.tempF}°F</span>
+              )}
+              <span>{weather.cloud}%</span>
+            </div>
+          </Fragment>
+        ))}
     </div>
     </div>
-  )
-}
+  );
+};
 
 
 const mapStateToProps = (state: GlobalState) => {
 const mapStateToProps = (state: GlobalState) => {
   return {
   return {
     configLoading: state.config.loading,
     configLoading: state.config.loading,
-    config: state.config.config
-  }
-}
+    config: state.config.config,
+  };
+};
 
 
-export default connect(mapStateToProps)(WeatherWidget);
+export default connect(mapStateToProps)(WeatherWidget);

+ 22 - 8
client/src/interfaces/Config.ts

@@ -1,8 +1,22 @@
-import { Model } from './';
-
-export interface Config extends Model {
-  key: string;
-  value: string;
-  valueType: string;
-  isLocked: boolean;
-}
+export interface Config {
+  WEATHER_API_KEY: string;
+  lat: number;
+  long: number;
+  isCelsius: boolean;
+  customTitle: string;
+  pinAppsByDefault: boolean;
+  pinCategoriesByDefault: boolean;
+  hideHeader: boolean;
+  useOrdering: string;
+  appsSameTab: boolean;
+  bookmarksSameTab: boolean;
+  searchSameTab: boolean;
+  hideApps: boolean;
+  hideCategories: boolean;
+  hideSearch: boolean;
+  defaultSearchProvider: string;
+  dockerApps: boolean;
+  dockerHost: string;
+  kubernetesApps: boolean;
+  unpinStoppedApps: boolean;
+}

+ 14 - 17
client/src/interfaces/Forms.ts

@@ -2,30 +2,27 @@ export interface WeatherForm {
   WEATHER_API_KEY: string;
   WEATHER_API_KEY: string;
   lat: number;
   lat: number;
   long: number;
   long: number;
-  isCelsius: number;
+  isCelsius: boolean;
 }
 }
 
 
 export interface SearchForm {
 export interface SearchForm {
-  hideSearch: number;
+  hideSearch: boolean;
   defaultSearchProvider: string;
   defaultSearchProvider: string;
-  searchSameTab: number;
+  searchSameTab: boolean;
 }
 }
 
 
-export interface SettingsForm {
+export interface OtherSettingsForm {
   customTitle: string;
   customTitle: string;
-  pinAppsByDefault: number;
-  pinCategoriesByDefault: number;
-  hideHeader: number;
-  hideApps: number;
-  hideCategories: number;
-  // hideSearch: number;
-  // defaultSearchProvider: string;
+  pinAppsByDefault: boolean;
+  pinCategoriesByDefault: boolean;
+  hideHeader: boolean;
+  hideApps: boolean;
+  hideCategories: boolean;
   useOrdering: string;
   useOrdering: string;
-  appsSameTab: number;
-  bookmarksSameTab: number;
-  // searchSameTab: number;
-  dockerApps: number;
+  appsSameTab: boolean;
+  bookmarksSameTab: boolean;
+  dockerApps: boolean;
   dockerHost: string;
   dockerHost: string;
-  kubernetesApps: number;
-  unpinStoppedApps: number;
+  kubernetesApps: boolean;
+  unpinStoppedApps: boolean;
 }
 }

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

@@ -1,7 +1,8 @@
 export * from './iconParser';
 export * from './iconParser';
 export * from './urlParser';
 export * from './urlParser';
-export * from './searchConfig';
 export * from './checkVersion';
 export * from './checkVersion';
 export * from './sortData';
 export * from './sortData';
 export * from './searchParser';
 export * from './searchParser';
 export * from './redirectUrl';
 export * from './redirectUrl';
+export * from './templateObjects';
+export * from './inputHandler';

+ 39 - 0
client/src/utility/inputHandler.ts

@@ -0,0 +1,39 @@
+import { ChangeEvent, SetStateAction } from 'react';
+
+type Event = ChangeEvent<HTMLInputElement | HTMLSelectElement>;
+
+interface Options {
+  isNumber?: boolean;
+  isBool?: boolean;
+}
+
+interface Params<T> {
+  e: Event;
+  options?: Options;
+  setStateHandler: (v: SetStateAction<T>) => void;
+  state: T;
+}
+
+export const inputHandler = <T>(params: Params<T>): void => {
+  const { e, options, setStateHandler, state } = params;
+
+  const rawValue = e.target.value;
+  let value: string | number | boolean = e.target.value;
+
+  if (options) {
+    const { isNumber = false, isBool = false } = options;
+
+    if (isNumber) {
+      value = parseFloat(rawValue);
+    }
+
+    if (isBool) {
+      value = !!parseInt(rawValue);
+    }
+  }
+
+  setStateHandler({
+    ...state,
+    [e.target.name]: value,
+  });
+};

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

@@ -1,24 +0,0 @@
-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;
-    }
-  }
-  
-  return _default;
-}

+ 3 - 6
client/src/utility/searchParser.ts

@@ -1,7 +1,6 @@
 import { queries } from './searchQueries.json';
 import { queries } from './searchQueries.json';
 import { Query, SearchResult } from '../interfaces';
 import { Query, SearchResult } from '../interfaces';
 import { store } from '../store/store';
 import { store } from '../store/store';
-import { searchConfig } from '.';
 
 
 export const searchParser = (searchQuery: string): SearchResult => {
 export const searchParser = (searchQuery: string): SearchResult => {
   const result: SearchResult = {
   const result: SearchResult = {
@@ -16,7 +15,7 @@ export const searchParser = (searchQuery: string): SearchResult => {
     },
     },
   };
   };
 
 
-  const customQueries = store.getState().config.customQueries;
+  const { customQueries, config } = store.getState().config;
 
 
   // Check if url or ip was passed
   // Check if url or ip was passed
   const urlRegex =
   const urlRegex =
@@ -27,9 +26,7 @@ export const searchParser = (searchQuery: string): SearchResult => {
   // Match prefix and query
   // Match prefix and query
   const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
   const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
 
 
-  const prefix = splitQuery
-    ? splitQuery[1]
-    : searchConfig('defaultSearchProvider', 'l');
+  const prefix = splitQuery ? splitQuery[1] : config.defaultSearchProvider;
 
 
   const search = splitQuery
   const search = splitQuery
     ? encodeURIComponent(splitQuery[2])
     ? encodeURIComponent(splitQuery[2])
@@ -47,7 +44,7 @@ export const searchParser = (searchQuery: string): SearchResult => {
     if (prefix === 'l') {
     if (prefix === 'l') {
       result.isLocal = true;
       result.isLocal = true;
     } else {
     } else {
-      result.sameTab = searchConfig('searchSameTab', false);
+      result.sameTab = config.searchSameTab;
     }
     }
 
 
     return result;
     return result;

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

@@ -0,0 +1,24 @@
+import { Config } from '../../interfaces';
+
+export const configTemplate: Config = {
+  WEATHER_API_KEY: '',
+  lat: 0,
+  long: 0,
+  isCelsius: true,
+  customTitle: 'Flame',
+  pinAppsByDefault: true,
+  pinCategoriesByDefault: true,
+  hideHeader: false,
+  useOrdering: 'createdAt',
+  appsSameTab: false,
+  bookmarksSameTab: false,
+  searchSameTab: false,
+  hideApps: false,
+  hideCategories: false,
+  hideSearch: false,
+  defaultSearchProvider: 'l',
+  dockerApps: false,
+  dockerHost: 'localhost',
+  kubernetesApps: false,
+  unpinStoppedApps: false,
+};

+ 2 - 0
client/src/utility/templateObjects/index.ts

@@ -0,0 +1,2 @@
+export * from './configTemplate';
+export * from './settingsTemplate';

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

@@ -0,0 +1,30 @@
+import { OtherSettingsForm, SearchForm, WeatherForm } from '../../interfaces';
+
+export const otherSettingsTemplate: OtherSettingsForm = {
+  customTitle: document.title,
+  pinAppsByDefault: true,
+  pinCategoriesByDefault: true,
+  hideHeader: false,
+  hideApps: false,
+  hideCategories: false,
+  useOrdering: 'createdAt',
+  appsSameTab: false,
+  bookmarksSameTab: false,
+  dockerApps: true,
+  dockerHost: 'localhost',
+  kubernetesApps: true,
+  unpinStoppedApps: true,
+};
+
+export const weatherSettingsTemplate: WeatherForm = {
+  WEATHER_API_KEY: '',
+  lat: 0,
+  long: 0,
+  isCelsius: true,
+};
+
+export const searchSettingsTemplate: SearchForm = {
+  hideSearch: false,
+  searchSameTab: false,
+  defaultSearchProvider: 'l',
+};