Use server actions over swr for retrieving data

This commit is contained in:
Joshua Coles 2024-02-11 17:20:41 +00:00
parent b82c8fe979
commit d8ad2f80b8
7 changed files with 169 additions and 215 deletions

116
package-lock.json generated
View File

@ -13,7 +13,7 @@
"@types/ramda": "^0.29.9", "@types/ramda": "^0.29.9",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"heat-calendar": "^1.0.7", "heat-calendar": "^1.0.7",
"next": "14.0.4", "next": "^14.1.0",
"ramda": "^0.29.1", "ramda": "^0.29.1",
"react": "^18", "react": "^18",
"react-calendar-heatmap": "^1.9.0", "react-calendar-heatmap": "^1.9.0",
@ -1441,9 +1441,9 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "14.0.4", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz",
"integrity": "sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==" "integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw=="
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
"version": "14.0.4", "version": "14.0.4",
@ -1455,9 +1455,9 @@
} }
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "14.0.4", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz",
"integrity": "sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==", "integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1470,9 +1470,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "14.0.4", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz",
"integrity": "sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==", "integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1485,9 +1485,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.0.4", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz",
"integrity": "sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==", "integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1500,9 +1500,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "14.0.4", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz",
"integrity": "sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==", "integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1515,9 +1515,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "14.0.4", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz",
"integrity": "sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==", "integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1530,9 +1530,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "14.0.4", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz",
"integrity": "sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==", "integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1545,9 +1545,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.0.4", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz",
"integrity": "sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==", "integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1560,9 +1560,9 @@
} }
}, },
"node_modules/@next/swc-win32-ia32-msvc": { "node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.0.4", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz",
"integrity": "sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==", "integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -1575,9 +1575,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "14.0.4", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz",
"integrity": "sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==", "integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3162,9 +3162,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001570", "version": "1.0.30001585",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001585.tgz",
"integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", "integrity": "sha512-yr2BWR1yLXQ8fMpdS/4ZZXpseBgE7o4g41x3a6AJOqZuOi+iE/WdJYAuZ6Y95i4Ohd2Y+9MzIWRR+uGABH4s3Q==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -5103,11 +5103,6 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
},
"node_modules/globals": { "node_modules/globals": {
"version": "13.24.0", "version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@ -7132,18 +7127,17 @@
"dev": true "dev": true
}, },
"node_modules/next": { "node_modules/next": {
"version": "14.0.4", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/next/-/next-14.0.4.tgz", "resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz",
"integrity": "sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==", "integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==",
"dependencies": { "dependencies": {
"@next/env": "14.0.4", "@next/env": "14.1.0",
"@swc/helpers": "0.5.2", "@swc/helpers": "0.5.2",
"busboy": "1.6.0", "busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406", "caniuse-lite": "^1.0.30001579",
"graceful-fs": "^4.2.11", "graceful-fs": "^4.2.11",
"postcss": "8.4.31", "postcss": "8.4.31",
"styled-jsx": "5.1.1", "styled-jsx": "5.1.1"
"watchpack": "2.4.0"
}, },
"bin": { "bin": {
"next": "dist/bin/next" "next": "dist/bin/next"
@ -7152,15 +7146,15 @@
"node": ">=18.17.0" "node": ">=18.17.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "14.0.4", "@next/swc-darwin-arm64": "14.1.0",
"@next/swc-darwin-x64": "14.0.4", "@next/swc-darwin-x64": "14.1.0",
"@next/swc-linux-arm64-gnu": "14.0.4", "@next/swc-linux-arm64-gnu": "14.1.0",
"@next/swc-linux-arm64-musl": "14.0.4", "@next/swc-linux-arm64-musl": "14.1.0",
"@next/swc-linux-x64-gnu": "14.0.4", "@next/swc-linux-x64-gnu": "14.1.0",
"@next/swc-linux-x64-musl": "14.0.4", "@next/swc-linux-x64-musl": "14.1.0",
"@next/swc-win32-arm64-msvc": "14.0.4", "@next/swc-win32-arm64-msvc": "14.1.0",
"@next/swc-win32-ia32-msvc": "14.0.4", "@next/swc-win32-ia32-msvc": "14.1.0",
"@next/swc-win32-x64-msvc": "14.0.4" "@next/swc-win32-x64-msvc": "14.1.0"
}, },
"peerDependencies": { "peerDependencies": {
"@opentelemetry/api": "^1.1.0", "@opentelemetry/api": "^1.1.0",
@ -9760,18 +9754,6 @@
"makeerror": "1.0.12" "makeerror": "1.0.12"
} }
}, },
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",

