Compare commits

..

No commits in common. "fdf57fa6b67db72bb92d5d167d0b36962c30707a" and "d4e2062243ea76975ae74615ef1206ed01b4f842" have entirely different histories.

6 changed files with 47 additions and 121 deletions

1
.gitignore vendored
View file

@ -11,7 +11,6 @@ yarn-error.log
*.swp *.swp
.docker
/.cache /.cache
/build /build
/public/build /public/build

View file

@ -1,13 +1,11 @@
# WorkTimer # WorkTimer
<img src="https://github.com/nzambello/work-timer/raw/main/public/logo.png" width="100" height="100" alt="WorkTimer" /> <img src="./public/logo.png" width="100" height="100" alt="WorkTimer" />
Time tracking app built with Remix, supports authentication, projects management, and monthly or custom reports. Time tracking app built with Remix, supports authentication, projects management, and monthly or custom reports.
Built for self-hosting: host it anywhere you want, and use it for free. Your time, your data. Built for self-hosting: host it anywhere you want, and use it for free. Your time, your data.
View on [DockerHub](https://hub.docker.com/r/nzambello/work-timer).
## Table of contents ## Table of contents
- [Features](#features) - [Features](#features)
@ -17,7 +15,6 @@ View on [DockerHub](https://hub.docker.com/r/nzambello/work-timer).
- [Tech stack](#tech-stack) - [Tech stack](#tech-stack)
- [Running locally](#running-locally) - [Running locally](#running-locally)
- [Running with Docker](#running-with-docker) - [Running with Docker](#running-with-docker)
- [Multi-platform docker image](#multi-platform-docker-image)
- [Screenshots](#screenshots) - [Screenshots](#screenshots)
- [License](#license) - [License](#license)
@ -48,26 +45,10 @@ If you want to use different defaults, you can build your own image. See [Runnin
### Docker compose ### Docker compose
Basic example:
```yaml
version: '3.8'
services:
work-timer:
image: nzambello/work-timer
container_name: work-timer
restart: always
ports:
- 8080:8080
volumes:
- ./dockerData/work-timer:/data # Path to data for DB persistence
```
Example of docker-compose.yml with [Traefik](https://traefik.io/) as reverse proxy: Example of docker-compose.yml with [Traefik](https://traefik.io/) as reverse proxy:
```yaml ```yaml
work-timer: uptime-kuma:
depends_on: depends_on:
- watchtower - watchtower
image: nzambello/work-timer image: nzambello/work-timer
@ -131,35 +112,28 @@ docker built -t work-timer .
docker run -p 127.0.0.1:8080:8080 work-timer docker run -p 127.0.0.1:8080:8080 work-timer
``` ```
### Multi-platform Docker image
```bash
docker buildx create --name mybuilder --driver docker-container --bootstrap --use # create a new builder and switch to it using a single command.
docker buildx build --platform linux/amd64,linux/arm64 -t nzambello/work-timer:latest --push .
```
## Screenshots ## Screenshots
### Light / Dark mode ### Light / Dark mode
<img width="300" src="https://github.com/nzambello/work-timer/raw/main/public/images/00-time-entries-light.png" /> <img width="300" src="./public/images/00-time-entries-light.png" />
<img width="300" src="https://github.com/nzambello/work-timer/raw/main/public/images/01-time-entries-dark.png" /> <img width="300" src="./public/images/01-time-entries-dark.png" />
### Time tracking ### Time tracking
<img width="500" src="https://github.com/nzambello/work-timer/raw/main/public/images/02-new-time-entry.png" /> <img width="500" src="./public/images/02-new-time-entry.png" />
### Projects ### Projects
<img width="300" src="https://github.com/nzambello/work-timer/raw/main/public/images/03-projects.png" /> <img width="300" src="./public/images/03-projects.png" />
<img width="300" src="https://github.com/nzambello/work-timer/raw/main/public/images/04-new-project.png" /> <img width="300" src="./public/images/04-new-project.png" />
### Reports ### Reports
<img width="500" src="https://github.com/nzambello/work-timer/raw/main/public/images/05-reports.png" /> <img width="500" src="./public/images/05-reports.png" />
## License ## License
[Nicola Zambello](https://github.com/nzambello) © 2023 [Nicola Zambello](https://github.com/nzambello) © 2023
[GNU GPLv3](https://github.com/nzambello/work-timer/raw/main/LICENSE) [GNU GPLv3](./LICENSE)

View file

@ -37,11 +37,6 @@ const TimeElapsed = ({ startTime, endTime }: Props) => {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [startTime, endTime]); }, [startTime, endTime]);
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
const hours = Math.floor(elapsed / 60 / 60); const hours = Math.floor(elapsed / 60 / 60);
const minutes = Math.floor((elapsed - hours * 60 * 60) / 60); const minutes = Math.floor((elapsed - hours * 60 * 60) / 60);
const seconds = Math.floor(elapsed - hours * 60 * 60 - minutes * 60); const seconds = Math.floor(elapsed - hours * 60 * 60 - minutes * 60);
@ -67,7 +62,6 @@ const TimeElapsed = ({ startTime, endTime }: Props) => {
> >
<code>{hoursString}</code> <code>{hoursString}</code>
</pre> </pre>
{isClient && (
<MediaQuery <MediaQuery
smallerThan="sm" smallerThan="sm"
styles={{ styles={{
@ -97,7 +91,6 @@ const TimeElapsed = ({ startTime, endTime }: Props) => {
</span> </span>
</p> </p>
</MediaQuery> </MediaQuery>
)}
</Box> </Box>
); );
}; };

View file

@ -11,13 +11,11 @@ export async function loader({ request }: LoaderArgs) {
header: true header: true
}); });
const timestamp = new Date().toISOString().replace(/\D/g, '').slice(0, -3);
return new Response(csv, { return new Response(csv, {
status: 200, status: 200,
headers: { headers: {
'Content-Type': 'text/csv', 'Content-Type': 'text/csv',
'Content-Disposition': `attachment; filename="work-timer-export-${timestamp}.csv"`, 'Content-Disposition': 'attachment; filename="export.csv"'
}, }
}); });
} }

View file

@ -159,17 +159,6 @@ export default function Index() {
management, and monthly or custom reports management, and monthly or custom reports
</Text> </Text>
<h2>
<Text
component="span"
variant="gradient"
gradient={{ from: 'blue', to: 'cyan' }}
inherit
>
Your time, your data
</Text>
</h2>
<Group className={classes.controls}> <Group className={classes.controls}>
<Button <Button
component={Link} component={Link}
@ -192,7 +181,7 @@ export default function Index() {
className={classes.control} className={classes.control}
leftIcon={<GitHub />} leftIcon={<GitHub />}
> >
View on GitHub GitHub
</Button> </Button>
</Group> </Group>
</Container> </Container>

View file

@ -48,21 +48,13 @@ export async function loader({ request }: LoaderArgs) {
await updateDuration(user.id); await updateDuration(user.id);
const timeByProject = await getTimeEntriesByDateAndProject({ return json({
user,
timeByProject: await getTimeEntriesByDateAndProject({
userId: user.id, userId: user.id,
dateFrom, dateFrom,
dateTo dateTo
}); }),
const total = timeByProject.reduce(
(acc, curr) => acc + (curr._sum.duration || 0),
0
);
return json({
user,
timeByProject,
total,
projects: await getProjects({ userId: user.id }) projects: await getProjects({ userId: user.id })
}); });
} }
@ -236,8 +228,8 @@ export default function ReportPage() {
<thead> <thead>
<tr> <tr>
<th>Project</th> <th>Project</th>
<th scope="col">Time</th> <th>Time</th>
{hourlyRate && <th scope="col">Billing</th>} {hourlyRate && <th>Billing</th>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -248,7 +240,7 @@ export default function ReportPage() {
}[] }[]
).map((projectData) => ( ).map((projectData) => (
<tr key={projectData.projectId}> <tr key={projectData.projectId}>
<td scope="row"> <td>
<Flex align="center"> <Flex align="center">
<ColorSwatch <ColorSwatch
mr="sm" mr="sm"
@ -280,25 +272,6 @@ export default function ReportPage() {
</tr> </tr>
))} ))}
</tbody> </tbody>
{!!reports.data?.timeByProject?.length && (
<tfoot>
<tr>
<th scope="row">Totals</th>
<th>{(reports.data.total / 1000 / 60 / 60).toFixed(2)} h</th>
{hourlyRate && (
<th>
{(
(reports.data.total * hourlyRate) /
1000 /
60 /
60
).toFixed(2)}{' '}
{reports.data?.user?.currency ?? '€'}
</th>
)}
</tr>
</tfoot>
)}
</Table> </Table>
)} )}
</> </>