9 Revize 0d35ce35af ... 0d9f15838f

Autor SHA1 Zpráva Datum
  mightyplow 0d9f15838f add selected item to appContext; unset selected item on remove and date change před 5 roky
  mightyplow c500f3254c add iconTypes dictionary před 5 roky
  mightyplow 630df6e87b use map for icons před 5 roky
  mightyplow 0c3ecc5ef9 add sass support před 5 roky
  mightyplow 3adcee51ac make css be the last imported item před 5 roky
  mightyplow 8a92525d3f make days swipable před 5 roky
  mightyplow 86fcc35784 make days swipable před 5 roky
  mightyplow bb805e1817 reset selected item on datechange před 5 roky
  mightyplow aa2aa2cf17 set key to input to ensure rerendering defaultValue před 5 roky

+ 57 - 11
package-lock.json

@@ -1021,6 +1021,22 @@
         "@types/react-router": "*"
       }
     },
+    "@types/react-swipe": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@types/react-swipe/-/react-swipe-6.0.0.tgz",
+      "integrity": "sha512-YYgC72+yZ/PYw7AV0JOZ0tKYbDvSERcUPqAn2TCoRZzDXEHHUssvPJiop7oEy3n7TXBfQdcLf13CA0q1x/EmFA==",
+      "dev": true,
+      "requires": {
+        "@types/react": "*",
+        "@types/swipe": "*"
+      }
+    },
+    "@types/swipe": {
+      "version": "2.0.27",
+      "resolved": "https://registry.npmjs.org/@types/swipe/-/swipe-2.0.27.tgz",
+      "integrity": "sha1-IDFs2XVmI+yzb8KjoTIIqgo8Lzg=",
+      "dev": true
+    },
     "@types/webpack-env": {
       "version": "1.14.1",
       "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.14.1.tgz",
@@ -4573,6 +4589,12 @@
       "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=",
       "dev": true
     },
+    "lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
+      "dev": true
+    },
     "lodash.memoize": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -6099,6 +6121,17 @@
         "tiny-warning": "^1.0.0"
       }
     },
+    "react-swipe": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/react-swipe/-/react-swipe-6.0.4.tgz",
+      "integrity": "sha512-NIF+gVOqPpE8GyCg0ssFC+fPgeqCwNnvqFU/A8nDAOvoncW3KjSVFwgkYNnErHvpFZGmsVw4SLWK96n7+mnChg==",
+      "dev": true,
+      "requires": {
+        "lodash.isequal": "^4.5.0",
+        "prop-types": "^15.6.0",
+        "swipe-js-iso": "^2.1.5"
+      }
+    },
     "readable-stream": {
       "version": "2.3.6",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
@@ -6393,6 +6426,15 @@
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
       "dev": true
     },
+    "sass": {
+      "version": "1.23.7",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.23.7.tgz",
+      "integrity": "sha512-cYgc0fanwIpi0rXisGxl+/wadVQ/HX3RhpdRcjLdj2o2ye/sxUTpAxIhbmJy3PLQgRFbf6Pn8Jsrta2vdXcoOQ==",
+      "dev": true,
+      "requires": {
+        "chokidar": ">=2.0.0 <4.0.0"
+      }
+    },
     "sax": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
@@ -6997,6 +7039,12 @@
         "util.promisify": "~1.0.0"
       }
     },
