Compare commits
No commits in common. "fdf57fa6b67db72bb92d5d167d0b36962c30707a" and "d4e2062243ea76975ae74615ef1206ed01b4f842" have entirely different histories.
fdf57fa6b6
...
d4e2062243
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,7 +11,6 @@ yarn-error.log
|
|||
|
||||
*.swp
|
||||
|
||||
.docker
|
||||
/.cache
|
||||
/build
|
||||
/public/build
|
||||
|
|
|
|||
44
README.md
44
README.md
|
|
@ -1,13 +1,11 @@
|
|||
# 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.
|
||||
|
||||
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
|
||||
|
||||
- [Features](#features)
|
||||
|
|
@ -17,7 +15,6 @@ View on [DockerHub](https://hub.docker.com/r/nzambello/work-timer).
|
|||
- [Tech stack](#tech-stack)
|
||||
- [Running locally](#running-locally)
|
||||
- [Running with Docker](#running-with-docker)
|
||||
- [Multi-platform docker image](#multi-platform-docker-image)
|
||||
- [Screenshots](#screenshots)
|
||||
- [License](#license)
|
||||
|
||||
|
|
@ -48,26 +45,10 @@ If you want to use different defaults, you can build your own image. See [Runnin
|
|||
|
||||
### 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:
|
||||
|
||||
```yaml
|
||||
work-timer:
|
||||
uptime-kuma:
|
||||
depends_on:
|
||||
- watchtower
|
||||
image: nzambello/work-timer
|
||||
|
|
@ -131,35 +112,28 @@ docker built -t 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
|
||||
|
||||
### 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="https://github.com/nzambello/work-timer/raw/main/public/images/01-time-entries-dark.png" />
|
||||
<img width="300" src="./public/images/00-time-entries-light.png" />
|
||||
<img width="300" src="./public/images/01-time-entries-dark.png" />
|
||||
|
||||
### 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
|
||||
|
||||
<img width="300" src="https://github.com/nzambello/work-timer/raw/main/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/03-projects.png" />
|
||||
<img width="300" src="./public/images/04-new-project.png" />
|
||||
|
||||
### 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
|
||||
|
||||
[Nicola Zambello](https://github.com/nzambello) © 2023
|
||||
|
||||
[GNU GPLv3](https://github.com/nzambello/work-timer/raw/main/LICENSE)
|
||||
[GNU GPLv3](./LICENSE)
|
||||
|
|
|
|||
|
|
@ -37,11 +37,6 @@ const TimeElapsed = ({ startTime, endTime }: Props) => {
|
|||
return () => clearInterval(interval);
|
||||
}, [startTime, endTime]);
|
||||
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
const hours = Math.floor(elapsed / 60 / 60);
|
||||
const minutes = Math.floor((elapsed - hours * 60 * 60) / 60);
|
||||
const seconds = Math.floor(elapsed - hours * 60 * 60 - minutes * 60);
|
||||
|
|
@ -67,37 +62,35 @@ const TimeElapsed = ({ startTime, endTime }: Props) => {
|
|||
>
|
||||
<code>{hoursString}</code>
|
||||
</pre>
|
||||
{isClient && (
|
||||
<MediaQuery
|
||||
smallerThan="sm"
|
||||
styles={{
|
||||
display: 'none'
|
||||
<MediaQuery
|
||||
smallerThan="sm"
|
||||
styles={{
|
||||
display: 'none'
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontSize: '0.75rem',
|
||||
margin: 0
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontSize: '0.75rem',
|
||||
margin: 0
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{Intl.DateTimeFormat(user?.dateFormat || 'en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(new Date(startTime))}
|
||||
</span>
|
||||
<span dangerouslySetInnerHTML={{ __html: ' — ' }} />
|
||||
<span>
|
||||
{endTime
|
||||
? Intl.DateTimeFormat(user?.dateFormat || 'en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(new Date(endTime))
|
||||
: 'now'}
|
||||
</span>
|
||||
</p>
|
||||
</MediaQuery>
|
||||
)}
|
||||
<span>
|
||||
{Intl.DateTimeFormat(user?.dateFormat || 'en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(new Date(startTime))}
|
||||
</span>
|
||||
<span dangerouslySetInnerHTML={{ __html: ' — ' }} />
|
||||
<span>
|
||||
{endTime
|
||||
? Intl.DateTimeFormat(user?.dateFormat || 'en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(new Date(endTime))
|
||||
: 'now'}
|
||||
</span>
|
||||
</p>
|
||||
</MediaQuery>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,13 +11,11 @@ export async function loader({ request }: LoaderArgs) {
|
|||
header: true
|
||||
});
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/\D/g, '').slice(0, -3);
|
||||
|
||||
return new Response(csv, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': `attachment; filename="work-timer-export-${timestamp}.csv"`,
|
||||
},
|
||||
'Content-Disposition': 'attachment; filename="export.csv"'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,17 +159,6 @@ export default function Index() {
|
|||
management, and monthly or custom reports
|
||||
</Text>
|
||||
|
||||
<h2>
|
||||
<Text
|
||||
component="span"
|
||||
variant="gradient"
|
||||
gradient={{ from: 'blue', to: 'cyan' }}
|
||||
inherit
|
||||
>
|
||||
Your time, your data
|
||||
</Text>
|
||||
</h2>
|
||||
|
||||
<Group className={classes.controls}>
|
||||
<Button
|
||||
component={Link}
|
||||
|
|
@ -192,7 +181,7 @@ export default function Index() {
|
|||
className={classes.control}
|
||||
leftIcon={<GitHub />}
|
||||
>
|
||||
View on GitHub
|
||||
GitHub
|
||||
</Button>
|
||||
</Group>
|
||||
</Container>
|
||||
|
|
|
|||
|
|
@ -48,21 +48,13 @@ export async function loader({ request }: LoaderArgs) {
|
|||
|
||||
await updateDuration(user.id);
|
||||
|
||||
const timeByProject = await getTimeEntriesByDateAndProject({
|
||||
userId: user.id,
|
||||
dateFrom,
|
||||
dateTo
|
||||
});
|
||||
|
||||
const total = timeByProject.reduce(
|
||||
(acc, curr) => acc + (curr._sum.duration || 0),
|
||||
0
|
||||
);
|
||||
|
||||
return json({
|
||||
user,
|
||||
timeByProject,
|
||||
total,
|
||||
timeByProject: await getTimeEntriesByDateAndProject({
|
||||
userId: user.id,
|
||||
dateFrom,
|
||||
dateTo
|
||||
}),
|
||||
projects: await getProjects({ userId: user.id })
|
||||
});
|
||||
}
|
||||
|
|
@ -236,8 +228,8 @@ export default function ReportPage() {
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th scope="col">Time</th>
|
||||
{hourlyRate && <th scope="col">Billing</th>}
|
||||
<th>Time</th>
|
||||
{hourlyRate && <th>Billing</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -248,7 +240,7 @@ export default function ReportPage() {
|
|||
}[]
|
||||
).map((projectData) => (
|
||||
<tr key={projectData.projectId}>
|
||||
<td scope="row">
|
||||
<td>
|
||||
<Flex align="center">
|
||||
<ColorSwatch
|
||||
mr="sm"
|
||||
|
|
@ -280,25 +272,6 @@ export default function ReportPage() {
|
|||
</tr>
|
||||
))}
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
Loading…
Reference in a new issue