View File

@ -15,7 +15,7 @@
"@types/ramda": "^0.29.9", "@types/ramda": "^0.29.9",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"heat-calendar": "^1.0.7", "heat-calendar": "^1.0.7",
"next": "14.0.4", "next": "^14.1.0",
"ramda": "^0.29.1", "ramda": "^0.29.1",
"react": "^18", "react": "^18",
"react-calendar-heatmap": "^1.9.0", "react-calendar-heatmap": "^1.9.0",

View File

@ -2,10 +2,13 @@ import {SubjectComparisonCard} from "@/app/cards/subjectComparisonCard";
import {CalendarOverviewCard} from "@/app/cards/calendarOverviewCard"; import {CalendarOverviewCard} from "@/app/cards/calendarOverviewCard";
import {OverviewConfig} from "@/app/overviewConfig"; import {OverviewConfig} from "@/app/overviewConfig";
import {SubjectOverviewCard} from "@/app/cards/subjectOverviewCard"; import {SubjectOverviewCard} from "@/app/cards/subjectOverviewCard";
import {getData} from "@/app/fetchData";
export default function OverviewPage({config}: { export default async function OverviewPage({config}: {
config: OverviewConfig config: OverviewConfig
}) { }) {
const data = await getData(config);
const projectIds = config.subjects.map((subject) => subject.projectId); const projectIds = config.subjects.map((subject) => subject.projectId);
return ( return (
@ -18,23 +21,17 @@ export default function OverviewPage({config}: {
key={subject.projectId} key={subject.projectId}
projectId={subject.projectId} projectId={subject.projectId}
title={subject.title} title={subject.title}
startTime={config.timePeriod.start} data={data}
endTime={config.timePeriod.end}
/> />
))} ))}
<CalendarOverviewCard <CalendarOverviewCard
data={data}
startTime={config.timePeriod.start} startTime={config.timePeriod.start}
endTime={config.timePeriod.end} endTime={config.timePeriod.end}
projectIds={projectIds}
/> />
<SubjectComparisonCard <SubjectComparisonCard data={data}/>
startTime={config.timePeriod.start}
endTime={config.timePeriod.end}
projectIds={projectIds}
/>
</div> </div>
</main> </main>
) )

View File

