Use server actions over swr for retrieving data
This commit is contained in:
@@ -2,10 +2,13 @@ import {SubjectComparisonCard} from "@/app/cards/subjectComparisonCard";
|
||||
import {CalendarOverviewCard} from "@/app/cards/calendarOverviewCard";
|
||||
import {OverviewConfig} from "@/app/overviewConfig";
|
||||
import {SubjectOverviewCard} from "@/app/cards/subjectOverviewCard";
|
||||
import {getData} from "@/app/fetchData";
|
||||
|
||||
export default function OverviewPage({config}: {
|
||||
export default async function OverviewPage({config}: {
|
||||
config: OverviewConfig
|
||||
}) {
|
||||
const data = await getData(config);
|
||||
|
||||
const projectIds = config.subjects.map((subject) => subject.projectId);
|
||||
|
||||
return (
|
||||
@@ -18,23 +21,17 @@ export default function OverviewPage({config}: {
|
||||
key={subject.projectId}
|
||||
projectId={subject.projectId}
|
||||
title={subject.title}
|
||||
startTime={config.timePeriod.start}
|
||||
endTime={config.timePeriod.end}
|
||||
data={data}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
<CalendarOverviewCard
|
||||
data={data}
|
||||
startTime={config.timePeriod.start}
|
||||
endTime={config.timePeriod.end}
|
||||
projectIds={projectIds}
|
||||
/>
|
||||
|
||||
<SubjectComparisonCard
|
||||
startTime={config.timePeriod.start}
|
||||
endTime={config.timePeriod.end}
|
||||
projectIds={projectIds}
|
||||
/>
|
||||
<SubjectComparisonCard data={data}/>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
"use client";
|
||||
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 dFns from 'date-fns';
|
||||
import CalendarHeatmap from 'react-calendar-heatmap';
|
||||
import 'react-calendar-heatmap/dist/styles.css';
|
||||
import '../calendar-styles.css'
|
||||
import {Tooltip} from 'react-tooltip';
|
||||
import {Data} from "@/app/fetchData";
|
||||
|
||||
const dailyGoal = 4;
|
||||
const granularity = 4;
|
||||
@@ -20,66 +18,48 @@ function computeCompletionShade(value: number) {
|
||||
return linearValue;
|
||||
}
|
||||
|
||||
function useCalendarData(projectIds: number[], initialDate: string, endDate: string) {
|
||||
const {
|
||||
data: timeEntries,
|
||||
} = useSWR<{
|
||||
raw_json: {
|
||||
start: string,
|
||||
seconds: number,
|
||||
function useCalendarData(data: Data, initialDate: Date, endDate: Date) {
|
||||
const timeEntries = data.timeEntries;
|
||||
|
||||
// Group by day, sum up seconds
|
||||
const grouped = R.groupBy((entry) => {
|
||||
return dFns.formatISO(dFns.startOfDay(dFns.parseISO(entry.start)));
|
||||
}, 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<{
|
||||
date: Date,
|
||||
count: number,
|
||||
}[]>([]);
|
||||
|
||||
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
|
||||
return Object.entries(summed)
|
||||
.map(([key, value]) => ({
|
||||
date: dFns.parseISO(key),
|
||||
count: value / (60 * 60),
|
||||
}))
|
||||
}
|
||||
|
||||
export function CalendarOverviewCard({
|
||||
projectIds,
|
||||
data,
|
||||
startTime,
|
||||
endTime
|
||||
}: {
|
||||
projectIds: number[],
|
||||
data: Data,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
}) {
|
||||
const initialDate = dFns.parseISO(startTime);
|
||||
const endDate = dFns.parseISO(endTime);
|
||||
const data = useCalendarData(projectIds, startTime, endTime);
|
||||
const calendarData = useCalendarData(data, initialDate, endDate);
|
||||
|
||||
return <Card className="col-span-1">
|
||||
<Tooltip id="calendar-tooltip"/>
|
||||
@@ -88,7 +68,7 @@ export function CalendarOverviewCard({
|
||||
showWeekdayLabels={true}
|
||||
startDate={initialDate}
|
||||
endDate={endDate}
|
||||
values={data}
|
||||
values={calendarData}
|
||||
classForValue={value => `color-github-${computeCompletionShade(value?.count ?? 0)}`}
|
||||
tooltipDataAttrs={(value: any) => {
|
||||
return value.date ? {
|
||||
|
||||
@@ -1,52 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import useSWR from "swr";
|
||||
import {fetcher} from "@/app/utils";
|
||||
import {useEffect, useState} from "react";
|
||||
import {Card, DonutChart, Title} from "@tremor/react";
|
||||
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({
|
||||
projectIds,
|
||||
startTime,
|
||||
endTime
|
||||
data,
|
||||
}: {
|
||||
projectIds: number[],
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
data: Data,
|
||||
}) {
|
||||
const {
|
||||
data: rawData,
|
||||
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 breakdownData = useBreakdownData(data);
|
||||
const colours = breakdownData.map((entry) => entry.colour);
|
||||
|
||||
const valueFormatter = (number: number) => `${(number / (60 * 60)).toFixed(2)} hours`;
|
||||
|
||||
|
||||
@@ -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 {Data} from "@/app/fetchData";
|
||||
|
||||
export function SubjectOverviewCard({
|
||||
projectId,
|
||||
title,
|
||||
startTime = '2023-12-15T00:00:00.000Z',
|
||||
endTime = '2024-01-25T00:00:00.000Z',
|
||||
data,
|
||||
projectId,
|
||||
}: {
|
||||
title?: string,
|
||||
data: Data,
|
||||
projectId: number,
|
||||
title?: string
|
||||
startTime?: string,
|
||||
endTime?: string,
|
||||
}) {
|
||||
const {
|
||||
data: _project
|
||||
} = useSWR<{
|
||||
raw_json: {
|
||||
name: string,
|
||||
color: string,
|
||||
}
|
||||
}[]>(`/api/project?select=raw_json&toggl_id=eq.${projectId}`, fetcher);
|
||||
const project = data.projects.find((project) => project.projectId === projectId)!;
|
||||
const totalDuration = data.timeEntries
|
||||
.filter((entry) => entry.projectId === projectId)
|
||||
.map((entry) => entry.duration)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
|
||||
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 (
|
||||
<Card style={{borderColor: project?.color}} decoration={'left'}>
|
||||
<Title>{title ?? project?.name}</Title>
|
||||
<Text>Total</Text>
|
||||
<Metric>{(a / (60 * 60)).toFixed(2)} hours</Metric>
|
||||
<Metric>{(totalDuration / (60 * 60)).toFixed(2)} hours</Metric>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user