+    "swipe-js-iso": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/swipe-js-iso/-/swipe-js-iso-2.1.5.tgz",
+      "integrity": "sha512-yTTU5tDYEvtKfCD8PN+Rva25acJwogUCd6wPT1n1im/MOJlg6PtHiPKaaNK7HDBiIrWThA/WRbJZbox2letghg==",
+      "dev": true
+    },
     "symbol-tree": {
       "version": "3.2.4",
       "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -7161,17 +7209,15 @@
         "semver": "^5.3.0",
         "tslib": "^1.8.0",
         "tsutils": "^2.29.0"
-      },
-      "dependencies": {
-        "tsutils": {
-          "version": "2.29.0",
-          "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
-          "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
-          "dev": true,
-          "requires": {
-            "tslib": "^1.8.1"
-          }
-        }
+      }
+    },
+    "tsutils": {
+      "version": "2.29.0",
+      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+      "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+      "dev": true,
+      "requires": {
+        "tslib": "^1.8.1"
       }
     },
     "tty-browserify": {

+ 3 - 0
package.json

@@ -36,6 +36,7 @@
     "@types/react-dom": "^16.9.4",
     "@types/react-router": "^5.1.3",
     "@types/react-router-dom": "^5.1.2",
+    "@types/react-swipe": "^6.0.0",
     "@types/webpack-env": "^1.14.1",
     "classnames": "^2.2.6",
     "parcel-bundler": "^1.12.4",
@@ -45,6 +46,8 @@
     "react-dom": "^16.11.0",
     "react-router": "^5.1.2",
     "react-router-dom": "^5.1.2",
+    "react-swipe": "^6.0.4",
+    "sass": "^1.23.7",
     "tslint": "^5.20.1"
   }
 }

+ 12 - 9
src/components/caloriesInput/CaloriesInput.tsx

@@ -1,10 +1,11 @@
 import React, { FormEvent } from 'react';
 import { IconButton } from '../button/IconButton';
 import styles from './caloriesInput.css';
+import { iconTypes } from '../icon/iconTypes';
 
 type CaloriesInputProps = {
-  addCalories: (calories: { title: string, count: number}) => void,
-  saveCalories: (calories: { title: string, count: number}) => void,
+  addCalories: (calories: { title: string, count: number }) => void,
+  saveCalories: (calories: { title: string, count: number }) => void,
   item?: CalorieValue
 };
 
@@ -13,7 +14,7 @@ interface ICalorieInputForm extends HTMLFormElement {
   caloriesCount: HTMLInputElement;
 }
 
