Compare commits

...

8 Commits

Author SHA1 Message Date
0314ccc2b3 Add start and end times to SubjectComparisonCard
Some checks are pending
Build and Publish Docker Container / build (push) Has started running
2024-02-07 11:52:57 +00:00
44df8a6fc6 Add OverviewConfig to allow for additional queries, setup sem2 page 2024-02-07 11:51:04 +00:00
0a89712965 Remove IDEA config, add types for things 2023-12-31 14:47:54 +00:00
c7fcb0990f Fix import 2023-12-25 12:53:32 +00:00
0ce2096fac I am lazy, remove the new heatmap for now 2023-12-25 12:52:10 +00:00
52b6b0eaee Bring some more things into the render fn 2023-12-24 18:40:33 +00:00
a61677fe35 Adapt Month labels 2023-12-24 18:33:46 +00:00
c85776a499 Initial import of react-calendar-heatmap code 2023-12-22 17:52:08 +00:00
16 changed files with 3926 additions and 107 deletions

3
.gitignore vendored
View File

@ -1,5 +1,8 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# IDEA
/.idea
# dependencies
/node_modules
/.pnp

5
.idea/.gitignore generated vendored
View File

@ -1,5 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/tremor-dashboard.iml" filepath="$PROJECT_DIR$/.idea/tremor-dashboard.iml" />
</modules>
</component>
</project>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

18
jest.config.ts Normal file
View File

@ -0,0 +1,18 @@
import type { Config } from 'jest'
import nextJest from 'next/jest.js'
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
})
// Add any custom config to be passed to Jest
const config: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
// Add more setup options before each test is run
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config)

3724
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"test": "jest"
},
"dependencies": {
"@heroicons/react": "^1.0.6",
@ -25,6 +26,8 @@
"swr": "^2.2.4"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-calendar-heatmap": "^1.6.6",
@ -32,8 +35,11 @@
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.0.4",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"ts-node": "^10.9.2",
"typescript": "^5"
}
}

40
src/app/OverviewPage.tsx Normal file
View File

@ -0,0 +1,40 @@
import {SubjectComparisonCard, SubjectOverviewCard} from "@/app/a.client";
import {CalendarOverviewCard} from "@/app/calendarOverviewCard";
import {OverviewConfig} from "@/app/overviewConfig";
export default function OverviewPage({config}: {
config: OverviewConfig
}) {
const projectIds = config.subjects.map((subject) => subject.projectId);
return (
<main className="m-6">
<h1 className="text-3xl font-semibold text-slate-900 dark:text-white my-2">{config.title}</h1>
<div className="grid gap-5 grid-cols-1 sm:grid-cols-4">
{config.subjects.map((subject) => (
<SubjectOverviewCard
key={subject.projectId}
projectId={subject.projectId}
title={subject.title}
startTime={config.timePeriod.start}
endTime={config.timePeriod.end}
/>
))}
<CalendarOverviewCard
startTime={config.timePeriod.start}
endTime={config.timePeriod.end}
projectIds={projectIds}
/>
<SubjectComparisonCard
startTime={config.timePeriod.start}
endTime={config.timePeriod.end}
projectIds={projectIds}
/>
</div>
</main>
)
}

View File

