diff --git a/src/App.js b/src/App.js index 7d79540..0ab6bb2 100644 --- a/src/App.js +++ b/src/App.js @@ -18,6 +18,7 @@ import ProfileMenu from 'components/profile/ProfileMenuView'; import Naps from 'pages/naps/naps/Naps'; import EditNap from 'pages/naps/edit/EditNap'; import Diapers from 'pages/diapers/Diapers'; +import Bodydata from 'pages/bodydata/Bodydata'; import Settings from 'pages/settings/Settings'; // Controllers @@ -28,6 +29,7 @@ import DiapersController from 'controllers/DiapersController'; // Libs import { toast } from "react-toastify"; import EditDiaper from 'pages/diapers/edit/EditDiaper'; +import BodydataController from 'controllers/BodydataController'; function App() { toast.configure({ @@ -73,6 +75,19 @@ function App() { currentUser }); + /* + * ============================== Bodydata + */ + let [measurements, setMeasurements] = useState([]); + + const bodydataController = BodydataController({ + setMeasurements, + firebase, + currentUser + }); + + console.log('Measurements', measurements); + return ( {currentUser ? ( @@ -145,6 +160,17 @@ function App() { )} /> + ( + + )} + /> { + const { + propstime = Date.now(), + propsweight = "", + propsbodysize = "", + propsheadcircumference = "", + headline = 'new Measurement', + submitText = 'save' + } = props; + + let [time, setTime] = useState(propstime); + let [weight, setWeight] = useState(propsweight); + let [bodySize, setBodySize] = useState(propsbodysize); + let [headCircumference, setHeadCircumference] = useState(propsheadcircumference); + + const onSubmit = (e) => { + e.preventDefault(); + const measurement = new Measurement(time, weight, bodySize, headCircumference); + + setTime(Date.now()); + setWeight(""); + setBodySize(""); + setHeadCircumference(""); + + props.onSubmit(measurement); + }; + + const onDateTimeChanged = (e) => { + if (e.target.value) { + setTime(new Date(e.target.value).getTime()); + } + }; + + const formatTimestampToInputDateTime = (ts) => { + let date = new Date(ts); + let dd = (date.getDate() < 10 ? "0" : "") + date.getDate(); + let MM = (date.getMonth() + 1 < 10 ? "0" : "") + (date.getMonth() + 1); + let yyyy = date.getFullYear(); + let hh = (date.getHours() < 10 ? "0" : "") + date.getHours(); + let mm = (date.getMinutes() < 10 ? "0" : "") + date.getMinutes(); + let str = `${yyyy}-${MM}-${dd}T${hh}:${mm}`; + return str; + }; + + return ( +
+
+ {headline} + + + + { + setWeight(e.target.value); + }} + /> + + { + setBodySize(e.target.value); + }} + /> + + { + setHeadCircumference(e.target.value); + }} + /> + +
+
+ ); +}; + +MeasurementForm.propTypes = { + time: PropTypes.number, + weight: PropTypes.number, + bodySize: PropTypes.number, + headCircumference: PropTypes.number, + onSubmit: PropTypes.func.isRequired, + headline: PropTypes.string, + submitText: PropTypes.string +}; + +export default MeasurementForm; diff --git a/src/components/bodydata/measurementform/measurements.scss b/src/components/bodydata/measurementform/measurements.scss new file mode 100644 index 0000000..c1aad2e --- /dev/null +++ b/src/components/bodydata/measurementform/measurements.scss @@ -0,0 +1,102 @@ +@import '../../../assets/scss/breakpoints'; +@import '../../../assets/scss/colors'; + +.measurement_form { + display: grid; + gap: 1rem; + grid-template-columns: 1fr 1fr; + grid-template-rows: repeat(7, auto); + grid-template-areas: + "legend legend" + "timelabel timelabel" + "time time" + "weightlabel weightlabel" + "weight weight" + "bodysizelabel bodysizelabel" + "bodysize bodysize" + "headcircumferencelabel headcircumferencelabel" + "headcircumference headcircumference" + "submit submit"; + + fieldset { + display: contents; + + legend { + grid-area: legend; + font-size: 1.4rem; + color: $mid; + font-weight: bold; + } + + input { + border: none; + border-bottom: 1px solid $light; + color: $dark; + outline: none; + width: 100%; + font-size: 1.3rem; + + &:focus { + border-bottom: 1px solid $highlight; + } + } + + label[for=time] { + grid-area: timelabel; + } + + input#time { + grid-area: time; + } + + label[for=weight] { + grid-area: weightlabel; + } + + .weight { + grid-area: weight; + } + + label[for=bodySize] { + grid-area: bodysizelabel; + } + + .bodySize { + grid-area: bodysize; + } + + label[for=headCircumference] { + grid-area: headcircumferencelabel; + } + + .headCircumference { + grid-area: headcircumference; + } + + input[type=submit] { + grid-area: submit; + border: none; + grid-row: 0 / span 2; + } + } +} + +.dark { + .naps_form { + legend { + color: $light; + } + + input { + background-color: transparent; + color: $white; + border-bottom: 1px solid $highlight; + } + + input[type=submit] { + background-color: $highlight; + color: $dark; + border: none; + } + } +} \ No newline at end of file diff --git a/src/components/diapers/diapersform/DiapersForm.js b/src/components/diapers/diapersform/DiapersForm.js index 0256997..39786f3 100644 --- a/src/components/diapers/diapersform/DiapersForm.js +++ b/src/components/diapers/diapersform/DiapersForm.js @@ -95,8 +95,9 @@ const DiapersForm = props => { } DiapersForm.propTypes = { - start: PropTypes.number, - end: PropTypes.number, + time: PropTypes.number, + pee: PropTypes.bool, + poo: PropTypes.bool, notes: PropTypes.string, onSubmit: PropTypes.func, headline: PropTypes.string, diff --git a/src/components/diapers/widgets/dailychart/ChartDaily.js b/src/components/diapers/widgets/dailychart/ChartDaily.js index 5c31156..e959d1e 100644 --- a/src/components/diapers/widgets/dailychart/ChartDaily.js +++ b/src/components/diapers/widgets/dailychart/ChartDaily.js @@ -21,7 +21,11 @@ const ChartDaily = props => { diapers.forEach(diaper => { if (data.length < maxDays) { - let tempDate = new Date(diaper.time).toLocaleDateString(); + let tempDate = new Date(diaper.time).toLocaleDateString([], { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); // check if we have reached the next day if (tempDate !== date) { // add last day to data array if not null @@ -51,7 +55,7 @@ const ChartDaily = props => { } function prepareChart() { - var dataTable = new GoogleCharts.api.visualization.DataTable(); + const dataTable = new GoogleCharts.api.visualization.DataTable(); dataTable.addColumn("string", "Date"); dataTable.addColumn("number", "Diapers"); dataTable.addColumn({ type: "number", role: "annotation" }); diff --git a/src/components/menu/LoggedInNav.js b/src/components/menu/LoggedInNav.js index 7c28339..95a1675 100644 --- a/src/components/menu/LoggedInNav.js +++ b/src/components/menu/LoggedInNav.js @@ -27,14 +27,14 @@ const LoggedInNav = (props) => { title="Diapers" /> - {/*
  • +
  • -
  • + {/*
  • { + const { firebase, currentUser, setMeasurements } = props; + + const createMeasurement = (measurement, successCb, errorCb) => { + const ref = firebase + .firestore() + .collection(`users/${currentUser.uid}/measurements`); + ref.add(measurement.toObject()) + .then(function () { if (successCb) { successCb(); } }) + .catch(function(error) { + console.error("Error writing document: ", error); + if (errorCb) { errorCb(); } + }); + }; + + const updateMeasurement = (measurement, successCb, errorCb) => { + const ref = firebase.firestore().collection(`users/${currentUser.uid}/measurements`); + ref.doc(measurement.id) + .update(measurement.toObject()) + .then(function() { + if (successCb) { + successCb(); + } + }) + .catch(function(error) { + console.error("Error updating document: ", error); + if (errorCb) { + errorCb(); + } + }); + }; + + const deleteMeasurement = (measurement, successCb, failureCb) => { + const ref = firebase.firestore().collection(`users/${currentUser.uid}/measurements`); + ref.doc(measurement.id) + .delete() + .then(function () { + if (successCb) { + successCb(); + } + }) + .catch(function (error) { + console.error("Error deleting document: ", error); + if (failureCb) { + failureCb(error); + } + });; + }; + + const getMeasurementById = (id, successCb, failureCb) => { + const doc = firebase + .firestore() + .collection(`users/${currentUser.uid}/measurements/`) + .doc(id) + .get() + .then(doc => { + if (!doc.exists) { + console.log("No such document!"); + if (failureCb) { + failureCb(); + } + } else { + if (successCb) { + successCb(doc); + } + } + }) + .catch(err => { + console.log("Error getting document", err); + if (failureCb) { + failureCb(err); + } + }); + return (doc); + }; + + const getMeasurements = (...args) => { + if (!currentUser || !currentUser.uid) { + return false; + } + + let ref = firebase.firestore().collection(`users/${currentUser.uid}/measurements`); + + if (!isNaN(args[0]) && parseInt(args[0]) < 100) { + // get last n Items + return ref.orderBy('time', 'desc').limit(parseInt(args[0])); + } + + if (args.length === 0) { + // get all Items of today + let todayMorning = new Date(); + todayMorning.setHours(0, 0, 0, 0); + let todayEvening = new Date(); + todayEvening.setHours(23, 59, 59, 999); + return ref + .where("time", ">=", parseInt(todayMorning.getTime())) + .where("time", "<=", parseInt(todayEvening.getTime())) + .orderBy("time", "desc"); + } + + if (parseInt(args[0]) > 100 && parseInt(args[1]) > 100) { + // get all Items between given timestamps + return ref + .where("time", ">=", parseInt(args[0])) + .where("time", "<=", parseInt(args[1])) + .orderBy("time", "desc"); + } + }; + + // get Measurements of last days + useEffect(() => { + // console.log("MeasurementsController mounted"); + + if (currentUser && currentUser.uid) { + let dateOffset = 24 * 60 * 60 * 1000 * 14; // 14 days + let firstDayStart = new Date(); + firstDayStart.setHours(0, 0, 0, 0); + firstDayStart.setTime(firstDayStart.getTime() - dateOffset); + let lastDayEnd = new Date(); + lastDayEnd.setHours(23, 59, 59, 999); + + let ref = firebase + .firestore() + .collection(`users/${currentUser.uid}/measurements`); + let unbindFirestore = ref + .where("time", ">=", firstDayStart.getTime()) + .where("time", "<=", lastDayEnd.getTime()) + .orderBy("time", "desc") + .onSnapshot((snapshot) => { + let measurements = []; + if (!snapshot.empty) { + snapshot.forEach((doc) => { + let measurement = new Measurement(); + measurement.fromFirebaseDoc(doc); + measurements.push(measurement); + }); + setMeasurements(measurements); + } else { + setMeasurements([]); + } + }); + + return () => { + unbindFirestore(); + // console.log("MeasurementsController unmounted"); + }; + } + }, [currentUser, firebase, setMeasurements]); + + return ({ + createMeasurement, + updateMeasurement, + deleteMeasurement, + getMeasurementById, + getMeasurements + }); +} + +BodydataController.propTypes = { + firebase: PropTypes.any, + currentUser: PropTypes.object.isRequired, + setMeasurements: PropTypes.func.isRequired +} + +export default BodydataController diff --git a/src/models/Measurement.js b/src/models/Measurement.js new file mode 100644 index 0000000..5c753a2 --- /dev/null +++ b/src/models/Measurement.js @@ -0,0 +1,56 @@ +class Measurement { + constructor( + time = null, + weight = null, + bodySize = null, + headCircumference, + id = null + ) { + this.id = id; + this.time = time; + this.weight = weight; + this.bodySize = bodySize; + this.headCircumference = headCircumference; + } + + fromObject(object) { + let { id = null, time = null, weight = null, bodySize = null, headCircumference = null } = object; + this.id = id; + this.time = time; + this.weight = weight; + this.bodySize = bodySize; + this.headCircumference = headCircumference; + } + + fromFirebaseDoc(doc) { + this.id = doc.id; + this.time = doc.data().time; + this.weight = doc.data().weight ? doc.data().weight : null; + this.bodySize = doc.data().bodySize ? doc.data().bodySize : null; + this.headCircumference = doc.data().headCircumference ? doc.data().headCircumference : null; + } + + hasWeight() { + return this.weight !== null; + } + + hasBodySize() { + return this.bodySize !== null; + } + + hasHeadCircumference() { + return this.headCircumference !== null; + } + + toObject(withId = false) { + let retObj = {}; + retObj.time = this.time; + if (this.weight) retObj.weight = this.weight; + if (this.bodySize) retObj.bodySize = this.bodySize; + if (this.headCircumference) retObj.headCircumference = this.headCircumference; + if (withId) retObj.id = this.id; + return retObj; + } +} + +export default Measurement; \ No newline at end of file diff --git a/src/pages/bodydata/Bodydata.js b/src/pages/bodydata/Bodydata.js new file mode 100644 index 0000000..d161e63 --- /dev/null +++ b/src/pages/bodydata/Bodydata.js @@ -0,0 +1,34 @@ +// React +import React from 'react' +import PropTypes from 'prop-types' + +// Libs +import { toast } from "react-toastify"; + +// Styles +import './bodydata.scss'; +import MeasurementForm from 'components/bodydata/measurementform/MeasurementForm'; + +const Bodydata = props => { + const { bodydataController } = props; + const onMeasurementFormSubmitted = (measurement) => { + bodydataController.createMeasurement( + measurement, + () => {toast.success('Measurement saved');}, + () => {toast.error('Measurement not saved');} + ); + } + return ( +
    +
    + +
    +
    + ); +} + +Bodydata.propTypes = { + currentUser: PropTypes.object +} + +export default Bodydata diff --git a/src/pages/bodydata/bodydata.scss b/src/pages/bodydata/bodydata.scss new file mode 100644 index 0000000..807405e --- /dev/null +++ b/src/pages/bodydata/bodydata.scss @@ -0,0 +1,50 @@ +@import '../../assets/scss/breakpoints'; +@import '../../assets/scss/colors'; + +section.page_bodydata { + padding: 2rem; + background-color: $mid; + grid-area: main; + display: grid; + gap: 2rem; + grid-template-columns: repeat(1, 1fr); + + @media (min-width: $breakpoints_m) { + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: $breakpoints_l) { + grid-template-columns: repeat(3, 1fr); + } + + .measurementform { + grid-column: 1; + grid-row: 1; + + @media (min-width: $breakpoints_m) { + grid-column: 1; + grid-row: 1; + } + + @media (min-width: $breakpoints_l) { + grid-column: 1 / span 2; + grid-row: 1; + } + } + + .list { + grid-column: 1; + grid-row: 3; + + @media (min-width: $breakpoints_m) { + grid-column: 2; + grid-row: 1; + } + + @media (min-width: $breakpoints_l) { + grid-column: 3; + grid-row: 1; + } + } + +}