From 2b76f74deeff9d344f6d89c230db78672392d3e9 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 21 Oct 2022 19:34:13 +0000 Subject: [PATCH] Added cycle tracking beta to app --- client/src/assets/semantic-helper.css | 4 + client/src/pages/CycletrackingPage.vue | 787 +++++++++++++++--- client/src/stores/mainStore.js | 2 +- configs/dev nginx sites available default.cfg | 85 +- server/models/Note.js | 3 +- 5 files changed, 702 insertions(+), 179 deletions(-) diff --git a/client/src/assets/semantic-helper.css b/client/src/assets/semantic-helper.css index b04045c..64942dc 100644 --- a/client/src/assets/semantic-helper.css +++ b/client/src/assets/semantic-helper.css @@ -169,6 +169,10 @@ i.green.icon.icon.icon.icon { } .button { box-shadow: 2px 2px 4px -2px rgba(40, 40, 40, 0.89) !important; + transition: all 0.9s ease; +} +.button:hover { + box-shadow: 3px 2px 5px -2px rgba(40, 40, 40, 0.95) !important; } .ui.green.buttons, .ui.green.button, .ui.green.button:hover { background-color: var(--main-accent); diff --git a/client/src/pages/CycletrackingPage.vue b/client/src/pages/CycletrackingPage.vue index d0d1949..82e51bc 100644 --- a/client/src/pages/CycletrackingPage.vue +++ b/client/src/pages/CycletrackingPage.vue @@ -1,101 +1,423 @@ @@ -103,24 +425,50 @@ import axios from 'axios' + var BASAL_TEMP = 'BT' + export default { - name: 'CycleTracking', + name: 'MetricTracking', components: { 'logo':require('@/components/LogoComponent.vue').default, }, data () { return { - appWorkingDate: null, + folded:[], + working: true, + showNotes: false, fields:[], - defaultFields:[ - {'type':'float','label':'Basil Temp', 'id':'BT'}, - {'type':'range','label':'Cervical Mucus', 'id':'CM'}, - {'type':'text','label':'Notes', 'id':'NO'}, - ], + fieldTypes:{ + 'float':'Precise Number', + 'shortRange':'Options 1-5', + 'longRange':'Options 1-10', + 'text':'Text Input', + 'boolean':'Yes, No', + 'period':'No, 1. Light, 2. Normal, 3. Heavy, 4. Irregular, 5. Painful', + 'mucus':'None, 1. Watery, 2. Eggwhite, 3. Creamy, 4. Sticky', + 'sex':'None, With contraception, Without contraception', + 'pms':'No, 1. Maybe, 2. A little, 3. Real Cranky', + }, + defaultFields:{ + 'BT': {'type':'float','label':'Basal Temp','icon':'thermometer half','width':'sixteen wide column'}, + 'CM': {'type':'mucus','label':'Cervical Mucus','icon':'','width':'eight wide column'}, + 'PE': {'type':'period','label':'Having Period','icon':'','width':'eight wide column'}, + 'SE': {'type':'sex','label':'Sex','icon':'','width':'eight wide column'}, + 'NO': {'type':'text','label':'Notes','icon':'','width':'sixteen wide column'}, + 'OV': {'type':'boolean','label':'Suspect Ovulation','icon':'','width':''}, + 'PM': {'type':'pms','label':'PMS','icon':'','width':''}, + 'HO': {'type':'boolean','label':'Horny','icon':'','width':''}, + 'DI': {'type':'text','label':'Diet','icon':'','width':''}, + 'EX': {'type':'text','label':'Exercise','icon':'','width':''}, + }, cycleData: {}, - dateCode: null, - openDay: [], + totalEntries: 0, + openDay: {}, + saveDataDebounce:null, + saving: 0, // 0 blank, 1 modified, 2 saving, 3 saved calendar: { + dateObject: null, + dateCode: null, monthName: '', month: '', year: '', @@ -128,6 +476,7 @@ weekdays: ['S','M','T','W','T','F','S'], today: 0, }, + tempChartDays: 60, } }, beforeCreate() { @@ -141,25 +490,178 @@ return } - // setup date code - // day - month - year - const now = new Date() - this.appWorkingDate = now - const dateSetup = [ - now.getDate(), // 1-31 (Day) - now.getMonth()+1, // 0-11 (Month) - now.getFullYear(), // 1888-2022 (Year) - ] + // set up reactive open day object + Object.keys(this.defaultFields).forEach(fieldId => { + this.$set(this.openDay, fieldId, '') + }) - this.dateCode = dateSetup.join('.') + // Include JS libraries + let graphs = document.createElement('script') + graphs.setAttribute('src', '//cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js') + document.head.appendChild(graphs) - this.setupCalendar(this.appWorkingDate) + // setup date to today + this.setupCalendar() this.fetchCycleData() }, + computed: { + getToday(){ + return this.generateDateCode(new Date()) + }, + getPreviousDay(){ + const workingDate = this.calendar.dateObject || new Date() + workingDate.setDate(workingDate.getDate()-1) + + return this.generateDateCode(workingDate) + }, + getNextDay(){ + const workingDate = this.calendar.dateObject || new Date() + workingDate.setDate(workingDate.getDate()+1) + + return this.generateDateCode(workingDate) + }, + getNextMonth(){ + const workingDate = this.calendar.dateObject || new Date() + workingDate.setMonth(workingDate.getMonth() +1) + + return this.generateDateCode(workingDate) + }, + getPreviousMonth(){ + const workingDate = this.calendar.dateObject || new Date() + workingDate.setMonth(workingDate.getMonth() -1) + + return this.generateDateCode(workingDate) + }, + }, methods: { + toggleFolded(key){ + const index = this.folded.indexOf(key) + if(index == -1){ + this.folded.push(key) + return + } + + this.folded.splice(index,1) + }, + showDayDataColor(day){ + // Determine if day has any data set + if(day == ''){ + return false + } + return !(this.cycleData[`${day}.${this.calendar.month}.${this.calendar.year}`]) + }, + isPeriod(day){ + const data = this.cycleData[`${day}.${this.calendar.month}.${this.calendar.year}`] + if(data?.PE > 0){ + return true + } + }, + isSex(day){ + const data = this.cycleData[`${day}.${this.calendar.month}.${this.calendar.year}`] + return data?.SE + }, + isMucus(day){ + const data = this.cycleData[`${day}.${this.calendar.month}.${this.calendar.year}`] + return data?.CM + }, + isNotes(day){ + const data = this.cycleData[`${day}.${this.calendar.month}.${this.calendar.year}`] + return data?.NO + }, + isTemp(day){ + const data = this.cycleData[`${day}.${this.calendar.month}.${this.calendar.year}`] + return data?.BT + }, + fieldRemove(field){ + for (let i = this.fields.length - 1; i >= 0; i--) { + if(field == this.fields[i]){ + this.fields.splice(i,1) + break + } + } + this.saveCycleData() + }, + fieldAdd(fieldId){ + this.fields.push(fieldId) + this.saveCycleData() + }, + graphCurrentData(){ + + // CSV or path to a CSV file. + let dataString = "Date,Temperature,Average\n" + + // Excel date format YYYYMMDD + const convertToExcelDate = (dateCode) => { + return dateCode + .split('.') + .reverse() + .map(item => String(item).padStart(2,0)) + .join('') + } + + const dataKeys = Object.keys(this.cycleData) + + // calculate average + let average = 0.0 + let totalTemps = 0 + for (var i = 0; i < dataKeys.length; i++) { + const current = this.cycleData[dataKeys[i]] + if(current.BT){ + average += parseFloat(current.BT) + totalTemps++ + } + } + average = (average/totalTemps) + + // build CSV data + for (var i = 0; i < dataKeys.length; i++) { + const current = this.cycleData[dataKeys[i]] + let nextFragment = [] + + // push date code + nextFragment.push(convertToExcelDate(dataKeys[i])) + + if(current.BT){ + // parse temp to fixed length float 00.00 + nextFragment.push(parseFloat(current.BT).toFixed(2)) + } else { + continue + } + + nextFragment.push(average) + + dataString += nextFragment.join(',') + "\n" + + if(i >= this.tempChartDays){ + break + } + } + + let graphDiv = document.getElementById("graphdiv") + const graphOptions = { + animatedZoom: true, + } + + const g = new Dygraph(graphDiv, dataString ,graphOptions) + }, saveField(fieldId, value){ - this.openDay[fieldId] = value + + // Dont save value if it hasn't changed + if(this.openDay[fieldId] == value){ return } + + // update field to be reactive + this.$set(this.openDay, fieldId, value) + + // remove debounce and set to modified + this.$nextTick(() => { + //0 blank, 1 modified, 2 saving, 3 saved + this.saving = 1 + clearTimeout(this.saveDataDebounce) + this.saveDataDebounce = setTimeout(() => { + this.saveDayData() + }, 500) + }) }, openDayData(dateCode){ @@ -168,16 +670,14 @@ return } - this.dateCode = dateCode || this.dateCode + this.setupCalendar(this.dateCodeToDate(dateCode)) - let currentDay = this.cycleData[this.dateCode] || {} - - //Set up each entry empty or with current value - this.fields.forEach(field => { - currentDay[field.id] = currentDay[field.id] || '' + // open day has all fields defined, just set values + let currentDay = this.cycleData[dateCode] || {} + Object.keys(this.openDay).forEach(fieldId => { + this.openDay[fieldId] = currentDay[fieldId] || '' }) - - this.openDay = currentDay + }, saveDayData(){ @@ -185,15 +685,18 @@ // remove empty keys let cleanDayData = {} Object.keys(this.openDay).forEach(key => { - if(this.openDay[key] != ''){ + if(this.openDay[key] != '' && this.openDay[key] != 0){ cleanDayData[key] = this.openDay[key] } }) - this.cycleData[this.dateCode] = cleanDayData + // Only save entry if there is data + delete this.cycleData[this.calendar.dateCode] + if(Object.keys(cleanDayData).length > 0){ + this.cycleData[this.calendar.dateCode] = cleanDayData + } - // Update calendar - this.setupCalendar(this.appWorkingDate) + this.graphCurrentData() this.saveCycleData() @@ -211,14 +714,20 @@ console.log('Didnt parse json') } - console.log(appData) + // console.clear() + // console.log(appData) this.cycleData = appData?.cycleData || {} this.fields = appData?.fields || [] this.$nextTick(() => { + this.totalEntries = Object.keys(this.cycleData).length this.setupFields() - this.openDayData(this.dateCode) + this.openDayData(this.calendar.dateCode) + + this.graphCurrentData() + + this.generateTonsOfRandomData() }) } }) @@ -231,9 +740,17 @@ fields: this.fields, cycleData: this.cycleData, }) + + // 0 blank, 1 modified, 2 saving, 3 saved + this.saving = 2 // Working + this.totalEntries = Object.keys(this.cycleData).length axios.post('/api/cycle-tracking/save', { cycleData:appData }) .then(response => { - { this.$bus.$emit('notification', 'Data Saved') } + // { this.$bus.$emit('notification', 'Data Saved') } + this.saving = 3 //Saved + setTimeout(() => { + this.saving = 0 //Reset + }, 2000) }) .catch(error => { this.$bus.$emit('notification', error) }) }, @@ -241,18 +758,54 @@ axios.post('/api/cycle-tracking/save', { cycleData:'' }) .then(response => { { this.$bus.$emit('notification', 'Data Deleted') } + this.fetchCycleData() }) }, setupFields(){ // push the first 3 default fields to users set if(this.fields.length == 0){ - for (let i = 0; i < 3; i++) { - this.fields.push(this.defaultFields[i]) + const fieldKeys = Object.keys(this.defaultFields) + console.log('Setup default fierds') + for (let i = 0; i < 5; i++) { + this.fields.push(fieldKeys[i]) } } }, + generateDateCode(date){ + + const dateSetup = [ + date.getDate(), // 1-31 (Day) + date.getMonth()+1, // 0-11 (Month) + date.getFullYear(), // 1888-2022 (Year) + ] + + return dateSetup.join('.') + }, + dateCodeToDate(dateCode){ + + const dateChunk = dateCode.split('.') + return new Date(dateChunk[2], dateChunk[1]-1, dateChunk[0]) + }, setupCalendar(date){ + + this.working = true + setTimeout(() => { + this.working = false + }, 500) + + if(!date && this.dateObject){ + date = this.dateObject + } + if(!date){ + date = new Date() + } + + this.calendar.dateObject = date + + this.calendar.dateCode = this.generateDateCode(date) + + // ------------ // setup calendar display var y = date.getFullYear() @@ -272,7 +825,7 @@ const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth); const monthStartDay = firstDay.getDay() - let days = Array(monthStartDay).fill("."); // Pad days to start on correct weekday + let days = Array(monthStartDay).fill(""); // Pad days to start on correct weekday for (let i = 0; i < daysInCurrentMonth; i++) { days.push(i+1) } @@ -291,7 +844,33 @@ */ // ------- - } + }, + generateTonsOfRandomData(){ + + return + + let workingDate = new Date() + + for (var i = 0; i < 365 * 2; i++) { + + const cycleTime = (i%30)+1 + const randomInt = Math.floor(Math.random() * cycleTime+20) + (cycleTime); + let randomTemp = parseFloat(`97.${randomInt}`) + const randomFive = Math.floor(Math.random() * 4) + 0; + const randomHundo = Math.floor(Math.random() * 100) + 1; + const randUnoOrDuo = Math.floor(Math.random() * 2) + 1; + + this.cycleData[this.generateDateCode(workingDate)] = { + 'BT':randomTemp, + 'CM':randomFive, + 'SE':randomHundo > 90 ? randUnoOrDuo : 0, + } + + workingDate.setDate(workingDate.getDate()-1) + } + + this.graphCurrentData(5000) + }, } } \ No newline at end of file diff --git a/client/src/stores/mainStore.js b/client/src/stores/mainStore.js index c24dec8..37d698e 100644 --- a/client/src/stores/mainStore.js +++ b/client/src/stores/mainStore.js @@ -48,7 +48,7 @@ export default new Vuex.Store({ 'small_element_bg_color': '#000', 'text_color': '#FFF', 'dark_border_color': '#555',//'#ACACAC', //Lighter color to accent elemnts user can interact with - 'border_color': '#0b0110', + 'border_color': '#505050', 'menu-accent': '#626262', 'menu-text': '#d9d9d9', }, diff --git a/configs/dev nginx sites available default.cfg b/configs/dev nginx sites available default.cfg index 623dad0..104b445 100644 --- a/configs/dev nginx sites available default.cfg +++ b/configs/dev nginx sites available default.cfg @@ -2,12 +2,22 @@ # Working dev server config # +server { + listen 80; + listen [::]:80; + server_name 192.168.1.164; + return 301 https://$host$request_uri; +} + + server { listen 443 ssl; - ssl_certificate /home/mab/ss/client/certs/192.168.1.164+4.pem; - ssl_certificate_key /home/mab/ss/client/certs/192.168.1.164+4-key.pem; + ssl_certificate /home/mab/ss/client/certs/nginx-selfsigned.crt; + ssl_certificate_key /home/mab/ss/client/certs/nginx-selfsigned.key; + ssl_dhparam /home/mab/ss/client/certs/dhparam.pem; + ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; ssl_protocols TLSV1.1 TLSV1.2 TLSV1.3; @@ -67,77 +77,6 @@ server { } -## -## Working Copy below -------------------------------------------- -## - -server { - - listen 443 ssl; - - ssl_certificate /home/mab/ss/client/certs/192.168.1.164+4.pem; - ssl_certificate_key /home/mab/ss/client/certs/192.168.1.164+4-key.pem; - ssl_session_cache shared:SSL:1m; - ssl_session_timeout 5m; - ssl_protocols TLSV1.1 TLSV1.2 TLSV1.3; - - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - - access_log /var/log/nginx/httpslocalhost.access.log; - error_log /var/log/nginx/httpslocalhost.error.log; - - client_max_body_size 20M; - - location / { - proxy_pass https://127.0.0.1:8081; - proxy_set_header Host localhost; - proxy_set_header X-Forwarded-Host localhost; - proxy_set_header X-Forwarded-Server localhost; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_redirect off; - proxy_connect_timeout 90s; - proxy_read_timeout 90s; - proxy_send_timeout 90s; - proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - } - - location /sockjs-node { - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; - - proxy_pass https://127.0.0.1:8081; - proxy_redirect off; - proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - } - - location /api { - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; - - proxy_pass http://127.0.0.1:3000; - proxy_redirect off; - proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - } - - location /socket { - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; - - proxy_pass http://127.0.0.1:3001; - proxy_redirect off; - proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - } - -} # Prod settings to serve static index diff --git a/server/models/Note.js b/server/models/Note.js index a842648..ad1f97a 100644 --- a/server/models/Note.js +++ b/server/models/Note.js @@ -994,7 +994,8 @@ Note.search = (userId, searchQuery, searchTags, fastFilters, masterKey) => { LEFT JOIN tag ON (tag.id = note_tag.tag_id) LEFT JOIN attachment ON (note.id = attachment.note_id AND attachment.visible = 1) LEFT JOIN user as shareUser ON (note.share_user_id = shareUser.id) - WHERE note.user_id = ? + WHERE note.user_id = ? + AND note.quick_note <= 1 ` //If text search returned results, limit search to those ids