@ -1,14 +1,12 @@
"use client"; "use client";
import {Card, Title} from "@tremor/react"; import {Card, Title} from "@tremor/react";
import {useEffect, useRef, useState} from "react";
import useSWR from "swr";
import {fetcher} from "@/app/utils";
import * as R from 'ramda'; import * as R from 'ramda';
import * as dFns from 'date-fns'; import * as dFns from 'date-fns';
import CalendarHeatmap from 'react-calendar-heatmap'; import CalendarHeatmap from 'react-calendar-heatmap';
import 'react-calendar-heatmap/dist/styles.css'; import 'react-calendar-heatmap/dist/styles.css';
import '../calendar-styles.css' import '../calendar-styles.css'
import {Tooltip} from 'react-tooltip'; import {Tooltip} from 'react-tooltip';
import {Data} from "@/app/fetchData";
const dailyGoal = 4; const dailyGoal = 4;
const granularity = 4; const granularity = 4;
@ -20,66 +18,48 @@ function computeCompletionShade(value: number) {
return linearValue; return linearValue;
} }
function useCalendarData(projectIds: number[], initialDate: string, endDate: string) { function useCalendarData(data: Data, initialDate: Date, endDate: Date) {
const { const timeEntries = data.timeEntries;
data: timeEntries,
} = useSWR<{ // Group by day, sum up seconds
raw_json: { const grouped = R.groupBy((entry) => {
start: string, return dFns.formatISO(dFns.startOfDay(dFns.parseISO(entry.start)));
seconds: number, }, timeEntries);
const summed = R.mapObjIndexed((entries) => {
return R.sum((entries ?? []).map((entry) => entry.duration))
}, grouped);
// Fill in missing days, hacky
dFns.eachDayOfInterval({
start: initialDate,
end: endDate,
}).forEach((date) => {
const key = dFns.formatISO(date);
if (summed[key] == undefined) {
summed[key] = 0;
} }
}[]>(`/api/time_entry?select=raw_json&start=gt.${initialDate}&start=lt.${endDate}&project_id=in.(${projectIds.join(',')})`, fetcher, {}); })
const [data, setData] = useState<{ return Object.entries(summed)
date: Date, .map(([key, value]) => ({
count: number, date: dFns.parseISO(key),
}[]>([]); count: value / (60 * 60),
}))
useEffect(() => {
if (timeEntries == undefined) return;
// Group by day, sum up seconds
const grouped = R.groupBy((entry) => {
return dFns.formatISO(dFns.startOfDay(dFns.parseISO(entry.raw_json.start)));
}, timeEntries);
const summed = R.mapObjIndexed((entries) => {
return R.sum((entries ?? []).map((entry) => entry.raw_json.seconds))
}, grouped);
// Fill in missing days, hacky
dFns.eachDayOfInterval({
start: dFns.parseISO(initialDate),
end: dFns.parseISO(endDate),
}).forEach((date) => {
const key = dFns.formatISO(date);
if (summed[key] == undefined) {
summed[key] = 0;
}
})
setData(Object.entries(summed)
.map(([key, value]) => ({
date: dFns.parseISO(key),
count: value / (60 * 60),
})));
}, [timeEntries]);
return data
} }
export function CalendarOverviewCard({ export function CalendarOverviewCard({
projectIds, data,
startTime, startTime,
endTime endTime
}: { }: {
projectIds: number[], data: Data,
startTime: string, startTime: string,
endTime: string, endTime: string,
}) { }) {
const initialDate = dFns.parseISO(startTime); const initialDate = dFns.parseISO(startTime);
const endDate = dFns.parseISO(endTime); const endDate = dFns.parseISO(endTime);
const data = useCalendarData(projectIds, startTime, endTime); const calendarData = useCalendarData(data, initialDate, endDate);
return <Card className="col-span-1"> return <Card className="col-span-1">
<Tooltip id="calendar-tooltip"/> <Tooltip id="calendar-tooltip"/>
@ -88,7 +68,7 @@ export function CalendarOverviewCard({
showWeekdayLabels={true} showWeekdayLabels={true}
startDate={initialDate} startDate={initialDate}
endDate={endDate} endDate={endDate}
values={data} values={calendarData}
classForValue={value => `color-github-${computeCompletionShade(value?.count ?? 0)}`} classForValue={value => `color-github-${computeCompletionShade(value?.count ?? 0)}`}
tooltipDataAttrs={(value: any) => { tooltipDataAttrs={(value: any) => {
return value.date ? { return value.date ? {

View File

@ -1,52 +1,36 @@
"use client"; "use client";
import useSWR from "swr";
import {fetcher} from "@/app/utils";
import {useEffect, useState} from "react";
import {Card, DonutChart, Title} from "@tremor/react"; import {Card, DonutChart, Title} from "@tremor/react";
import * as R from "ramda"; import * as R from "ramda";
import {Data} from "@/app/fetchData";
function useBreakdownData(data: Data): {
name: string,
value: number,
colour: string
}[] {
const sorted = R.sortBy(R.prop('projectId'), data.timeEntries);
const grouped = R.groupWith(R.eqBy(R.prop('projectId')), sorted);
return grouped
.map((entries) => {
const project = data.projects.find(p => p.projectId === entries[0].projectId)!;
return ({
name: project.name,
value: (entries).map((entry) => entry.duration).reduce((a, b) => a + b, 0),
colour: project.color,
});
})
}
export function SubjectComparisonCard({ export function SubjectComparisonCard({
projectIds, data,
startTime,
endTime
}: { }: {
projectIds: number[], data: Data,
startTime: string,
endTime: string,
}) { }) {
const { const breakdownData = useBreakdownData(data);
data: rawData, const colours = breakdownData.map((entry) => entry.colour);
error,
isLoading,
} = useSWR<{
raw_json: {
seconds: number,
},
project: {
name: string,
raw_json: any
}
}[]>(`/api/time_entry?select=raw_json,project:project_id(name,raw_json)&start=gt.${startTime}&start=lt.${endTime}&project_id=in.(${projectIds.join(',')})`, fetcher, {});
const [breakdownData, setBreakdownData] = useState<{
name: string,
value: number
}[]>([]);
const [colours, setColours] = useState<string[]>([]);
useEffect(() => {
const breakdownData = R.toPairs(R.groupBy((entry) => entry.project.name, rawData ?? []))
.map(([name, entries]) => ({
name,
value: (entries ?? []).map((entry) => entry.raw_json.seconds).reduce((a, b) => a + b, 0),
colour: entries?.[0].project.raw_json.color
}))
setBreakdownData(breakdownData);
setColours(breakdownData.map((entry) => entry.colour));
}, [rawData]);
const valueFormatter = (number: number) => `${(number / (60 * 60)).toFixed(2)} hours`; const valueFormatter = (number: number) => `${(number / (60 * 60)).toFixed(2)} hours`;

View File

@ -1,61 +1,27 @@
"use client";
import useSWR from "swr";
import {fetcher} from "@/app/utils";
import {useEffect, useState} from "react";
import {Card, Metric, Text, Title} from "@tremor/react"; import {Card, Metric, Text, Title} from "@tremor/react";
import {Data} from "@/app/fetchData";
export function SubjectOverviewCard({ export function SubjectOverviewCard({
projectId,
title, title,
startTime = '2023-12-15T00:00:00.000Z', data,
endTime = '2024-01-25T00:00:00.000Z', projectId,
}: { }: {
title?: string,
data: Data,
projectId: number, projectId: number,
title?: string
startTime?: string,
endTime?: string,
}) { }) {
const { const project = data.projects.find((project) => project.projectId === projectId)!;
data: _project const totalDuration = data.timeEntries
} = useSWR<{ .filter((entry) => entry.projectId === projectId)
raw_json: { .map((entry) => entry.duration)
name: string, .reduce((a, b) => a + b, 0);
color: string,
}
}[]>(`/api/project?select=raw_json&toggl_id=eq.${projectId}`, fetcher);
const [project, setProject] = useState({
name: '',
color: '',
});
useEffect(() => {
if (_project) {
setProject(_project[0].raw_json);
}
}, [_project]);
const {
data,
error,
isLoading,
} = useSWR<any[]>(`/api/time_entry?select=raw_json&project_id=eq.${projectId}&start=gt.${startTime}&start=lt.${endTime}`, fetcher);
const [a, setA] = useState(0)
useEffect(() => {
if (data) {
setA(data
.map((entry) => entry.raw_json.seconds)
.reduce((a, b) => a + b, 0));
}
}, [data]);
return ( return (
<Card style={{borderColor: project?.color}} decoration={'left'}> <Card style={{borderColor: project?.color}} decoration={'left'}>
<Title>{title ?? project?.name}</Title> <Title>{title ?? project?.name}</Title>
<Text>Total</Text> <Text>Total</Text>
<Metric>{(a / (60 * 60)).toFixed(2)} hours</Metric> <Metric>{(totalDuration / (60 * 60)).toFixed(2)} hours</Metric>
</Card> </Card>
) )
} }

45
src/app/fetchData.ts Normal file
View File

@ -0,0 +1,45 @@
"use server";
import {OverviewConfig} from "@/app/overviewConfig";
export interface Data {
projects: {
projectId: number,
name: string,
color: string,
}[],
timeEntries: {
projectId: number,
start: string,
end: string,
duration: number,
// description: string,
}[],
}
export async function getData(config: OverviewConfig) {
const projectIds = config.subjects.map((subject) => subject.projectId);
const projectResponse = await fetch(`https://revision.joshuacoles.me/api/project?select=raw_json&toggl_id=in.(${projectIds.join(',')})`);
const projects = await projectResponse.json();
const projectLensed = projects.map((project: any) => ({
projectId: project.raw_json.id,
name: project.raw_json.name,
color: project.raw_json.color,
}));
const timeEntriesResponse = await fetch(`https://revision.joshuacoles.me/api/time_entry?select=project_id,raw_json&project_id=in.(${projectIds.join(',')})&start=gt.${config.timePeriod.start}&start=lt.${config.timePeriod.end}`);
const timeEntries = await timeEntriesResponse.json();
const timeEntriesLensed = timeEntries.map((timeEntry: any) => ({
projectId: timeEntry.project_id,
start: timeEntry.raw_json.start,
end: timeEntry.raw_json.end,
duration: timeEntry.raw_json.seconds,
// description: timeEntry.raw_json.description,
}));
return {
projects: projectLensed,
timeEntries: timeEntriesLensed,
};
}