@ -8,14 +8,24 @@ import * as R from "ramda";
export function SubjectOverviewCard({
projectId,
title
title,
startTime = '2023-12-15T00:00:00.000Z',
endTime = '2024-01-25T00:00:00.000Z',
}: {
projectId: number,
title?: string
startTime?: string,
endTime?: string,
}) {
const {
data: _project
} = useSWR<any>(`/api/project?select=raw_json&toggl_id=eq.${projectId}`, fetcher);
} = useSWR<{
raw_json: {
name: string,
color: string,
}
}[]>(`/api/project?select=raw_json&toggl_id=eq.${projectId}`, fetcher);
const [project, setProject] = useState({
name: '',
color: '',
@ -31,7 +41,7 @@ export function SubjectOverviewCard({
data,
error,
isLoading,
} = useSWR<any[]>(`/api/time_entry?select=raw_json&project_id=eq.${projectId}&start=gt.2023-12-15T00:00:00.000Z`, fetcher);
} = 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(() => {
@ -43,7 +53,7 @@ export function SubjectOverviewCard({
}, [data]);
return (
<Card style={{ borderColor: project?.color }} decoration={'left'}>
<Card style={{borderColor: project?.color}} decoration={'left'}>
<Title>{title ?? project?.name}</Title>
<Text>Total</Text>
<Metric>{(a / (60 * 60)).toFixed(2)} hours</Metric>
@ -51,30 +61,47 @@ export function SubjectOverviewCard({
)
}
export function SubjectComparisonCard({projectIds}: {
projectIds: number[]
export function SubjectComparisonCard({
projectIds,
startTime,
endTime
}: {
projectIds: number[],
startTime: string,
endTime: string,
}) {
const {
data,
data: rawData,
error,
isLoading,
} = useSWR<any[]>(`/api/time_entry?select=raw_json,project:project_id(name,raw_json)&project_id=in.(${projectIds.join(',')})`, fetcher, {});
} = 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 [a, setA] = useState<{ name: string, value: number }[]>([]);
const [breakdownData, setBreakdownData] = useState<{
name: string,
value: number
}[]>([]);
const [colours, setColours] = useState<string[]>([]);
useEffect(() => {
const a = R.toPairs(R.groupBy((entry) => entry.project.name, data ?? []))
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
}))
setA(a);
setBreakdownData(breakdownData);
setColours(a.map((entry) => entry.colour));
}, [data]);
setColours(breakdownData.map((entry) => entry.colour));
}, [rawData]);
const valueFormatter = (number: number) => `${(number / (60 * 60)).toFixed(2)} hours`;
@ -83,7 +110,7 @@ export function SubjectComparisonCard({projectIds}: {
<Title>Relative Breakdown</Title>
<DonutChart
className="mt-6"
data={a ?? []}
data={breakdownData ?? []}
category="value"
index="name"
valueFormatter={valueFormatter}

View File

@ -8,16 +8,7 @@ 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';
const initialDate = dFns.parseISO('2023-12-15T00:00:00.000Z')
const endDate = dFns.parseISO('2024-01-25T00:00:00.000Z')
const projectIds = [
195482340,
195519024,
195518593,
195754611,
];
import {Tooltip} from 'react-tooltip';
const dailyGoal = 4;
const granularity = 4;
@ -29,17 +20,15 @@ function computeCompletionShade(value: number) {
return linearValue;
}
function useCalendarData() {
function useCalendarData(projectIds: number[], initialDate: string, endDate: string) {
const {
data: timeEntries,
error,
isLoading,
} = useSWR<{
raw_json: {
start: string,
seconds: number,
}
}[]>(`/api/time_entry?select=raw_json&start=gt.${dFns.formatISO(initialDate)}&project_id=in.(${projectIds.join(',')})`, fetcher, {});
}[]>(`/api/time_entry?select=raw_json&start=gt.${initialDate}&start=lt.${endDate}&project_id=in.(${projectIds.join(',')})`, fetcher, {});
const [data, setData] = useState<{
date: Date,
@ -60,8 +49,8 @@ function useCalendarData() {
// Fill in missing days, hacky
dFns.eachDayOfInterval({
start: initialDate,
end: endDate,
start: dFns.parseISO(initialDate),
end: dFns.parseISO(endDate),
}).forEach((date) => {
const key = dFns.formatISO(date);
if (summed[key] == undefined) {
@ -69,8 +58,6 @@ function useCalendarData() {
}
})
debugger
setData(Object.entries(summed)
.map(([key, value]) => ({
date: dFns.parseISO(key),
@ -81,8 +68,18 @@ function useCalendarData() {
return data
}
export function CalendarOverviewCard() {
const data = useCalendarData();
export function CalendarOverviewCard({
projectIds,
startTime,
endTime
}: {
projectIds: number[],
startTime: string,
endTime: string,
}) {
const initialDate = dFns.parseISO(startTime);
const endDate = dFns.parseISO(endTime);
const data = useCalendarData(projectIds, startTime, endTime);
return <Card className="col-span-1">
<Tooltip id="calendar-tooltip"/>
@ -96,7 +93,7 @@ export function CalendarOverviewCard() {
tooltipDataAttrs={(value: any) => {
return value.date ? {
'data-tooltip-id': `calendar-tooltip`,
'data-tooltip-content': value.count ?`${dFns.format(value.date, 'EEE do')}: ${value.count.toFixed(2)} hours` : `${dFns.format(value.date, 'EEE do')}`
'data-tooltip-content': value.count ? `${dFns.format(value.date, 'EEE do')}: ${value.count.toFixed(2)} hours` : `${dFns.format(value.date, 'EEE do')}`
} : undefined
}}
/>

View File

@ -5,8 +5,8 @@ import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
title: 'Work Tracker',
description: 'Track time spent on different subjects',
}
export default function RootLayout({

40
src/app/overviewConfig.ts Normal file
View File

@ -0,0 +1,40 @@
export interface OverviewConfig {
title: string,
subjects: {
title?: string,
projectId: number,
}[],
goalHours: number,
timePeriod: {
start: string,
end: string
},
}
export const semester1Revision: OverviewConfig = {
title: 'Semester 1 Revision',
goalHours: 4,
subjects: [
{
projectId: 195482340,
},
{
title: 'Measure Theory',
projectId: 195519024,
},
{
title: 'Quantum Mechanics',
projectId: 195518593,
},
{
projectId: 195754611,
}
],
timePeriod: {
start: "2023-12-15T00:00:00.000Z",
end: "2024-01-25T00:00:00.000Z"
}
}

View File

@ -1,33 +1,6 @@
import {SubjectComparisonCard, SubjectOverviewCard} from "@/app/a.client";
import {CalendarOverviewCard} from "@/app/calendarOverviewCard";
import OverviewPage from "@/app/OverviewPage";
import {semester1Revision} from "@/app/overviewConfig";
export default function Home() {
return (
<main className="m-6">
<h1 className="text-3xl font-semibold text-slate-900 dark:text-white my-2">Revision Tracker</h1>
<div className="grid gap-5 grid-cols-1 sm:grid-cols-4">
<SubjectOverviewCard
projectId={195482340}
/>
<SubjectOverviewCard
title='Measure Theory'
projectId={195519024}
/>
<SubjectOverviewCard
title='Quantum Mechanics'
projectId={195518593}
/>
<SubjectOverviewCard
projectId={195754611}
/>
<CalendarOverviewCard/>
</div>
</main>
)
return <OverviewPage config={semester1Revision}/>
}

32
src/app/sem2/page.tsx Normal file
View File

@ -0,0 +1,32 @@
import OverviewPage from "@/app/OverviewPage";
import {OverviewConfig} from "@/app/overviewConfig";
const semester2: OverviewConfig = {
title: 'Semester 2',
goalHours: 4,
subjects: [
{
projectId: 195754611,
},
{
projectId: 199383703,
},
{
projectId: 199383691,
},
{
projectId: 198859760,
},
{
projectId: 199383698,
},
],
timePeriod: {
start: "2024-02-03T00:00:00.000Z",
end: "2024-05-10T00:00:00.000Z"
}
}
export default function Home() {
return <OverviewPage config={semester2}/>
}