-function CaloriesInput ({ addCalories, saveCalories, item }: CaloriesInputProps) {
+function CaloriesInput ({addCalories, saveCalories, item}: CaloriesInputProps) {
   function onSubmit (event: FormEvent<ICalorieInputForm>) {
     event.preventDefault();
 
@@ -22,14 +23,14 @@ function CaloriesInput ({ addCalories, saveCalories, item }: CaloriesInputProps)
     const count = Number(form.caloriesCount.value) || 0;
 
     if (!item) {
-      addCalories({ title, count });
+      addCalories({title, count});
 
       form.querySelectorAll('input').forEach((formElement) => {
         formElement.value = '';
         formElement.blur();
       });
     } else {
-      saveCalories({ title, count});
+      saveCalories({title, count});
     }
   }
 
@@ -39,20 +40,22 @@ function CaloriesInput ({ addCalories, saveCalories, item }: CaloriesInputProps)
   } = item || {};
 
   const buttonIcon = item
-    ? 'tick'
-    : 'plus';
+    ? iconTypes.TICK
+    : iconTypes.PLUS;
 
   return (
     <form onSubmit={onSubmit} className={styles.caloriesInput}>
       <div className={styles.inputWrapper}>
         <div className={styles.formRow}>
           <label htmlFor="caloriesTitle">Nahrungsmittel:</label>
-          <input id="caloriesTitle" name="caloriesTitle" type="text" defaultValue={itemTitle} required />
+          <input key={itemTitle} id="caloriesTitle" name="caloriesTitle" type="text" defaultValue={itemTitle}
+                 required />
         </div>
 
         <div className={styles.formRow}>
           <label htmlFor="caloriesCount">Kalorien:</label>
-          <input id="caloriesCount" name="caloriesCount" type="number" defaultValue={itemCount} required />
+          <input key={itemCount} id="caloriesCount" name="caloriesCount" type="number" defaultValue={itemCount}
+                 required />
         </div>
       </div>
 

+ 2 - 1
src/components/caloriesList/CaloriesDateInput.tsx

@@ -4,11 +4,12 @@ import { getDateString } from '../../utils/date';
 import styles from './caloriesDateInput.css';
 
 function CaloriesDateInput () {
-  const {selectedDate, setSelectedDate} = useContext(AppContext);
+  const {selectedDate, setSelectedDate, setSelectedItem} = useContext(AppContext);
 
   function onDateChange (event: ChangeEvent<HTMLInputElement>) {
     const newDate = (new Date(event.target.value)).getTime();
     setSelectedDate(newDate);
+    setSelectedItem();
   }
 
   const date = new Date(selectedDate);

+ 51 - 13
src/components/caloriesList/CaloriesList.tsx

@@ -1,15 +1,22 @@
-import React, { useContext, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
+import ReactSwipe from 'react-swipe';
 import { AppContext } from '../../layout/AppContext';
 import { haveSameDay } from '../../utils/date';
 import { CaloriesInput } from '../caloriesInput/CaloriesInput';
-import { CalorieRow } from './CalorieRow';
-import styles from './caloriesList.css';
 import { ItemActions } from './ItemActions';
+import { ItemList } from './ItemList';
 import { ListEditBox } from './ListEditBox';
+import styles from './caloriesList.css';
 
 function CaloriesList () {
-  const {calorieItems = [], setCalorieItems, selectedDate} = useContext(AppContext);
-  const [selectedItem, setSelectedItem] = useState<CalorieValue>();
+  const {
+    calorieItems = [],
+    setCalorieItems,
+    selectedDate,
+    setSelectedDate,
+    selectedItem,
+    setSelectedItem
+  } = useContext(AppContext);
 
   function addCalories (addedCalories: { title: string, count: number }) {
     const updatedCalories = [
@@ -25,6 +32,7 @@ function CaloriesList () {
 
   function removeItem () {
     setCalorieItems(calorieItems.filter((item) => item !== selectedItem));
+    setSelectedItem();
   }
 
   function saveCalories (changedCalories: { title: string, count: number }) {
@@ -38,7 +46,7 @@ function CaloriesList () {
       ...changedCalories
     };
 
-    const updatedItems =  [
+    const updatedItems = [
       ...calorieItems.slice(0, itemIndex),
       updatedItem,
       ...calorieItems.slice(itemIndex + 1)
@@ -56,19 +64,49 @@ function CaloriesList () {
     return setSelectedItem(item);
   }
 
-  const calories = calorieItems.filter(({timestamp}) => haveSameDay(timestamp, selectedDate));
+  const swipeItem = (date: number, items: CalorieValue[]) => ({date, items});
+
+  const dayCalories = [
+    selectedDate - 3600 * 24 * 1000,
+    selectedDate,
+    selectedDate + 3600 * 24 * 1000
+  ].map((date) => {
+    return swipeItem(date, calorieItems.filter(({timestamp}) => haveSameDay(timestamp, date)));
+  });
 
   return (
     <section className={styles.caloriesList}>
-      <section className={styles.caloriesItems}>
-        {calories.map((item: CalorieValue, index: number) => {
-          // todo: use better key
-          const isSelected = item === selectedItem;
+      <ReactSwipe swipeOptions={{
+        continuous: false,
+        speed: 250,
+        startSlide: 1,
+        transitionEnd (slide) {
+          const newDate = slide === 0
+            ? selectedDate - 3600 * 24 * 1000
+            : slide === 2
+              ? selectedDate + 3600 * 24 * 1000
+              : selectedDate;
+
+          setSelectedDate(newDate);
+        }
+      }} style={{
+        child: {
+          float: 'left',
+          position: 'relative'
+        },
+        container: {
+          flex: 1
+        },
+        wrapper: {
+          height: '100%'
+        }
+      }}>
+        {dayCalories.map(({date, items}) => {
           return (
-            <CalorieRow key={index} {...{item, onRowClick, isSelected}} />
+            <ItemList key={date} {...{items, selectedItem, onRowClick}} />
           );
         })}
-      </section>
+      </ReactSwipe>
 
       <div className={styles.editBoxes}>
         {selectedItem &&

+ 2 - 1
src/components/caloriesList/ItemActions.tsx

@@ -1,5 +1,6 @@
 import React from 'react';
 import { IconButton } from '../button/IconButton';
+import { iconTypes } from '../icon/iconTypes';
 import styles from './itemActions.css';
 
 type ItemActionsProps = {
@@ -13,7 +14,7 @@ function ItemActions (props: ItemActionsProps) {
 
   return (
     <section className={styles.itemActions}>
-      <IconButton icon="trash" buttonClassName={styles.button} onClick={removeItem} />
+      <IconButton icon={iconTypes.TRASH} buttonClassName={styles.button} onClick={removeItem} />
     </section>
   );
 }

+ 31 - 0
src/components/caloriesList/ItemList.tsx

@@ -0,0 +1,31 @@
+import cx from 'classnames';
+import React from 'react';
+import { CalorieRow } from './CalorieRow';
+import styles from './itemList.css';
+
+type ItemListProps = {
+  className?: string,
+  items: CalorieValue[],
+  selectedItem?: CalorieValue,
+  onRowClick: (item: CalorieValue) => void
+};
+
+function ItemList (props: ItemListProps) {
+  const {className, items, selectedItem, onRowClick, ...restProps} = props;
+
+  return (
+    <section {...restProps} className={cx(styles.itemList, className)}>
+      {items.map((item: CalorieValue, index: number) => {
+        // todo: use better key
+        const isSelected = item === selectedItem;
+        return (
+          <CalorieRow key={index} {...{item, onRowClick, isSelected}} />
+        );
+      })}
+    </section>
+  );
+}
+
+export {
+  ItemList
+};

+ 0 - 4
src/components/caloriesList/caloriesList.css

@@ -4,10 +4,6 @@
     height: 100%;
 }
 
-.caloriesItems {
-    overflow: auto;
-}
-
 .editBoxes {
     margin-top: auto;
 }

+ 4 - 0
src/components/caloriesList/itemList.css

@@ -0,0 +1,4 @@
+.itemList {
+    height: 100%;
+    overflow: auto;
+}

+ 1 - 1
src/components/icon/Icon.tsx

@@ -1,6 +1,6 @@
 import cx from 'classnames';
 import React from 'react';
-import styles from './icon.css';
+import styles from './icon.scss';
 
 type IconProps = {
   icon: string,

+ 0 - 46
src/components/icon/icon.css

@@ -1,46 +0,0 @@
-:root {
-    --icon-settings: '\e900';
-    --icon-plus: '\e901';
-    --icon-trash: '\e902';
-    --icon-edit: '\e903';
-    --icon-tick: '\e904';
-}
-
-.icon {
-    display: inline-flex;
-    align-items: center;
-
-    &:after {
-        font-family: simpleCalorieTracker;
-    }
-
-    &.icon-settings {
-        &:after {
-            content: var(--icon-settings);
-        }
-    }
-
-    &.icon-plus {
-        &:after {
-            content: var(--icon-plus);
-        }
-    }
-
-    &.icon-edit {
-        &:after {
-            content: var(--icon-edit);
-        }
-    }
-
-    &.icon-trash {
-        &:after {
-            content: var(--icon-trash);
-        }
-    }
-
-    &.icon-tick {
-        &:after {
-            content: var(--icon-tick);
-        }
-    }
-}

+ 16 - 0
src/components/icon/icon.scss

@@ -0,0 +1,16 @@
+@import '../../styles/maps/icons';
+
+.icon {
+    display: inline-flex;
+    align-items: center;
+
+    &:after {
+        font-family: simpleCalorieTracker;
+    }
+
+    @each $name, $value in $icons {
+        &.icon-#{$name}:after {
+            content: $value;
+        }
+    }
+}

+ 11 - 0
src/components/icon/iconTypes.ts

@@ -0,0 +1,11 @@
+const iconTypes = {
+  EDIT: 'edit',
+  PLUS: 'plus',
+  SETTINGS: 'settings',
+  TICK: 'tick',
+  TRASH: 'trash'
+};
+
+export {
+  iconTypes
+};

+ 9 - 1
src/layout/App.tsx

@@ -17,13 +17,21 @@ const storedCalories = JSON.parse(localStorage.getItem('calorieItems') || '[]');
 function App (): ReactElement {
   const [calorieItems, setCalorieItems] = useState<CalorieItems>(storedCalories);
   const [selectedDate, setSelectedDate] = useState<number>(Date.now());
+  const [selectedItem, setSelectedItem] = useState<CalorieValue>();
 
   useEffect(() => {
     localStorage.setItem('calorieItems', JSON.stringify(calorieItems));
   });
 
   return (
-    <AppContextProvider value={{calorieItems, setCalorieItems, selectedDate, setSelectedDate}}>
+    <AppContextProvider value={{
+      calorieItems,
+      selectedDate,
+      selectedItem,
+      setCalorieItems,
+      setSelectedDate,
+      setSelectedItem
+    }}>
       <PageHeader />
 
       <main id="pageContent">

+ 6 - 2
src/layout/AppContext.ts

@@ -5,14 +5,18 @@ type AppContextType = {
   selectedDate: number,
   setSelectedDate: (selectedDate: number) => void,
   calorieItems: CalorieItems;
-  setCalorieItems: (calorieItems: CalorieValue[]) => void;
+  setCalorieItems: (calorieItems: CalorieValue[]) => void,
+  selectedItem: CalorieValue | undefined,
+  setSelectedItem: (item?: CalorieValue) => void
 };
 
 const AppContext = createContext<AppContextType>({
   calorieItems: [],
   selectedDate: Date.now(),
+  selectedItem: undefined,
   setCalorieItems: () => [],
-  setSelectedDate: () => getDateString(new Date())
+  setSelectedDate: () => getDateString(new Date()),
+  setSelectedItem: () => {}
 });
 
 export {

+ 2 - 1
src/layout/pageFooter/PageFooter.tsx

@@ -1,12 +1,13 @@
 import React from 'react';
 import { IconButton } from '../../components/button/IconButton';
+import { iconTypes } from '../../components/icon/iconTypes';
 import styles from './pageFooter.css';
 
 function PageFooter () {
   return (
     <footer id="pageFooter" className={styles.pageFooter}>
       <section className={styles.buttons}>
-        <IconButton icon="settings" />
+        <IconButton icon={iconTypes.SETTINGS} />
       </section>
     </footer>
   );

+ 7 - 0
src/styles/maps/icons.scss

@@ -0,0 +1,7 @@
+$icons: (
+  settings: '\e900',
+  plus: '\e901',
+  trash: '\e902',
+  edit: '\e903',
+  tick: '\e904'
+);

+ 0 - 2
src/styles/maps/icons.yml

@@ -1,2 +0,0 @@
-settings: '\e900'
-plus: '\e901'

+ 5 - 0
src/typings/cssModules.d.ts

@@ -2,3 +2,8 @@ declare module '*.css' {
   const styles: { [className: string]: string };
   export default styles;
 }
+
+declare module '*.scss' {
+  const styles: { [className: string]: string };
+  export default styles;
+}

+ 7 - 1
tslint.json

@@ -8,7 +8,13 @@
         "interface-over-type-literal": false,
         "trailing-comma": false,
         "quotemark": false,
-        "space-before-function-paren": false
+        "space-before-function-paren": false,
+        "ordered-imports": [
+            true,
+            {
+                "import-sources-order": "lowercase-last"
+            }
+        ]
     },
     "rulesDirectory": []
 }