From cfd45ddd3c2150823f441f77deeefcdd2419ccf1 Mon Sep 17 00:00:00 2001 From: Ore Ogundipe Date: Mon, 26 Aug 2024 16:27:45 -0700 Subject: [PATCH] feat: allow people to connect to activity watch and download screen time events as csv --- .../components/features/integrations/data.ts | 18 +- .../integrations/integrations.container.tsx | 18 +- .../modals/activitywatch-modal.tsx | 186 ++++++++++++++---- frontend/src/pages/integrations.tsx | 2 +- frontend/src/services/storage.service.ts | 12 +- 5 files changed, 181 insertions(+), 55 deletions(-) diff --git a/frontend/src/components/features/integrations/data.ts b/frontend/src/components/features/integrations/data.ts index 8d8b4adc..bc1717c2 100644 --- a/frontend/src/components/features/integrations/data.ts +++ b/frontend/src/components/features/integrations/data.ts @@ -48,15 +48,15 @@ export const integrations = [ // description: "MagicFlow is the productivity tracker that helps you focus on what matters most.", // active: true, // }, - // { - // slug: "activityWatch", - // title: "ActivityWatch (Screen Time)", - // href: "https://activitywatch.net", - // imageUrl: "/images/integrations/activitywatch_icon.png", - // description: - // "ActivityWatch is an open source, privacy first app that automatically tracks how you spend time on your devices.", - // active: true, - // }, + { + slug: "activityWatch", + title: "ActivityWatch (Screen Time)", + href: "https://activitywatch.net", + imageUrl: "/images/integrations/activitywatch_icon.png", + description: + "ActivityWatch is an open source, privacy first app that automatically tracks how you spend time on your devices.", + active: true, + }, // { // slug: "spotify", // title: "Spotify (Music)", diff --git a/frontend/src/components/features/integrations/integrations.container.tsx b/frontend/src/components/features/integrations/integrations.container.tsx index e8587b81..08058d87 100644 --- a/frontend/src/components/features/integrations/integrations.container.tsx +++ b/frontend/src/components/features/integrations/integrations.container.tsx @@ -23,6 +23,15 @@ export const IntegrationsContainer = () => { const museContext = useContext(MuseContext); + const [activityWatchConnected, setActivityWatchConnected] = useState(false); + + useEffect(() => { + // Check ActivityWatch connection status after component mounts + const isActivityWatchConnected = + typeof window !== "undefined" && Boolean(localStorage?.getItem("selectedActivityWatchHost")); + setActivityWatchConnected(isActivityWatchConnected); + }, []); + function handleIntegrationClick(integrationSlug: IntegrationSlug) { switch (integrationSlug) { case "fusion": @@ -71,7 +80,7 @@ export const IntegrationsContainer = () => { return Boolean(magicflowData?.magicflowToken); case "activityWatch": // check if activityWatch is connected - return false; + return activityWatchConnected; case "muse": // call function for muse integration return Boolean(museContext?.museClient?.connectionStatus); @@ -111,16 +120,11 @@ export const IntegrationsContainer = () => { } }, [neurosityLoading, user]); - // useEffect(() => { - // if (!museContext.museClient) { - // museContext.connectMuse(); - // } - // }, [user]); return (

Integrations and all connected apps

- Connect to applications and sensors for context on your life experiences{" "} + Connect to applications and sensors for context on when life events happen{" "}

{integrations.map((integration) => ( diff --git a/frontend/src/components/features/integrations/modals/activitywatch-modal.tsx b/frontend/src/components/features/integrations/modals/activitywatch-modal.tsx index 023a895c..5d2ffe45 100644 --- a/frontend/src/components/features/integrations/modals/activitywatch-modal.tsx +++ b/frontend/src/components/features/integrations/modals/activitywatch-modal.tsx @@ -1,13 +1,15 @@ import * as Accordion from "@radix-ui/react-accordion"; import * as Dialog from "@radix-ui/react-dialog"; -import { ChevronDown, X } from "lucide-react"; +import { ChevronDown, Download, RefreshCw, X } from "lucide-react"; import Image from "next/image"; import { useSession } from "next-auth/react"; -import { FC, useState } from "react"; -import { AWClient } from "aw-client"; +import { FC, useEffect, useState } from "react"; +import { AWClient, IBucket } from "aw-client"; import { Button, Input } from "~/components/ui"; import { activityWatchSteps } from "../data"; +import dayjs from "dayjs"; +import { writeDataToStore } from "~/services/storage.service"; interface IActivityWatchModalProps { isOpen: boolean; @@ -15,18 +17,47 @@ interface IActivityWatchModalProps { } export const ActivityWatchModal: FC = ({ isOpen, onCloseModal }) => { - const { data: sessionData } = useSession(); - const [hostname, setHostname] = useState(""); + const [hostList, setHostList] = useState<{ [bucketId: string]: IBucket }>({}); + const [selectedHost, setSelectedHost] = useState(""); - const handleSubmit = () => { - // make the aw cient calls here + const [startDate, setStartDate] = useState(dayjs().subtract(1, "month")); + const [endDate, setEndDate] = useState(dayjs()); + + const fetchHosts = () => { const awClient = new AWClient("fusionClient"); awClient.getBuckets().then((buckets) => { - console.log(buckets); + setHostList(buckets); }); }; - const handleChange = (evt: React.ChangeEvent) => setHostname(evt.target.value); + const fetchData = async (startDate: dayjs.Dayjs, endDate: dayjs.Dayjs) => { + // make the aw cient calls here + const awClient = new AWClient("fusionClient"); + const events = await awClient.getEvents(selectedHost, { + start: startDate.toDate(), + end: endDate.toDate(), + }); + return events; + }; + + useEffect(() => { + fetchHosts(); + }, []); + + // Save selectedHost to local storage whenever it changes + useEffect(() => { + if (selectedHost) { + localStorage.setItem("selectedActivityWatchHost", selectedHost); + } + }, [selectedHost]); + + // Load selectedHost from local storage on component mount + useEffect(() => { + const savedHost = localStorage.getItem("selectedActivityWatchHost"); + if (savedHost && hostList[savedHost]) { + setSelectedHost(savedHost); + } + }, [hostList]); return ( @@ -37,10 +68,10 @@ export const ActivityWatchModal: FC = ({ isOpen, onClo /> - Connect ActivityWatch data + Connect ActivityWatch - Enter your hostname to fetch existing data + Choose your ActivityWatch host to fetch your screentime data = ({ isOpen, onClo Close -

Steps

- - {activityWatchSteps.map((step) => ( - - -
-
- {step.id} -
-

{step.step}

-
- - -
- - {/* Magic Flow Home page */} - -
- ))} - {/* TODO: Update activity watch steps and include it here */} - {/* we should actually fetch the buckets */} -
+ {!hostList && ( +
+

Steps

+ + {activityWatchSteps.map((step) => ( + + +
+
+ {step.id} +
+

{step.step}

+
+ + +
+ + {/* Magic Flow Home page */} + +
+ ))} + {/* TODO: Update activity watch steps and include it here */} + {/* we should actually fetch the buckets */} +
+
+ )}
- {/* */} - + + {hostList && ( +
+ + +
+ )} +
+ +
+

Fetch Screentime Data

+ +
+
+ + setStartDate(dayjs(e.target.value))} + /> +
+
+ + setEndDate(dayjs(e.target.value))} + /> +
+
+ +
+ +
diff --git a/frontend/src/pages/integrations.tsx b/frontend/src/pages/integrations.tsx index 1cab1d75..ae7a8e4c 100644 --- a/frontend/src/pages/integrations.tsx +++ b/frontend/src/pages/integrations.tsx @@ -11,7 +11,7 @@ const IntegrationsPage: NextPage = () => { diff --git a/frontend/src/services/storage.service.ts b/frontend/src/services/storage.service.ts index a847a1ea..1a0b6b32 100644 --- a/frontend/src/services/storage.service.ts +++ b/frontend/src/services/storage.service.ts @@ -12,7 +12,15 @@ export function convertToCSV(arr: any[]) { return array .map((it) => { - return Object.values(it).toString(); + return Object.values(it) + .map((value) => { + if (typeof value === "string" && value.includes(",")) { + // Escape commas and wrap in quotes + return `"${value.replace(/"/g, '""')}"`; + } + return value; + }) + .join(","); }) .join("\n"); } @@ -28,7 +36,7 @@ export async function writeDataToStore(dataName: string, data: any, fileTimestam const content = convertToCSV(data); // convert to csv format const hiddenElement = document.createElement("a"); - hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURI(content)}`; + hiddenElement.href = `data:text/csv;charset=utf-8,${encodeURIComponent(content)}`; hiddenElement.target = "_blank"; hiddenElement.download = fileName; hiddenElement.click();