From 8efd8216cf7d3df6eb333e8bedc3243528fd13a9 Mon Sep 17 00:00:00 2001 From: joost-stessens <122776799+joost-stessens@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:51:41 +0200 Subject: [PATCH 01/10] Chore: improve styling with colors & icons --- wearables-dashboard/src/App.tsx | 36 +++++--- .../SleepTimeline/SleepTimeline.tsx | 6 +- .../src/components/Topnav/Topnav.tsx | 83 ++++++++++++++----- 3 files changed, 92 insertions(+), 33 deletions(-) diff --git a/wearables-dashboard/src/App.tsx b/wearables-dashboard/src/App.tsx index 09c0722..868677a 100644 --- a/wearables-dashboard/src/App.tsx +++ b/wearables-dashboard/src/App.tsx @@ -8,6 +8,18 @@ const theme = createTheme({ typography: { fontFamily: "Lato", }, + palette: { + primary: { + main: "#6440EB", + }, + secondary: { + main: "#fefefe", + }, + // background color for the entire app + background: { + default: "#f4f5fd", + }, + }, }); import "./App.css"; @@ -28,17 +40,19 @@ function App() { <> - - - {selectedTab === "analytics" && } - {selectedTab === "settings" && ( - - - - - - )} - + + + + {selectedTab === "analytics" && } + {selectedTab === "settings" && ( + + + + + + )} + + diff --git a/wearables-dashboard/src/components/SleepTimeline/SleepTimeline.tsx b/wearables-dashboard/src/components/SleepTimeline/SleepTimeline.tsx index 4e76173..864a804 100644 --- a/wearables-dashboard/src/components/SleepTimeline/SleepTimeline.tsx +++ b/wearables-dashboard/src/components/SleepTimeline/SleepTimeline.tsx @@ -78,11 +78,11 @@ export function SleepTimeline() { - - + + - + diff --git a/wearables-dashboard/src/components/Topnav/Topnav.tsx b/wearables-dashboard/src/components/Topnav/Topnav.tsx index 1850e00..26d31f4 100644 --- a/wearables-dashboard/src/components/Topnav/Topnav.tsx +++ b/wearables-dashboard/src/components/Topnav/Topnav.tsx @@ -10,20 +10,19 @@ import { Menu, MenuItem, Tooltip, + Typography, } from "@mui/material"; import { AccountCircle } from "@mui/icons-material"; +import DirectionsRunIcon from "@mui/icons-material/DirectionsRun"; +import AnalyticsIcon from "@mui/icons-material/Analytics"; +import DevicesIcon from "@mui/icons-material/Devices"; +import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import SettingsIcon from "@mui/icons-material/Settings"; import "./Topnav.css"; import { mockUsers, useUser } from "../../services/UserService"; const TAB_STYLING = { - "&:hover": { - color: "#fff", - opacity: 0.6, - }, - "&.Mui-selected": { - color: "#fff", - }, "&.Mui-focusVisible": { backgroundColor: "rgba(100, 95, 228, 0.32)", }, @@ -54,17 +53,38 @@ export default function Topnav({ }; return ( - - + + + + + + + Healthosia + + + - - + } + /> + } + disabled + /> + } + disabled + /> + } + /> From b0e5107b3bb20b9811a5d84be7c0316246d5134f Mon Sep 17 00:00:00 2001 From: joost-stessens <122776799+joost-stessens@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:52:19 +0200 Subject: [PATCH 02/10] Chore: update title in HTML --- wearables-dashboard/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wearables-dashboard/index.html b/wearables-dashboard/index.html index 341cd59..f3304aa 100644 --- a/wearables-dashboard/index.html +++ b/wearables-dashboard/index.html @@ -8,7 +8,7 @@ rel="stylesheet" /> - Flex showcase + Healthosia - Luzmo Flex SDK sample application
From ea2e14788c558ccee93bcb4cafa28cb21539b229 Mon Sep 17 00:00:00 2001 From: joost-stessens <122776799+joost-stessens@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:03:18 +0200 Subject: [PATCH 03/10] Chore: remove unused variables + dynamically retrieve font family from theme --- .../src/components/Analytics/Analytics.tsx | 59 ++++++++----------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/wearables-dashboard/src/components/Analytics/Analytics.tsx b/wearables-dashboard/src/components/Analytics/Analytics.tsx index cc355df..c34b7dd 100644 --- a/wearables-dashboard/src/components/Analytics/Analytics.tsx +++ b/wearables-dashboard/src/components/Analytics/Analytics.tsx @@ -38,7 +38,6 @@ import { import { useUser } from "../../services/UserService"; const LUZMO_VIZ_ITEM_SMALL_STYLE = { width: "100%", height: "10rem" }; -const LUZMO_VIZ_ITEM_MEDIUM_STYLE = { width: "100%", height: "20rem" }; const LUZMO_VIZ_ITEM_LARGE_STYLE = { width: "100%", height: "40rem" }; interface TabPanelProps { @@ -81,7 +80,6 @@ export default function Analytics() { const hasSleepData = sleepDevice !== undefined; let stepDatetimeLevel = "day"; - let sleepDatetimeLevel = "day"; if (stepDevice) { stepDatetimeLevel = @@ -91,14 +89,6 @@ export default function Analytics() { ? "hour" : "minute"; } - if (sleepDevice) { - sleepDatetimeLevel = - sleepDevice.intervalInSeconds > 1800 - ? "day" - : sleepDevice.intervalInSeconds > 60 - ? "hour" - : "minute"; - } // Create the necessary filters to filter to the user's data /* @@ -106,35 +96,37 @@ export default function Analytics() { * This is a mock implementation, which performs the filtering on the client side. * In a real-world scenario, the filtering must be done server side in the Authorization request for security purposes! */ - const filtersToApply: FilterGroup[] = [ - { - condition: "and", - filters: [ - // Steps filter on Patient ID column + const filtersToApply: FilterGroup[] = user + ? [ { - expression: "? = ?", - parameters: [ + condition: "and", + filters: [ + // Steps filter on Patient ID column { - column_id: "572740ed-aeed-4aeb-b318-d059e8be35a6", - dataset_id: "1c759996-74fd-438d-bcba-eb58838a5b03", + expression: "? = ?", + parameters: [ + { + column_id: "572740ed-aeed-4aeb-b318-d059e8be35a6", + dataset_id: "1c759996-74fd-438d-bcba-eb58838a5b03", + }, + user.id, + ], }, - user.id, - ], - }, - // Sleep filter on Patient ID column - { - expression: "? = ?", - parameters: [ + // Sleep filter on Patient ID column { - column_id: "695b3aee-6772-4aa6-bb50-a67a88bfc26b", - dataset_id: "f0e0df8c-87fc-4bdc-ab7f-8cd744146284", + expression: "? = ?", + parameters: [ + { + column_id: "695b3aee-6772-4aa6-bb50-a67a88bfc26b", + dataset_id: "f0e0df8c-87fc-4bdc-ab7f-8cd744146284", + }, + user.id, + ], }, - user.id, ], }, - ], - }, - ]; + ] + : []; const handleSleepTabChange = (_: SyntheticEvent, newValue: number) => { setValue(newValue); @@ -145,7 +137,7 @@ export default function Analytics() { ...widgetOptions, theme: { font: { - fontFamily: "Lato", + fontFamily: theme.typography.fontFamily, }, mainColor: theme.palette.primary.main, }, @@ -213,7 +205,6 @@ export default function Analytics() { type="heat-table" style={LUZMO_VIZ_ITEM_LARGE_STYLE} contextId="heat-table-sleep" - canFilter="all" filters={filtersToApply} > From b1a48c7814b628849420be6ff1c530b7dd536049 Mon Sep 17 00:00:00 2001 From: joost-stessens <122776799+joost-stessens@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:03:28 +0200 Subject: [PATCH 04/10] Chore: disable filtering on heat table --- .../src/components/Analytics/sleep/heatTableConsts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wearables-dashboard/src/components/Analytics/sleep/heatTableConsts.ts b/wearables-dashboard/src/components/Analytics/sleep/heatTableConsts.ts index bf0c8e8..e65cd27 100644 --- a/wearables-dashboard/src/components/Analytics/sleep/heatTableConsts.ts +++ b/wearables-dashboard/src/components/Analytics/sleep/heatTableConsts.ts @@ -92,7 +92,7 @@ export const heattableOptions: VizItemOptions = { spacing: null, }, interactivity: { - select: true, + select: false, urlConfig: { target: "_blank", url: null, From 912c29dd7ffb6655456aff2b5fd003a234bcb5b6 Mon Sep 17 00:00:00 2001 From: joost-stessens <122776799+joost-stessens@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:30:17 +0200 Subject: [PATCH 05/10] Chore: slightly improve widget configurations --- .../src/components/Analytics/sleep/areaChartConsts.ts | 2 +- .../components/Analytics/steps/heightNumberWidgetConsts.ts | 2 +- .../src/components/Analytics/steps/stepsNumberWidgetConsts.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/wearables-dashboard/src/components/Analytics/sleep/areaChartConsts.ts b/wearables-dashboard/src/components/Analytics/sleep/areaChartConsts.ts index 88b2063..cd2ed08 100644 --- a/wearables-dashboard/src/components/Analytics/sleep/areaChartConsts.ts +++ b/wearables-dashboard/src/components/Analytics/sleep/areaChartConsts.ts @@ -2,7 +2,7 @@ import { VizItemOptions } from "@luzmo/react-embed"; import { Slots } from "@luzmo/react-embed"; export const areaChartOptions: VizItemOptions = { - interpolation: "step-after", + interpolation: "step-before", interactivity: { brush: true, urlConfig: { diff --git a/wearables-dashboard/src/components/Analytics/steps/heightNumberWidgetConsts.ts b/wearables-dashboard/src/components/Analytics/steps/heightNumberWidgetConsts.ts index 2208198..0ed34c6 100644 --- a/wearables-dashboard/src/components/Analytics/steps/heightNumberWidgetConsts.ts +++ b/wearables-dashboard/src/components/Analytics/steps/heightNumberWidgetConsts.ts @@ -56,7 +56,7 @@ export function getHeightNumberWidgetSlots(datetimeLevel: string): Slot[] { column: "24bf0c70-5464-4131-81af-9281e6d3c4f0", set: "1c759996-74fd-438d-bcba-eb58838a5b03", label: { - en: `Height meters per ${datetimeLevel}`, + en: `Height meters climbed per ${datetimeLevel}`, }, type: "numeric", format: ",.3as", diff --git a/wearables-dashboard/src/components/Analytics/steps/stepsNumberWidgetConsts.ts b/wearables-dashboard/src/components/Analytics/steps/stepsNumberWidgetConsts.ts index d4ab0b0..ff4d5bb 100644 --- a/wearables-dashboard/src/components/Analytics/steps/stepsNumberWidgetConsts.ts +++ b/wearables-dashboard/src/components/Analytics/steps/stepsNumberWidgetConsts.ts @@ -12,7 +12,7 @@ export function getStepNumberWidgetSlots(datetimeLevel: string): Slot[] { column: "27978dfd-7218-40a4-88cf-59de2df91935", set: "1c759996-74fd-438d-bcba-eb58838a5b03", label: { - en: `Number of steps by ${datetimeLevel}`, + en: `Number of steps per ${datetimeLevel}`, }, type: "numeric", format: ",.3as", @@ -81,7 +81,7 @@ export const stepNumberWidgetSlots: Slot[] = [ column: "27978dfd-7218-40a4-88cf-59de2df91935", set: "1c759996-74fd-438d-bcba-eb58838a5b03", label: { - en: "Number of steps by day", + en: "Total steps per day", }, type: "numeric", format: ",.3as", From ce797623d83b323b67252892fe31e3d3acac8869 Mon Sep 17 00:00:00 2001 From: joost-stessens <122776799+joost-stessens@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:30:49 +0200 Subject: [PATCH 06/10] Chore: add icons to user devices --- .../src/components/Settings/UserDevices.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/wearables-dashboard/src/components/Settings/UserDevices.tsx b/wearables-dashboard/src/components/Settings/UserDevices.tsx index c9b930f..7f021f4 100644 --- a/wearables-dashboard/src/components/Settings/UserDevices.tsx +++ b/wearables-dashboard/src/components/Settings/UserDevices.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Card, CardContent, @@ -6,9 +5,12 @@ import { List, ListItem, ListItemText, - ListSubheader, + ListItemIcon, Divider, } from "@mui/material"; +import DirectionsWalkIcon from "@mui/icons-material/DirectionsWalk"; +import NightsStayIcon from "@mui/icons-material/NightsStay"; +import DeviceUnknownIcon from "@mui/icons-material/DeviceUnknown"; import { User } from "../../types/types"; @@ -38,18 +40,19 @@ export const UserDevices = ({ user }: { user: User }) => { - {user.name}'s Devices + Your devices - - Devices - - } - > + {user.devices.map((device, index) => ( + + {device.type === "sleep" && } + {device.type === "steps" && } + {!["steps", "sleep"].includes(device.type) && ( + + )} + Date: Mon, 2 Sep 2024 11:31:50 +0200 Subject: [PATCH 07/10] Chore: improve topnav styling + add mock navigation elements --- .../src/components/Topnav/Topnav.css | 3 + .../src/components/Topnav/Topnav.tsx | 55 ++++++++++++++++--- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/wearables-dashboard/src/components/Topnav/Topnav.css b/wearables-dashboard/src/components/Topnav/Topnav.css index e69de29..f1f88ec 100644 --- a/wearables-dashboard/src/components/Topnav/Topnav.css +++ b/wearables-dashboard/src/components/Topnav/Topnav.css @@ -0,0 +1,3 @@ +.topnav { + padding-right: 0 !important; +} diff --git a/wearables-dashboard/src/components/Topnav/Topnav.tsx b/wearables-dashboard/src/components/Topnav/Topnav.tsx index 26d31f4..cff4ab1 100644 --- a/wearables-dashboard/src/components/Topnav/Topnav.tsx +++ b/wearables-dashboard/src/components/Topnav/Topnav.tsx @@ -21,11 +21,14 @@ import SettingsIcon from "@mui/icons-material/Settings"; import "./Topnav.css"; import { mockUsers, useUser } from "../../services/UserService"; +import { User } from "../../types/types"; +import { useTheme } from "@mui/material/styles"; const TAB_STYLING = { "&.Mui-focusVisible": { backgroundColor: "rgba(100, 95, 228, 0.32)", }, + minWidth: 60, }; export default function Topnav({ @@ -36,11 +39,10 @@ export default function Topnav({ onTabChange: (tab: string) => void; }) { const { user, switchUser } = useUser(); - // const [value, setValue] = useState(selectedTab); + const theme = useTheme(); const [anchorEl, setAnchorEl] = useState(null); const handleChange = (_: SyntheticEvent, newValue: string) => { - // setValue(newValue); onTabChange(newValue); }; @@ -52,15 +54,21 @@ export default function Topnav({ setAnchorEl(null); }; + const handleUserChange = (user: User) => { + handleClose(); + switchUser(user); + }; + return ( @@ -100,32 +108,63 @@ export default function Topnav({ width: "100%", backgroundColor: "#635ee7", }, + ".MuiTabs-flexContainer": { + flexWrap: "wrap", + }, }} centered={true} > + Analytics +
+ } sx={TAB_STYLING} icon={} /> + Connect + + } sx={TAB_STYLING} icon={} disabled /> + Upload + + } sx={TAB_STYLING} icon={} disabled /> + Settings + + } sx={TAB_STYLING} icon={} /> @@ -162,7 +201,7 @@ export default function Topnav({ {mockUsers.map((u) => ( switchUser(u)} + onClick={() => handleUserChange(u)} sx={{ backgroundColor: u.id === user?.id ? "#e5e5e5" : "inherit", }} From 4d624b96bf378d6eea7ac99bea2a8c66c70d1174 Mon Sep 17 00:00:00 2001 From: joost-stessens <122776799+joost-stessens@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:32:16 +0200 Subject: [PATCH 08/10] Chore: add custom breakpoints to theme --- wearables-dashboard/src/App.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/wearables-dashboard/src/App.tsx b/wearables-dashboard/src/App.tsx index b8642a0..b326b8e 100644 --- a/wearables-dashboard/src/App.tsx +++ b/wearables-dashboard/src/App.tsx @@ -25,6 +25,15 @@ const theme = createTheme({ default: "#f4f5fd", }, }, + breakpoints: { + values: { + xs: 0, + sm: 900, + md: 1200, + lg: 1536, + xl: 1920, + }, + }, }); import "./App.css"; From 9c6ad174f27a20ab42d9f2f8094cc1765fb57a1a Mon Sep 17 00:00:00 2001 From: joost-stessens <122776799+joost-stessens@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:34:51 +0200 Subject: [PATCH 09/10] Feat: improve UI layout + add circle gauge + make timeline interactive + add textual insights --- wearables-dashboard/package.json | 1 + .../src/components/Analytics/Analytics.tsx | 362 +++++++++++++----- .../Analytics/sleep/circleGaugeConsts.ts | 28 ++ .../SleepTimeline/SleepTimeline.tsx | 52 ++- .../src/services/DataService.ts | 165 ++++++++ 5 files changed, 500 insertions(+), 108 deletions(-) create mode 100644 wearables-dashboard/src/components/Analytics/sleep/circleGaugeConsts.ts create mode 100644 wearables-dashboard/src/services/DataService.ts diff --git a/wearables-dashboard/package.json b/wearables-dashboard/package.json index 458a146..af280d3 100644 --- a/wearables-dashboard/package.json +++ b/wearables-dashboard/package.json @@ -17,6 +17,7 @@ "@luzmo/react-embed": "next", "@mui/icons-material": "^5.16.4", "@mui/lab": "^5.0.0-alpha.172", + "axios": "^1.7.7", "react": "^18.3.1", "react-dom": "^18.3.1", "userflow.js": "^2.12.1" diff --git a/wearables-dashboard/src/components/Analytics/Analytics.tsx b/wearables-dashboard/src/components/Analytics/Analytics.tsx index c34b7dd..6835db5 100644 --- a/wearables-dashboard/src/components/Analytics/Analytics.tsx +++ b/wearables-dashboard/src/components/Analytics/Analytics.tsx @@ -1,4 +1,5 @@ -import { Grid, Box, Tabs, Tab } from "@mui/material"; +import { useState, SyntheticEvent, useEffect } from "react"; +import { Grid, Box, Tabs, Tab, Typography, Stack, Paper } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import { @@ -7,6 +8,8 @@ import { VizItemOptions, } from "@luzmo/react-embed"; +import { useUser } from "../../services/UserService"; + import { SleepTimeline } from "../SleepTimeline/SleepTimeline"; import { heattableOptions, heatTableSlots } from "./sleep/heatTableConsts"; @@ -28,18 +31,27 @@ import { areaChartOptions as stepsAreaChartOptions, areaChartSlots as stepsAreaChartSlots, } from "./steps/areaChartConsts"; +import { + circleGaugeOptions, + circleGaugeSlots, +} from "./sleep/circleGaugeConsts"; import "./Analytics.css"; -import { useState, SyntheticEvent } from "react"; import { - numberWidgetOptions, - numberWidgetSlots, -} from "./sleep/numberWidgetConsts"; -import { useUser } from "../../services/UserService"; + getSleepScoreLastNight, + getStepsPerDay, +} from "../../services/DataService"; const LUZMO_VIZ_ITEM_SMALL_STYLE = { width: "100%", height: "10rem" }; +const LUZMO_VIZ_ITEM_MEDIUM_STYLE = { width: "100%", height: "20rem" }; const LUZMO_VIZ_ITEM_LARGE_STYLE = { width: "100%", height: "40rem" }; +const TEXTUAL_INSIGHTS_STYLE = { + textAlign: "center", + paddingY: "0.5rem", + paddingX: "0.2rem", +}; + interface TabPanelProps { children?: React.ReactNode; index: number; @@ -63,11 +75,67 @@ function CustomTabPanel(props: TabPanelProps) { } export default function Analytics() { + const { user } = useUser(); + const theme = useTheme(); const [value, setValue] = useState(0); + const [sleepScoreYesterday, setSleepScoreYesterday] = useState< + number | undefined + >(undefined); + const [sleepScoreDayBefore, setSleepScoreDayBefore] = useState< + number | undefined + >(undefined); + const [averageKcalBurned, setAverageKcalBurned] = useState< + number | undefined + >(undefined); + const [stepsToday, setStepsToday] = useState(undefined); - const theme = useTheme(); + useEffect(() => { + if (!user) return; - const { user } = useUser(); + setSleepScoreYesterday(undefined); + + // Fetch sleep score from yesterday + getSleepScoreLastNight(user.id).then((sleepScores) => { + setSleepScoreYesterday( + sleepScores.lastNightSleepScore > -1 + ? Math.round(sleepScores.lastNightSleepScore * 100) + : undefined + ); + + setSleepScoreDayBefore( + sleepScores.previousNightSleepScore > -1 + ? Math.round(sleepScores.previousNightSleepScore * 100) + : undefined + ); + }); + }, [user]); + + useEffect(() => { + if (!user) return; + + setStepsToday(undefined); + + // Fetch sleep score from yesterday + getStepsPerDay(user.id).then((stepsPerDay) => { + // Check if the first entry is from today & set the steps + setStepsToday( + stepsPerDay[0].date === + new Date(new Date().toISOString().split("T")[0]).toISOString() + ? stepsPerDay[0].steps + : 0 + ); + + // Calculate the average steps per day to estimate the kcal burned + const totalSteps = stepsPerDay.reduce( + (acc, current) => acc + current.steps, + 0 + ); + + setAverageKcalBurned( + Math.round((totalSteps / stepsPerDay.length) * 0.04) + ); + }); + }, [user]); const stepDevice = user ? user.devices.find((d) => d.type === "steps") @@ -84,12 +152,26 @@ export default function Analytics() { if (stepDevice) { stepDatetimeLevel = stepDevice.intervalInSeconds > 1800 - ? "day" + ? "week" : stepDevice.intervalInSeconds > 60 - ? "hour" - : "minute"; + ? "day" + : "hour"; } + const hourOfDay = new Date().getHours(); + const timeOfDay = + hourOfDay > 6 && hourOfDay < 12 + ? "morning" + : hourOfDay >= 12 && hourOfDay < 18 + ? "afternoon" + : hourOfDay < 22 + ? "evening" + : "night"; + + const [defaultTab, setDefaultTab] = useState( + hasSleepData ? (hourOfDay < 15 ? 1 : 0) : 0 + ); + // Create the necessary filters to filter to the user's data /* * NOTE: @@ -158,67 +240,158 @@ export default function Analytics() { <> {/* Create Luzmo number evolution widget with steps info */} {hasStepsData && ( - - - - )} - {hasSleepData && ( - <> - {/* Create Luzmo number evolution widget with sleep info */} - + + + + Good {timeOfDay} {user?.name}! + + + {hasSleepData && sleepScoreYesterday && sleepScoreDayBefore && ( + + Your sleep score was {sleepScoreYesterday}%{" "} + last night, which is{" "} + {sleepScoreDayBefore > sleepScoreYesterday + ? "less than" + : sleepScoreDayBefore < sleepScoreYesterday + ? "more than" + : "equal to"}{" "} + the night before ({sleepScoreDayBefore}%). + + )} + {hasSleepData && !sleepScoreYesterday && ( + + Loading sleep insights... + + )} + {hasStepsData && stepsToday && ( + + Based on your walking history, you should have used about{" "} + {averageKcalBurned} kcal by the end of today + (currently at {stepsToday} steps today). + + )} + {hasStepsData && !stepsToday && ( + + Loading step insights... + + )} + + + + + + )} + {hasSleepData && ( + <> + {/* Create Luzmo circular gauge widget with sleep info */} + + + + {/* Create sleep timeline */} - - + + + + {/* Create Luzmo sleep insights with custom tabs element */} + - - - - - - - - - - - - + + {defaultTab === 0 && ( + + )} + {defaultTab === 1 && ( + <> + + + + + + + + + + + + + + + + + + )} + )} @@ -226,37 +399,44 @@ export default function Analytics() { {hasStepsData && ( <> - + + + - - - - {/* Create Luzmo line chart with steps info */} - + + + + {!hasSleepData && ( + + + + + + )} )} diff --git a/wearables-dashboard/src/components/Analytics/sleep/circleGaugeConsts.ts b/wearables-dashboard/src/components/Analytics/sleep/circleGaugeConsts.ts new file mode 100644 index 0000000..aa4b549 --- /dev/null +++ b/wearables-dashboard/src/components/Analytics/sleep/circleGaugeConsts.ts @@ -0,0 +1,28 @@ +import { VizItemOptions } from "@luzmo/react-embed"; +import { Slots } from "@luzmo/react-embed"; + +export const circleGaugeOptions: VizItemOptions = { + circle: { + degrees: 360, + flip: false, + }, +}; + +export const circleGaugeSlots: Slots = [ + { + name: "measure", + content: [ + { + column: "d0ff1497-fa36-4dce-9480-a03e2f65c5ee", + set: "f0e0df8c-87fc-4bdc-ab7f-8cd744146284", + label: { + en: "Average sleep score", + }, + type: "numeric", + + format: ",.0a%", + aggregationFunc: "average", + }, + ], + }, +]; diff --git a/wearables-dashboard/src/components/SleepTimeline/SleepTimeline.tsx b/wearables-dashboard/src/components/SleepTimeline/SleepTimeline.tsx index 864a804..33d6187 100644 --- a/wearables-dashboard/src/components/SleepTimeline/SleepTimeline.tsx +++ b/wearables-dashboard/src/components/SleepTimeline/SleepTimeline.tsx @@ -8,14 +8,6 @@ import { TimelineOppositeContent, TimelineDot, } from "@mui/lab"; - -// import Timeline from "@mui/lab/Timeline"; -// import TimelineItem from "@mui/lab/TimelineItem"; -// import TimelineSeparator from "@mui/lab/TimelineSeparator"; -// import TimelineConnector from "@mui/lab/TimelineConnector"; -// import TimelineContent from "@mui/lab/TimelineContent"; -// import TimelineOppositeContent from "@mui/lab/TimelineOppositeContent"; -// import TimelineDot from "@mui/lab/TimelineDot"; import FastfoodIcon from "@mui/icons-material/Fastfood"; import LaptopMacIcon from "@mui/icons-material/LaptopMac"; import HotelIcon from "@mui/icons-material/Hotel"; @@ -28,7 +20,15 @@ const TIMELINE_ITEM_SX = { const TIMELINE_CONTENT_SX = { py: "12px", px: 2, alignSelf: "center" }; -export function SleepTimeline() { +type SLEEP_TIMELINE_PROPS = { + selected: number; + onSelectedChange: (newSelected: number) => void; +}; + +export function SleepTimeline({ + selected, + onSelectedChange, +}: SLEEP_TIMELINE_PROPS) { return ( @@ -41,11 +41,20 @@ export function SleepTimeline() { 9:30 am - - + + onSelectedChange(0)} + sx={{ cursor: selected === 0 ? "default" : "pointer" }} + > - + @@ -64,7 +73,7 @@ export function SleepTimeline() { - + @@ -78,11 +87,20 @@ export function SleepTimeline() { - - + + onSelectedChange(1)} + sx={{ cursor: selected === 1 ? "default" : "pointer" }} + > - + @@ -94,7 +112,7 @@ export function SleepTimeline() { - + diff --git a/wearables-dashboard/src/services/DataService.ts b/wearables-dashboard/src/services/DataService.ts new file mode 100644 index 0000000..6c6264f --- /dev/null +++ b/wearables-dashboard/src/services/DataService.ts @@ -0,0 +1,165 @@ +import axios from "axios"; + +const API_BASE_URL = "https://api.luzmo.com/0.1.0"; + +export type SleepScoreData = { + lastNightSleepScore: number; + previousNightSleepScore: number; +}; + +export type StepsPerDayData = { + date: string; + steps: number; +}[]; + +export async function getSleepScoreLastNight( + userId: string +): Promise { + const sleepScoreQuery = { + dimensions: [ + // Group on day level + { + dataset_id: "f0e0df8c-87fc-4bdc-ab7f-8cd744146284", + column_id: "3fc71210-6362-4bf6-ba1b-d27d16b1c36d", + level: 5, + }, + ], + measures: [ + // Calculate average sleep score + { + dataset_id: "f0e0df8c-87fc-4bdc-ab7f-8cd744146284", + column_id: "d0ff1497-fa36-4dce-9480-a03e2f65c5ee", + aggregation: { + type: "average", + }, + }, + ], + where: [ + // Filter by user ID + /* IMPORTANT NOTE: + * This is a simplified example that doesn't use authorization tokens. In a real-world application, you should never pass the user ID directly to the API client-side, as it can be easily manipulated. + * Instead, always use a user-specific Authorization token that handles the multi-tenancy filtering, and use this key-token pair to retrieve the data. + */ + { + expression: "? = ?", + parameters: [ + { + column_id: "695b3aee-6772-4aa6-bb50-a67a88bfc26b", + dataset_id: "f0e0df8c-87fc-4bdc-ab7f-8cd744146284", + }, + userId, + ], + }, + ], + order: [ + // Order by day in descending order + { + dataset_id: "f0e0df8c-87fc-4bdc-ab7f-8cd744146284", + column_id: "3fc71210-6362-4bf6-ba1b-d27d16b1c36d", + level: 5, + order: "desc", + }, + ], + limit: { + by: 2, + }, + }; + try { + const response = await axios.post(`${API_BASE_URL}/data`, { + action: "get", + version: "0.1.0", + find: { queries: [sleepScoreQuery] }, + }); + const results = response.data.data; + const output = { + lastNightSleepScore: -1, + previousNightSleepScore: -1, + }; + const currentDate = new Date().toISOString().split("T")[0]; + + const previousDate = new Date(currentDate); + previousDate.setDate(new Date(currentDate).getDate() - 1); + + const dateBeforePreviousDate = new Date(previousDate); + dateBeforePreviousDate.setDate(new Date(previousDate).getDate() - 1); + + results.forEach((result: any) => { + const date = result[0].split("T")[0]; + if ( + new Date(previousDate).toISOString() === new Date(date).toISOString() + ) { + output.lastNightSleepScore = result[1]; + } else if ( + new Date(dateBeforePreviousDate).toISOString() === + new Date(date).toISOString() + ) { + output.previousNightSleepScore = result[1]; + } else { + console.log("Date received is not yesterday or the day before:", date); + } + }); + + return output; + } catch (error) { + console.error("Error fetching sleep score:", error); + throw error; + } +} + +export async function getStepsPerDay(userId: string): Promise { + const stepsPerDayQuery = { + dimensions: [ + // Group on day level + { + dataset_id: "1c759996-74fd-438d-bcba-eb58838a5b03", + column_id: "87680fa2-96d2-42cf-ae76-557c3996cdf2", + level: 5, + }, + ], + measures: [ + // Calculate total steps (default aggregation is sum) + { + dataset_id: "1c759996-74fd-438d-bcba-eb58838a5b03", + column_id: "27978dfd-7218-40a4-88cf-59de2df91935", + }, + ], + where: [ + { + expression: "? = ?", + parameters: [ + { + column_id: "572740ed-aeed-4aeb-b318-d059e8be35a6", + dataset_id: "1c759996-74fd-438d-bcba-eb58838a5b03", + }, + userId, + ], + }, + ], + order: [ + // Order by day in descending order + { + dataset_id: "1c759996-74fd-438d-bcba-eb58838a5b03", + column_id: "87680fa2-96d2-42cf-ae76-557c3996cdf2", + level: 5, + order: "desc", + }, + ], + }; + try { + const response = await axios.post(`${API_BASE_URL}/data`, { + action: "get", + version: "0.1.0", + find: { queries: [stepsPerDayQuery] }, + }); + + return response.data.data.map((result: any) => { + return { + date: result[0], + steps: result[1], + }; + }); + } catch (error) { + console.error("Error fetching steps per day:", error); + throw error; + } +} From e17c94c8bba9a732911b3a41f5c032b6a1addbb9 Mon Sep 17 00:00:00 2001 From: joost-stessens <122776799+joost-stessens@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:36:48 +0200 Subject: [PATCH 10/10] Chore: push lock files --- wearables-dashboard/bun.lockb | Bin 123905 -> 132995 bytes wearables-dashboard/package-lock.json | 100 ++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/wearables-dashboard/bun.lockb b/wearables-dashboard/bun.lockb index 867d4628a172235fccd6ef32adaf159d05d4809e..46897f293019392d383f57094705690c6ebd9f46 100644 GIT binary patch delta 28055 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 diff --git a/wearables-dashboard/package-lock.json b/wearables-dashboard/package-lock.json index 5a6b459..261fea2 100644 --- a/wearables-dashboard/package-lock.json +++ b/wearables-dashboard/package-lock.json @@ -13,6 +13,7 @@ "@luzmo/react-embed": "next", "@mui/icons-material": "^5.16.4", "@mui/lab": "^5.0.0-alpha.172", + "axios": "^1.7.7", "react": "^18.3.1", "react-dom": "^18.3.1", "userflow.js": "^2.12.1" @@ -2060,6 +2061,23 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -2195,6 +2213,18 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2263,6 +2293,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2768,6 +2807,40 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3221,6 +3294,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -3472,6 +3566,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",