Compare commits
8
Commits
65e8899426
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
026d0cfd56 | ||
|
|
1a14485c57 | ||
|
|
50730a2b09 | ||
|
|
9fc14cffd0 | ||
|
|
e534506894 | ||
|
|
89558cdd1a | ||
|
|
d6b319e6a5 | ||
|
|
f350018eb2 |
@@ -25,9 +25,35 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and Push Docker image -- finn-board
|
||||
- name: Build and Push Docker image
|
||||
uses: docker/build-push-action@675965c0e16f1a0f94ecafff969d8c966f92c17b
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: git.joshuacoles.me/joshuacoles/revision-ui:latest
|
||||
|
||||
build-arm:
|
||||
runs-on: macos-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to Docker
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: git.joshuacoles.me
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and Push Docker image
|
||||
uses: docker/build-push-action@675965c0e16f1a0f94ecafff969d8c966f92c17b
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: git.joshuacoles.me/joshuacoles/revision-ui:arm
|
||||
|
||||
@@ -40,3 +40,6 @@ next-env.d.ts
|
||||
|
||||
# Private env files
|
||||
.env
|
||||
|
||||
# Caddyfile for local development when testing weird deploys
|
||||
/Caddyfile
|
||||
|
||||
@@ -8,7 +8,12 @@ export type DonutChartData = {
|
||||
value: number;
|
||||
}[];
|
||||
|
||||
export function Donut({data, centerLabel, formatter, hoverSuffix}: {
|
||||
export function Donut({
|
||||
data,
|
||||
centerLabel,
|
||||
formatter,
|
||||
hoverSuffix
|
||||
}: {
|
||||
data: DonutChartData,
|
||||
formatter: (value: number) => string,
|
||||
centerLabel: string,
|
||||
@@ -71,12 +76,13 @@ export function Donut({data, centerLabel, formatter, hoverSuffix}: {
|
||||
strokeWidth="3"
|
||||
strokeDasharray={`${value * normalisingFactor} ${100 - value * normalisingFactor}`}
|
||||
strokeDashoffset={100 - (offset * normalisingFactor) + 25}
|
||||
style={{outline: 'none'}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
<g className="chart-text">
|
||||
<g className="chart-text" style={{ userSelect: 'none'}}>
|
||||
<text x="50%" y="50%" className="chart-number">
|
||||
{formatter(total)}
|
||||
</text>
|
||||
|
||||
+47
-19
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import * as dFns from "date-fns";
|
||||
import * as R from 'ramda';
|
||||
|
||||
interface DateValue {
|
||||
date: Date;
|
||||
@@ -12,6 +13,7 @@ interface Props {
|
||||
data: DateValue[];
|
||||
className?: string;
|
||||
goal: number;
|
||||
penaliseOvertime?: boolean;
|
||||
}
|
||||
|
||||
function GitHubCalendarHeatmap({
|
||||
@@ -19,7 +21,8 @@ function GitHubCalendarHeatmap({
|
||||
endDate,
|
||||
data,
|
||||
className,
|
||||
goal
|
||||
goal,
|
||||
penaliseOvertime = false,
|
||||
}: Props) {
|
||||
const numDays = dFns.differenceInDays(endDate, startDate);
|
||||
|
||||
@@ -37,23 +40,48 @@ function GitHubCalendarHeatmap({
|
||||
const dayGoalTime = goal;
|
||||
const weekGoalTime = 5 * dayGoalTime;
|
||||
|
||||
const getColorForValue = (value: number, goalTime: number): string => {
|
||||
const values = [
|
||||
"#eeeeee",
|
||||
"#d6e685",
|
||||
"#8cc665",
|
||||
"#44a340",
|
||||
"#1e6823",
|
||||
]
|
||||
const getColorForValue = (value: number, goalTime: number, zeroColor: string): string => {
|
||||
const buckets = [
|
||||
{
|
||||
min: -Infinity,
|
||||
max: 0,
|
||||
color: zeroColor,
|
||||
},
|
||||
{
|
||||
min: 0,
|
||||
max: 0.40,
|
||||
color: '#d6e685',
|
||||
},
|
||||
{
|
||||
min: 0.40,
|
||||
max: 0.50,
|
||||
color: '#8cc665',
|
||||
},
|
||||
{
|
||||
min: 0.50,
|
||||
max: 0.90,
|
||||
color: '#44a340',
|
||||
},
|
||||
{
|
||||
min: 0.90,
|
||||
// If penalising overtime, the max is 1.125, otherwise it's infinity (ie this is the last bucket)
|
||||
max: penaliseOvertime ? 1.125 : Infinity,
|
||||
color: '#1e6823',
|
||||
},
|
||||
{
|
||||
min: 1.125,
|
||||
max: 1.25,
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
min: 1.25,
|
||||
max: Infinity,
|
||||
color: '#ef4444'
|
||||
},
|
||||
];
|
||||
|
||||
const linearValue = Math.round((value / goalTime) * 4);
|
||||
|
||||
// If we did something, but not enough to reach the first level, return 1
|
||||
if (linearValue == 0 && value > 0) return values[1];
|
||||
|
||||
// Clamp to the granularity
|
||||
if (linearValue > 4) return values[4];
|
||||
return values[linearValue];
|
||||
const linearValue = value / goalTime;
|
||||
return R.find(minMax => minMax.min < linearValue && linearValue <= minMax.max, buckets)!.color;
|
||||
};
|
||||
|
||||
const getWeekTotal = (weekIndex: number): number => {
|
||||
@@ -88,7 +116,7 @@ function GitHubCalendarHeatmap({
|
||||
const dayOfWeek = (date.getDay() + 6) % 7;
|
||||
const x = weekIndex * (squareSize + padding);
|
||||
const y = dayOfWeek * (squareSize + padding);
|
||||
const color = getColorForValue(count, dayGoalTime);
|
||||
const color = getColorForValue(count, dayGoalTime, dFns.isWeekend(date) ? '#d4d4d4' : '#eeeeee');
|
||||
|
||||
return (
|
||||
<rect
|
||||
@@ -109,7 +137,7 @@ function GitHubCalendarHeatmap({
|
||||
const x = weekIndex * (squareSize + padding);
|
||||
const y = 7 * (squareSize + padding) + weekTotalPadding; // Position for the week total
|
||||
const count = getWeekTotal(weekIndex);
|
||||
const color = getColorForValue(count, weekGoalTime);
|
||||
const color = getColorForValue(count, weekGoalTime, '#eeeeee');
|
||||
|
||||
return (
|
||||
<rect
|
||||
|
||||
@@ -47,6 +47,7 @@ export default function OverviewPage({
|
||||
<CalendarOverviewCard
|
||||
data={data}
|
||||
goal={config.goalHours}
|
||||
penaliseOvertime={config.penaliseOvertime}
|
||||
startTime={config.timePeriod.start}
|
||||
endTime={config.timePeriod.end}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {Tooltip} from 'react-tooltip';
|
||||
import {Data} from "@/data/fetchData";
|
||||
import HeatMap from "@/components/HeatMap";
|
||||
|
||||
function useCalendarData(data: Data, initialDate: Date, endDate: Date) {
|
||||
function useCalendarData(data: Data) {
|
||||
const timeEntries = data.timeEntries;
|
||||
|
||||
// Group by day, sum up seconds
|
||||
@@ -17,17 +17,6 @@ function useCalendarData(data: Data, initialDate: Date, endDate: Date) {
|
||||
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;
|
||||
}
|
||||
})
|
||||
|
||||
return Object.entries(summed)
|
||||
.map(([key, value]) => ({
|
||||
date: dFns.parseISO(key),
|
||||
@@ -38,23 +27,25 @@ function useCalendarData(data: Data, initialDate: Date, endDate: Date) {
|
||||
export function CalendarOverviewCard({
|
||||
data,
|
||||
goal,
|
||||
penaliseOvertime,
|
||||
startTime,
|
||||
endTime,
|
||||
}: {
|
||||
data: Data,
|
||||
goal: number,
|
||||
penaliseOvertime?: boolean,
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
}) {
|
||||
const initialDate = dFns.parseISO(startTime);
|
||||
const endDate = dFns.parseISO(endTime);
|
||||
const calendarData = useCalendarData(data, initialDate, endDate);
|
||||
const calendarData = useCalendarData(data);
|
||||
|
||||
return <Card className="col-span-1">
|
||||
<Tooltip id="calendar-tooltip"/>
|
||||
<Title>Semester Overview</Title>
|
||||
<div className="m-2">
|
||||
<HeatMap startDate={initialDate} endDate={endDate} data={calendarData} goal={goal}/>
|
||||
<HeatMap startDate={initialDate} endDate={endDate} data={calendarData} goal={goal} penaliseOvertime={penaliseOvertime}/>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
|
||||
Vendored
+10
-11
@@ -1,4 +1,4 @@
|
||||
import type {ColumnType, JSONColumnType} from "kysely";
|
||||
import type {ColumnType} from "kysely";
|
||||
|
||||
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S, I | undefined, U>
|
||||
@@ -32,13 +32,13 @@ export interface Client {
|
||||
export interface Project {
|
||||
active: boolean;
|
||||
client_id: number | null;
|
||||
color: string;
|
||||
id: Generated<number>;
|
||||
name: string;
|
||||
raw_json: JSONColumnType<{
|
||||
color: string;
|
||||
id: number;
|
||||
name: string;
|
||||
}>;
|
||||
raw_json: Json;
|
||||
server_created_at: Timestamp;
|
||||
server_deleted_at: Timestamp | null;
|
||||
server_updated_at: Timestamp;
|
||||
toggl_id: Int8;
|
||||
workspace_id: Int8;
|
||||
}
|
||||
@@ -47,13 +47,12 @@ export interface TimeEntry {
|
||||
description: string;
|
||||
id: Generated<number>;
|
||||
project_id: Int8 | null;
|
||||
raw_json: JSONColumnType<{
|
||||
start: string;
|
||||
end: string;
|
||||
seconds: number;
|
||||
}>;
|
||||
raw_json: Json;
|
||||
server_deleted_at: Timestamp | null;
|
||||
server_updated_at: Timestamp;
|
||||
start: Timestamp;
|
||||
stop: Timestamp;
|
||||
tags: Generated<Json>;
|
||||
toggl_id: Int8;
|
||||
}
|
||||
|
||||
|
||||
+11
-10
@@ -30,6 +30,7 @@ export interface OverviewConfig {
|
||||
}[],
|
||||
|
||||
goalHours: number,
|
||||
penaliseOvertime?: boolean,
|
||||
|
||||
timePeriod: {
|
||||
start: string,
|
||||
@@ -51,12 +52,12 @@ export async function getData(config: OverviewConfig): Promise<Data> {
|
||||
let projectIds = config.subjects.map((subject) => subject.projectId.toString());
|
||||
|
||||
const projects = await db.selectFrom('project')
|
||||
.select('raw_json')
|
||||
.where('project.toggl_id', 'in', projectIds)
|
||||
.execute();
|
||||
.select(['toggl_id', 'name', 'color'])
|
||||
.where('toggl_id', 'in', projectIds)
|
||||
.execute()
|
||||
|
||||
const timeEntries = await db.selectFrom('time_entry')
|
||||
.select(['project_id', 'raw_json'])
|
||||
.select(['project_id', 'start', 'stop'])
|
||||
.where('project_id', 'in', projectIds)
|
||||
.where('start', '>', dFns.parseISO(config.timePeriod.start))
|
||||
.where('start', '<', dFns.parseISO(config.timePeriod.end))
|
||||
@@ -64,16 +65,16 @@ export async function getData(config: OverviewConfig): Promise<Data> {
|
||||
|
||||
return {
|
||||
projects: projects.map((project) => ({
|
||||
projectId: project.raw_json.id,
|
||||
name: project.raw_json.name,
|
||||
color: project.raw_json.color,
|
||||
projectId: parseInt(project.toggl_id),
|
||||
name: project.name,
|
||||
color: project.color,
|
||||
})),
|
||||
|
||||
timeEntries: timeEntries.map((timeEntry) => ({
|
||||
projectId: parseInt(timeEntry.project_id!),
|
||||
start: timeEntry.raw_json.start,
|
||||
end: timeEntry.raw_json.end,
|
||||
duration: timeEntry.raw_json.seconds,
|
||||
start: dFns.formatISO(timeEntry.start),
|
||||
end: dFns.formatISO(timeEntry.stop),
|
||||
duration: dFns.differenceInSeconds(timeEntry.stop, timeEntry.start)
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -29,7 +29,8 @@ export const semester1Revision: OverviewConfig = {
|
||||
export const semester2: OverviewConfig = {
|
||||
title: 'Semester 2',
|
||||
periodKey: 'y5-s2',
|
||||
goalHours: 7.5,
|
||||
goalHours: 6,
|
||||
penaliseOvertime: true,
|
||||
subjects: [
|
||||
{
|
||||
projectId: 195754611,
|
||||
|
||||
Reference in New Issue
Block a user