Compare commits

...

726 commits

Author SHA1 Message Date
renovate[bot]
eae4df7d06
chore(deps): update dependency eslint-plugin-react to v7.37.4 2025-01-23 13:57:18 +00:00
renovate[bot]
7a2be2e8a7
chore(deps): update dependency @types/node to v20.17.16 2025-01-23 09:28:53 +00:00
renovate[bot]
0731b8775b
chore(deps): update dependency @types/react to v18.3.18 2025-01-23 00:54:08 +00:00
renovate[bot]
6f65299dba
chore(deps): update dependency @types/node to v20.17.14 2025-01-22 21:26:50 +00:00
renovate[bot]
c55f4b392e
chore(deps): update dependency @types/lodash to v4.17.14 2025-01-22 17:49:30 +00:00
renovate[bot]
b7f59a37cf
chore(deps): update dependency @sentry/node to v7.120.3 (#337)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 13:31:27 +00:00
renovate[bot]
cb255fa417
chore(deps): update dependency vite to v5.4.12 [security] 2025-01-22 10:06:31 +00:00
renovate[bot]
eecd241970
chore(deps): update radix-ui-primitives monorepo 2024-12-13 02:06:31 +00:00
renovate[bot]
716265de5b
chore(deps): update dependency recharts to v2.15.0 2024-12-12 23:35:19 +00:00
renovate[bot]
afeb363742
chore(deps): update dependency @types/express to v5 2024-12-12 19:52:25 +00:00
renovate[bot]
1809527fd2
chore(deps): update postgres docker tag to v16.6 2024-12-12 16:45:34 +00:00
renovate[bot]
cb2dcdee6f
chore(deps): update opentelemetry-js monorepo 2024-12-12 14:49:16 +00:00
renovate[bot]
da9780a4c9
chore(deps): update docker/dockerfile docker tag to v1.12 2024-12-12 09:47:37 +00:00
renovate[bot]
f43ff30f65
chore(deps): update dependency typescript to v5.7.2 2024-12-12 06:19:36 +00:00
renovate[bot]
1575932035
chore(deps): update dependency recharts to v2.14.1 2024-12-12 04:15:46 +00:00
renovate[bot]
590bdc6dd7
chore(deps): update dependency prettier to v3.4.2 2024-12-12 00:11:55 +00:00
renovate[bot]
725458141c
chore(deps): update dependency lucide-react to v0.468.0 2024-12-11 22:22:45 +00:00
renovate[bot]
b79a18c7f3
chore(deps): update dependency @testing-library/react to v16.1.0 2024-12-11 18:16:48 +00:00
renovate[bot]
287d2e2055
chore(deps): update dependency @opentelemetry/instrumentation-pino to v0.45.0 2024-12-11 16:02:14 +00:00
renovate[bot]
9e1ae77b06
chore(deps): update dependency @types/node to v20.17.10 2024-12-11 13:08:40 +00:00
renovate[bot]
f80946425d
chore(deps): update dependency @opentelemetry/instrumentation-pg to v0.49.0 2024-12-11 09:17:59 +00:00
renovate[bot]
870893d066
chore(deps): update dependency @opentelemetry/instrumentation-nestjs-core to v0.43.0 2024-12-11 06:29:47 +00:00
renovate[bot]
cd47efce04
chore(deps): update react monorepo 2024-12-11 05:00:33 +00:00
renovate[bot]
7943ba4ed8
chore(deps): update traefik docker tag to v2.11.15 2024-12-11 02:02:42 +00:00
renovate[bot]
058948812b
chore(deps): update dependency @opentelemetry/instrumentation-express to v0.46.0 2024-12-10 22:58:10 +00:00
renovate[bot]
abbd3d6dd6
chore(deps): update dependency @opentelemetry/instrumentation-dns to v0.42.0 2024-12-10 20:39:34 +00:00
renovate[bot]
ace650aaeb
chore(deps): update traefik docker tag to v2.11.14 2024-12-10 15:15:20 +00:00
renovate[bot]
c6cda6213c
chore(deps): update react monorepo 2024-12-10 09:26:58 +00:00
renovate[bot]
602bbb21b3
chore(deps): update nest monorepo 2024-12-10 06:04:12 +00:00
renovate[bot]
8486ddfb06
chore(deps): update grafana/promtail docker tag to v2.9.11 2024-12-10 04:04:21 +00:00
renovate[bot]
e8f27d836b
chore(deps): update dependency tailwindcss to v3.4.16 2024-12-10 01:51:11 +00:00
renovate[bot]
bcdd0cbfee
chore(deps): update dependency class-variance-authority to v0.7.1 2024-12-09 22:57:00 +00:00
renovate[bot]
cd749139d0
chore(deps): update dependency axios to v1.7.9 2024-12-09 19:00:25 +00:00
renovate[bot]
37f54786f2
chore(deps): update dependency @vitejs/plugin-react to v4.3.4 2024-12-09 17:34:44 +00:00
renovate[bot]
b7dbdbd8b0
chore(deps): update dependency @types/node to v20.17.9 2024-12-09 13:12:23 +00:00
renovate[bot]
a8302e175a
chore(deps): update dependency @types/cookie-parser to v1.4.8 2024-12-09 11:34:53 +00:00
renovate[bot]
292054c46e
chore(deps): update dependency @sentry/node to v7.120.1 2024-12-09 08:15:49 +00:00
renovate[bot]
f6f17833bc
chore(deps): update grafana/loki docker tag to v2.9.11 (#334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 05:33:16 +00:00
renovate[bot]
3a74a1cc2f
chore(deps): update grafana/grafana-oss docker tag to v10.4.14 (#333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 01:12:58 +00:00
renovate[bot]
b3b2d9f09a
chore(deps): update dependency @nestjs/axios to v3.1.3 2024-12-08 23:46:08 +00:00
renovate[bot]
07c00c2e9d
chore(deps): update grafana/grafana-oss docker tag to v10.4.13 2024-11-19 16:17:26 +00:00
renovate[bot]
147ee19921
chore(deps): update prom/prometheus docker tag to v2.55.1 2024-11-19 13:07:21 +00:00
renovate[bot]
dc78a4dcb9
chore(deps): update postgres docker tag to v16.5 2024-11-19 09:08:03 +00:00
renovate[bot]
da8c778470
chore(deps): update dependency @opentelemetry/instrumentation-pino to v0.44.0 2024-11-19 07:52:11 +00:00
renovate[bot]
6c19584e28
chore(deps): update dependency @opentelemetry/instrumentation-pg to v0.48.0 2024-11-19 03:44:15 +00:00
renovate[bot]
4b46bf227e
chore(deps): update dependency @opentelemetry/instrumentation-nestjs-core to v0.42.0 2024-11-19 00:23:05 +00:00
renovate[bot]
335f90b90d
chore(deps): update dependency @opentelemetry/instrumentation-express to v0.45.0 2024-11-18 22:31:25 +00:00
renovate[bot]
38432afb95
chore(deps): update dependency @opentelemetry/instrumentation-dns to v0.41.0 2024-11-18 20:26:17 +00:00
renovate[bot]
0fab34227d
chore(deps): update opentelemetry-js monorepo 2024-11-18 16:17:36 +00:00
renovate[bot]
ba0eefdcea
chore(deps): update docker/dockerfile docker tag to v1.11 2024-11-18 12:52:35 +00:00
renovate[bot]
c81fa2b217
chore(deps): update dependency recharts to v2.13.3 2024-11-18 10:32:06 +00:00
renovate[bot]
228f26dada
chore(deps): update dependency react-router-dom to v6.28.0 2024-11-18 08:05:42 +00:00
renovate[bot]
529454fbf3
chore(deps): update dependency lucide-react to v0.460.0 2024-11-18 04:57:53 +00:00
renovate[bot]
4d3780211b
chore(deps): update dependency eslint-plugin-react to v7.37.2 2024-11-18 02:20:59 +00:00
renovate[bot]
4f84edb26a
chore(deps): update dependency eslint-plugin-import to v2.31.0 2024-11-17 21:02:17 +00:00
renovate[bot]
31a22951cb
chore(deps): update dependency @types/node to v20.17.6 2024-11-17 19:52:34 +00:00
renovate[bot]
99a8c32486
chore(deps): update dependency @testing-library/jest-dom to v6.6.3 2024-11-17 15:56:35 +00:00
renovate[bot]
716e3f3879
chore(deps): update dependency @sentry/node to v7.120.0 2024-11-17 13:40:23 +00:00
renovate[bot]
6e0fcd8e29
chore(deps): update dependency @opentelemetry/instrumentation-pino to v0.43.0 2024-11-17 09:38:16 +00:00
renovate[bot]
0b7b94282c
chore(deps): update dependency @opentelemetry/instrumentation-pg to v0.47.1 2024-11-17 06:26:24 +00:00
renovate[bot]
d1a317aceb
chore(deps): update dependency @opentelemetry/instrumentation-nestjs-core to v0.41.0 2024-11-17 04:46:29 +00:00
renovate[bot]
d5b384098b
chore(deps): update dependency @opentelemetry/instrumentation-express to v0.44.0 2024-11-17 00:21:13 +00:00
renovate[bot]
d97ac14b89
chore(deps): update dependency @opentelemetry/instrumentation-dns to v0.40.0 2024-11-16 22:21:15 +00:00
renovate[bot]
77931d94a4
chore(deps): update dependency @nestjs/config to v3.3.0 2024-11-16 18:56:24 +00:00
renovate[bot]
4dbc49277c
chore(deps): update dependency @nestjs/axios to v3.1.2 2024-11-16 16:39:32 +00:00
renovate[bot]
ab12aa70a4
chore(deps): update traefik docker tag to v2.11.13 2024-11-16 13:28:14 +00:00
renovate[bot]
60b538ef00
chore(deps): update react monorepo 2024-11-16 10:36:24 +00:00
renovate[bot]
6363cf8567
chore(deps): update radix-ui-primitives monorepo 2024-11-16 07:35:57 +00:00
renovate[bot]
16f82dcffc
chore(deps): update nest monorepo 2024-11-16 04:43:14 +00:00
renovate[bot]
58d4282723
chore(deps): update grafana/tempo docker tag to v2.6.1 2024-11-16 01:45:04 +00:00
renovate[bot]
805aa2b0b6
chore(deps): update grafana/grafana-oss docker tag to v10.4.12 2024-11-15 22:26:45 +00:00
renovate[bot]
2e572d526f
chore(deps): update dependency vite to v5.4.11 2024-11-15 19:39:58 +00:00
renovate[bot]
927a54f247
chore(deps): update dependency typescript to v5.6.3 2024-11-15 17:14:16 +00:00
renovate[bot]
8dc79d4b22
chore(deps): update dependency tailwindcss to v3.4.15 2024-11-15 14:15:07 +00:00
renovate[bot]
f034b1cd1a
chore(deps): update dependency postcss to v8.4.49 2024-11-15 09:53:47 +00:00
renovate[bot]
c188bd030f
chore(deps): update dependency pg to v8.13.1 2024-11-15 06:49:31 +00:00
renovate[bot]
9036547105
chore(deps): update dependency eslint-plugin-jsx-a11y to v6.10.2 2024-11-15 03:17:52 +00:00
renovate[bot]
01f6043fdf
chore(deps): update dependency cookie-parser to v1.4.7 2024-11-15 01:12:08 +00:00
renovate[bot]
6123b62e94
chore(deps): update dependency @vitejs/plugin-react to v4.3.3 2024-11-14 21:15:33 +00:00
renovate[bot]
aed7d408f4
chore(deps): update dependency @types/lodash to v4.17.13 2024-11-14 18:40:36 +00:00
renovate[bot]
593bc32b96
chore(deps): update dependency @types/jest to v29.5.14 2024-11-14 17:23:51 +00:00
renovate[bot]
eee615bec0
chore(deps): update traefik docker tag to v2.11.10 2024-09-19 23:09:31 +00:00
renovate[bot]
5badd50e6b
chore(deps): update dependency @types/react to v18.3.8 2024-09-19 18:56:49 +00:00
renovate[bot]
cdb9c95d8e
chore(deps): update dependency @testing-library/react to v16 2024-09-19 16:48:12 +00:00
renovate[bot]
8f1adc3811
chore(deps): update grafana/tempo docker tag to v2.6.0 2024-09-19 13:31:06 +00:00
renovate[bot]
e02b29d98d
chore(deps): update docker/dockerfile docker tag to v1.10 2024-09-19 09:33:31 +00:00
renovate[bot]
a7dfa6b3b3
chore(deps): update dependency typescript to v5.6.2 2024-09-19 08:36:17 +00:00
renovate[bot]
f93dec6bc3
chore(deps): update dependency pg to v8.13.0 2024-09-19 04:47:44 +00:00
renovate[bot]
fb0c367b09
chore(deps): update dependency lucide-react to v0.441.0 2024-09-19 01:41:00 +00:00
renovate[bot]
2de40e010b
chore(deps): update dependency eslint-plugin-react to v7.36.1 2024-09-18 21:53:27 +00:00
renovate[bot]
4aec5c2576
chore(deps): update dependency eslint-plugin-jsx-a11y to v6.10.0 2024-09-18 18:24:11 +00:00
renovate[bot]
5225764be9
chore(deps): update dependency eslint-plugin-import to v2.30.0 2024-09-18 16:27:53 +00:00
renovate[bot]
31ae8743e6
chore(deps): update dependency @opentelemetry/instrumentation-pino to v0.42.0 2024-09-18 14:35:58 +00:00
renovate[bot]
169af3d246
chore(deps): update dependency @opentelemetry/instrumentation-pg to v0.44.0 2024-09-18 11:29:15 +00:00
renovate[bot]
c05a8e84f5
chore(deps): update dependency @opentelemetry/instrumentation-nestjs-core to v0.40.0 2024-09-18 07:14:13 +00:00
renovate[bot]
ebadc6498e
chore(deps): update dependency @opentelemetry/instrumentation-express to v0.42.0 2024-09-18 04:05:20 +00:00
renovate[bot]
1dc59c3e2c
chore(deps): update dependency @opentelemetry/instrumentation-dns to v0.39.0 2024-09-18 01:15:49 +00:00
renovate[bot]
714467e43d
chore(deps): update traefik docker tag to v2.11.9 2024-09-17 21:38:23 +00:00
renovate[bot]
e7fd293459
chore(deps): update dependency tailwindcss to v3.4.12 2024-09-17 18:46:01 +00:00
renovate[bot]
d34d6b2c6a
chore(deps): update dependency @types/react to v18.3.7 2024-09-17 16:51:28 +00:00
renovate[bot]
33d8e29cb2
chore(deps): update dependency @nestjs/swagger to v7.4.2 2024-09-17 12:10:36 +00:00
renovate[bot]
00f3883930
chore(deps): update nest monorepo to v10.4.3 2024-09-17 10:27:25 +00:00
renovate[bot]
a08ab594ab
chore(deps): update dependency vite to v5.4.6 2024-09-17 04:51:45 +00:00
renovate[bot]
fd6d7905e4
chore(deps): update dependency tailwindcss to v3.4.11 2024-09-17 01:05:04 +00:00
renovate[bot]
0a9ab9815e
chore(deps): update dependency react-router-dom to v6.26.2 2024-09-16 21:03:04 +00:00
renovate[bot]
a81bf71d12
chore(deps): update dependency postcss to v8.4.47 2024-09-16 19:26:15 +00:00
renovate[bot]
3912aef540
chore(deps): update dependency eslint to v8.57.1 2024-09-16 15:49:47 +00:00
renovate[bot]
cbb44f0000
chore(deps): update dependency @types/react to v18.3.6 2024-09-16 14:34:22 +00:00
renovate[bot]
ebac91e007
chore(deps): update dependency @nestjs/swagger to v7.4.1 2024-09-16 11:19:02 +00:00
renovate[bot]
7bce9b9ae2
chore(deps): update dependency @types/node to v20.16.5 2024-09-16 06:05:26 +00:00
renovate[bot]
6f883f3388
chore(deps): update dependency @types/jest to v29.5.13 2024-09-16 03:27:43 +00:00
renovate[bot]
a8b1664d81
chore(deps): update dependency postcss to v8.4.42 2024-09-01 04:30:41 +00:00
renovate[bot]
7812b21766
chore(deps): update dependency axios to v1.7.7 2024-09-01 01:55:36 +00:00
renovate[bot]
d429c0b05a
chore(deps): update testing-library monorepo 2024-08-31 21:27:29 +00:00
renovate[bot]
326ec79732
chore(deps): update sentry 2024-08-31 20:12:13 +00:00
renovate[bot]
d940b2cb40
chore(deps): update react monorepo 2024-08-31 15:36:57 +00:00
renovate[bot]
a015c70f32
chore(deps): update dependency lucide-react to v0.437.0 2024-08-31 12:37:18 +00:00
renovate[bot]
a273d9d74e
chore(deps): update radix-ui-primitives monorepo 2024-08-31 10:50:47 +00:00
renovate[bot]
3eb53a1f95
chore(deps): update prom/prometheus docker tag to v2.54.1 2024-08-31 06:29:27 +00:00
renovate[bot]
74ff62da23
chore(deps): update postgres docker tag to v16.4 2024-08-31 05:07:29 +00:00
renovate[bot]
ac6e17b0ae
chore(deps): update pino 2024-08-31 01:50:37 +00:00
renovate[bot]
c29d382b57
chore(deps): update dependency axios to v1.7.6 2024-08-30 23:00:19 +00:00
renovate[bot]
7b6274ad58
chore(deps): update opentelemetry-js monorepo 2024-08-30 19:10:02 +00:00
renovate[bot]
f0a9c60dac
chore(deps): update nest monorepo 2024-08-30 16:34:31 +00:00
renovate[bot]
06d76ff954
chore(deps): update grafana/tempo docker tag to v2.5.0 2024-08-30 13:52:10 +00:00
renovate[bot]
1d9f2d4dfa
chore(deps): update dependency @nestjs/cli to v10.4.5 2024-08-30 10:38:24 +00:00
renovate[bot]
66f9f966a8
chore(deps): update docker/dockerfile docker tag to v1.9 2024-08-30 06:04:17 +00:00
renovate[bot]
66321a0e97
chore(deps): update dependency vitest to v1.6.0 2024-08-30 04:49:04 +00:00
renovate[bot]
827fc8ee3b
chore(deps): update dependency vite to v5.4.2 2024-08-30 02:05:49 +00:00
renovate[bot]
9cc7ad417c
chore(deps): update dependency typescript to v5.5.4 2024-08-29 22:20:51 +00:00
renovate[bot]
5b8fa3a5fd
chore(deps): update dependency ts-jest to v29.2.5 2024-08-29 22:13:14 +00:00
renovate[bot]
66ff74767c
chore(deps): update dependency react-router-dom to v6.26.1 2024-08-29 18:14:37 +00:00
renovate[bot]
43c1fa772a
chore(deps): update dependency prettier to v3.3.3 2024-08-29 15:33:06 +00:00
renovate[bot]
d175c81f76
chore(deps): update dependency pg to v8.12.0 2024-08-29 14:04:53 +00:00
renovate[bot]
5861cda91a
chore(deps): update dependency lucide-react to v0.436.0 2024-08-29 11:06:23 +00:00
renovate[bot]
a7e09b65e7
chore(deps): update dependency joi to v17.13.3 2024-08-29 06:25:27 +00:00
renovate[bot]
e7a38d319f
chore(deps): update dependency eslint-plugin-react to v7.35.0 2024-08-29 04:52:22 +00:00
renovate[bot]
ab9df4519c
chore(deps): update dependency eslint-plugin-jsx-a11y to v6.9.0 2024-08-29 01:07:24 +00:00
renovate[bot]
72d054d230
chore(deps): update dependency eslint-plugin-jsdoc to v48.11.0 2024-08-28 21:29:43 +00:00
renovate[bot]
ebb12805da
chore(deps): update dependency @vitejs/plugin-react to v4.3.1 2024-08-28 19:02:23 +00:00
renovate[bot]
e1904d44a9
chore(deps): update dependency @types/node to v20.16.2 2024-08-28 17:51:16 +00:00
renovate[bot]
ee1b52736d
chore(deps): update dependency @opentelemetry/instrumentation-pino to v0.41.0 2024-08-28 12:06:10 +00:00
renovate[bot]
5c4ffa69cb
chore(deps): update dependency @opentelemetry/instrumentation-pg to v0.43.0 2024-08-28 08:09:32 +00:00
renovate[bot]
6d5cae2646
chore(deps): update dependency @opentelemetry/instrumentation-nestjs-core to v0.39.0 2024-08-28 03:06:22 +00:00
renovate[bot]
37fc1bc3ab
chore(deps): update dependency @opentelemetry/instrumentation-express to v0.41.1 2024-08-28 00:41:16 +00:00
renovate[bot]
eeeb4b1b86
chore(deps): update dependency @opentelemetry/instrumentation-dns to v0.38.0 2024-08-27 22:58:44 +00:00
renovate[bot]
af2268a285
chore(deps): update dependency @nestjs/swagger to v7.4.0 2024-08-27 18:43:17 +00:00
renovate[bot]
c336c80941
chore(deps): update dependency @nestjs/cli to v10.4.4 2024-08-27 16:19:48 +00:00
renovate[bot]
f9926d31e9
chore(deps): update grafana/grafana-oss docker tag to v10.4.8 2024-08-27 13:59:37 +00:00
renovate[bot]
6fc1583d89
chore(deps): update traefik docker tag to v2.11.8 2024-08-27 10:33:02 +00:00
renovate[bot]
3001b25908
chore(deps): update grafana/promtail docker tag to v2.9.10 2024-08-27 06:49:06 +00:00
renovate[bot]
7017dd8f2d
chore(deps): update grafana/loki docker tag to v2.9.10 2024-08-27 04:51:02 +00:00
renovate[bot]
fd2db3144d
chore(deps): update grafana/grafana-oss docker tag to v10.4.7 2024-08-27 01:52:21 +00:00
renovate[bot]
464799e6d7
chore(deps): update dependency tailwindcss to v3.4.10 2024-08-26 22:51:36 +00:00
renovate[bot]
894d35fde6
chore(deps): update dependency rimraf to v5.0.10 2024-08-26 18:34:00 +00:00
renovate[bot]
ed8d432c0f
chore(deps): update dependency recharts to v2.12.7 2024-08-26 17:06:16 +00:00
renovate[bot]
df92e3e252
chore(deps): update dependency react-files to v3.0.3 2024-08-26 12:17:46 +00:00
renovate[bot]
e50a9df39f
chore(deps): update dependency postcss to v8.4.41 2024-08-26 09:27:50 +00:00
renovate[bot]
3a9b9c1588
chore(deps): update dependency clsx to v2.1.1 2024-08-26 08:02:24 +00:00
renovate[bot]
96d83ee36d
chore(deps): update dependency axios to v1.7.5 2024-08-26 04:42:06 +00:00
renovate[bot]
292c8fcfdb
chore(deps): update dependency autoprefixer to v10.4.20 2024-08-26 01:56:20 +00:00
renovate[bot]
f3dac1720b
chore(deps): update dependency @types/lodash to v4.17.7 2024-08-25 22:07:57 +00:00
renovate[bot]
16181a21e6
chore(deps): update dependency @nestjs/serve-static to v4.0.2 2024-08-25 20:18:23 +00:00
renovate[bot]
1ae3c73214
chore(deps): update dependency @nestjs/config to v3.2.3 2024-08-25 17:17:39 +00:00
renovate[bot]
b6b47836ae
chore(deps): update dependency @nestjs/axios to v3.0.3 2024-08-25 14:24:22 +00:00
renovate[bot]
78f9f8fbe7
chore(deps): update dependency axios to v1.7.4 [security] 2024-08-13 20:22:49 +00:00
renovate[bot]
b8e7c0b4a4
chore(deps): update dependency vite to v5.1.7 [security] 2024-04-03 20:07:07 +00:00
renovate[bot]
b85818fec4
chore(deps): update prom/prometheus docker tag to v2.50.1 2024-03-16 13:08:46 +00:00
renovate[bot]
109995ff68
chore(deps): update opentelemetry-js monorepo 2024-03-16 09:55:19 +00:00
renovate[bot]
888deaac26
chore(deps): update grafana/tempo docker tag to v2.4.0 2024-03-16 06:03:46 +00:00
renovate[bot]
617a2e5b2f
chore(deps): update grafana/grafana-oss docker tag to v10.4.0 2024-03-16 04:25:45 +00:00
renovate[bot]
930347e83f
chore(deps): update dependency recharts to v2.12.3 2024-03-16 02:12:09 +00:00
renovate[bot]
4a1ec3791b
chore(deps): update dependency eslint-plugin-react to v7.34.1 2024-03-15 21:32:18 +00:00
renovate[bot]
1657b2c835
chore(deps): update dependency axios to v1.6.8 2024-03-15 16:45:05 +00:00
renovate[bot]
2657c84a84
chore(deps): update dependency vitest to v1.4.0 2024-03-15 12:54:10 +00:00
renovate[bot]
379a4b854a
chore(deps): update dependency @types/node to v20.11.28 2024-03-15 09:12:33 +00:00
renovate[bot]
f2c907dc18
chore(deps): update docker/dockerfile docker tag to v1.7 2024-03-15 07:04:21 +00:00
renovate[bot]
42404e8c39
chore(deps): update dependency typescript to v5.4.2 2024-03-15 03:32:35 +00:00
renovate[bot]
501f728faa
chore(deps): update dependency lucide-react to v0.358.0 2024-03-15 00:51:09 +00:00
renovate[bot]
f392d700b1
chore(deps): update dependency eslint-plugin-react to v7.34.0 2024-03-14 23:11:42 +00:00
renovate[bot]
d3321ce2d5
chore(deps): update dependency eslint-plugin-jsdoc to v48.2.1 2024-03-14 18:21:16 +00:00
renovate[bot]
28ed747e49
chore(deps): update dependency @types/react to v18.2.66 2024-03-14 15:40:27 +00:00
renovate[bot]
04f9408563
chore(deps): update dependency eslint to v8.57.0 2024-03-14 12:06:18 +00:00
renovate[bot]
75f891a4b9
chore(deps): update dependency @sentry/node to v7.107.0 2024-03-14 10:31:44 +00:00
renovate[bot]
32fe7202a0
chore(deps): update dependency @types/lodash to v4.17.0 2024-03-14 06:05:35 +00:00
renovate[bot]
3eb8c22f80
chore(deps): update dependency @sentry/node to v7.106.1 2024-03-14 05:07:00 +00:00
renovate[bot]
60baf98615
chore(deps): update dependency @opentelemetry/instrumentation-pino to v0.36.0 2024-03-14 02:02:45 +00:00
renovate[bot]
3789b7328b
chore(deps): update dependency @opentelemetry/instrumentation-pg to v0.39.1 2024-03-13 23:18:55 +00:00
renovate[bot]
e5339a588d
chore(deps): update dependency @types/node to v20.11.27 2024-03-13 18:02:04 +00:00
renovate[bot]
fd6810b2e5
chore(deps): update dependency @types/react-dom to v18.2.22 2024-03-13 12:18:38 +00:00
renovate[bot]
22da9a204f
chore(deps): update dependency @opentelemetry/instrumentation-nestjs-core to v0.35.0 2024-03-13 10:19:03 +00:00
renovate[bot]
78abea9fb8
chore(deps): update dependency @opentelemetry/instrumentation-express to v0.36.1 2024-03-13 07:15:02 +00:00
renovate[bot]
0d3438fc20
chore(deps): update dependency @opentelemetry/instrumentation-dns to v0.34.0 2024-03-13 03:57:30 +00:00
renovate[bot]
60750ebd28
chore(deps): update react monorepo 2024-03-13 02:01:26 +00:00
renovate[bot]
39d29d813b
chore(deps): update grafana/promtail docker tag to v2.9.5 2024-03-12 23:05:02 +00:00
renovate[bot]
99f413aa6a
chore(deps): update grafana/loki docker tag to v2.9.5 2024-03-12 18:32:27 +00:00
renovate[bot]
781db64f83
chore(deps): update dependency vitest to v1.3.1 2024-03-12 17:53:53 +00:00
renovate[bot]
b3d4c4e561
chore(deps): update dependency vite to v5.1.6 2024-03-12 13:56:39 +00:00
renovate[bot]
fb494879cd
chore(deps): update dependency recharts to v2.12.2 2024-03-12 09:47:20 +00:00
renovate[bot]
5e4e850f7e
chore(deps): update dependency react-router-dom to v6.22.3 2024-03-12 08:07:20 +00:00
renovate[bot]
e15d032458
chore(deps): update dependency joi to v17.12.2 2024-03-12 03:47:51 +00:00
renovate[bot]
edfb506d8d
chore(deps): update dependency autoprefixer to v10.4.18 2024-03-12 02:02:32 +00:00
renovate[bot]
44d46a9456
chore(deps): update dependency @types/node to v20.11.26 2024-03-11 23:00:06 +00:00
renovate[bot]
bd9e44a335
chore(deps): update dependency @types/cookie-parser to v1.4.7 2024-03-11 19:48:59 +00:00
renovate[bot]
17e185584a
chore(deps): update actions/setup-node action to v4 (#309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-11 20:45:27 +01:00
renovate[bot]
163b23dca9
chore(deps): update pino (major) (#321)
chore(deps): update pino

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-11 20:45:01 +01:00
renovate[bot]
36d0320eee
chore(deps): update traefik docker tag to v2.11.0 2024-02-19 00:20:28 +00:00
renovate[bot]
7dc1d091c7
chore(deps): update postgres docker tag to v16.2 2024-02-18 22:39:04 +00:00
renovate[bot]
90827600fc
chore(deps): update dependency @nestjs/terminus to v10.2.3 2024-02-18 18:12:03 +00:00
renovate[bot]
0399ded420
chore(deps): update dependency vitest to v1.3.0 2024-02-18 15:27:56 +00:00
renovate[bot]
99beeefc50
chore(deps): update dependency pino to v8.19.0 2024-02-18 13:50:42 +00:00
renovate[bot]
d85b400813
chore(deps): update dependency lucide-react to v0.331.0 2024-02-18 09:35:58 +00:00
renovate[bot]
428d7cf6d2
chore(deps): update dependency eslint-plugin-jsdoc to v48.1.0 2024-02-18 08:01:37 +00:00
renovate[bot]
a7424faf53
chore(deps): update dependency @sentry/node to v7.101.1 2024-02-18 04:38:06 +00:00
renovate[bot]
bbffccaaf6
chore(deps): update grafana/grafana-oss docker tag to v10.3.3 2024-02-18 00:45:32 +00:00
renovate[bot]
ffeede779f
chore(deps): update dependency vite to v5.1.3 2024-02-17 22:01:34 +00:00
renovate[bot]
9cea913af7
chore(deps): update dependency react-router-dom to v6.22.1 2024-02-17 18:52:33 +00:00
renovate[bot]
c585f395d8
chore(deps): update dependency @types/react to v18.2.56 2024-02-17 16:16:38 +00:00
renovate[bot]
9db08e17aa
chore(deps): update dependency @types/node to v20.11.19 2024-02-17 13:59:34 +00:00
renovate[bot]
7aaac70657
chore(deps): update dependency lucide-react to v0.330.0 2024-02-12 19:19:26 +00:00
renovate[bot]
601501c9e2
chore(deps): update nest monorepo to v10.3.3 2024-02-12 10:10:01 +00:00
renovate[bot]
3eaac9a102
chore(deps): lock file maintenance 2024-02-12 03:17:24 +00:00
renovate[bot]
b64c3050db
chore(deps): update dependency lucide-react to v0.325.0 2024-02-12 00:56:49 +00:00
renovate[bot]
ff1e1a3aaf
chore(deps): update dependency lucide-react to v0.324.0 2024-02-11 18:13:08 +00:00
renovate[bot]
ad5705ec93
chore(deps): update dependency vite to v5.1.1 2024-02-09 10:36:41 +00:00
renovate[bot]
60b3eb3740
chore(deps): update dependency recharts to v2.12.0 2024-02-09 04:47:48 +00:00
renovate[bot]
6c59303481
chore(deps): update dependency @types/node to v20.11.17 2024-02-08 22:50:09 +00:00
renovate[bot]
cc91a97d08
chore(deps): update dependency vite to v5.1.0 2024-02-08 15:24:31 +00:00
renovate[bot]
9f52138e5c
chore(deps): update dependency @nestjs/swagger to v7.3.0 2024-02-08 13:24:37 +00:00
renovate[bot]
4798f7277f
chore(deps): update dependency @nestjs/config to v3.2.0 2024-02-08 09:37:13 +00:00
renovate[bot]
f8cb1b58b8
chore(deps): update nest monorepo 2024-02-08 06:43:11 +00:00
renovate[bot]
911284b1c1
chore(deps): update dependency postcss to v8.4.35 2024-02-08 03:47:58 +00:00
renovate[bot]
21b520807e
chore(deps): update dependency @types/react-dom to v18.2.19 2024-02-08 00:30:33 +00:00
renovate[bot]
80ac3480b2
chore(deps): update dependency @sentry/node to v7.100.1 2024-02-07 21:54:54 +00:00
renovate[bot]
d0718f1789
chore(deps): update dependency @nestjs/typeorm to v10.0.2 2024-02-07 18:15:25 +00:00
renovate[bot]
aea78142e5
chore(deps): update dependency @nestjs/serve-static to v4.0.1 2024-02-07 15:44:20 +00:00
renovate[bot]
950f103fe0
chore(deps): update dependency @nestjs/cli to v10.3.2 2024-02-07 13:27:56 +00:00
renovate[bot]
fb21bd8e8c
chore(deps): update dependency @nestjs/axios to v3.0.2 2024-02-07 10:22:31 +00:00
renovate[bot]
53d6578042
chore(deps): update dependency eslint-plugin-jsdoc to v48.0.6 2024-02-06 19:06:09 +00:00
renovate[bot]
e677a37eaa
chore(deps): update dependency @sentry/node to v7.100.0 2024-02-06 16:28:27 +00:00
renovate[bot]
76f959463f
chore(deps): update typescript-eslint monorepo to v6.21.0 2024-02-06 13:55:48 +00:00
renovate[bot]
38bebbcf34
chore(deps): update dependency @types/react to v18.2.55 2024-02-06 10:08:10 +00:00
renovate[bot]
067b557092
chore(deps): update dependency postcss to v8.4.34 2024-02-06 07:31:13 +00:00
renovate[bot]
26e201e64e
chore(deps): update dependency eslint-plugin-jsdoc to v48.0.5 2024-02-06 04:16:28 +00:00
renovate[bot]
2079e4fdfe
chore(deps): update dependency @types/react to v18.2.54 2024-02-06 01:14:19 +00:00
renovate[bot]
da9bb7773c
chore(deps): update dependency lucide-react to v0.323.0 2024-02-05 21:01:28 +00:00
renovate[bot]
b31d0963bc
chore(deps): update dependency @testing-library/jest-dom to v6.4.2 2024-02-05 15:07:00 +00:00
renovate[bot]
9f61ad0688
chore(deps): update dependency @nestjs/cli to v10.3.1 2024-02-05 09:04:46 +00:00
renovate[bot]
9a4320fea7
chore(deps): update dependency @types/react to v18.2.53 2024-02-05 06:24:03 +00:00
renovate[bot]
7d76e5c633
chore(deps): lock file maintenance 2024-02-05 01:26:40 +00:00
renovate[bot]
cee277370d
chore(deps): update dependency prettier to v3.2.5 2024-02-04 06:57:04 +00:00
renovate[bot]
720e408c3c
chore(deps): update dependency @types/react to v18.2.52 2024-02-03 04:22:55 +00:00
renovate[bot]
56a988241d
chore(deps): update dependency react-router-dom to v6.22.0 2024-02-02 11:15:29 +00:00
renovate[bot]
0c7881304e
chore(deps): update dependency pino to v8.18.0 2024-02-02 06:28:32 +00:00
renovate[bot]
be28251218
chore(deps): update dependency lucide-react to v0.321.0 2024-02-02 04:55:44 +00:00
renovate[bot]
99fbee8549
chore(deps): update dependency @types/jest to v29.5.12 2024-02-02 00:55:19 +00:00
renovate[bot]
9dccc96135
chore(deps): update dependency @testing-library/react to v14.2.1 2024-02-01 22:33:51 +00:00
renovate[bot]
31805a714f
chore(deps): update dependency @types/node to v20.11.16 2024-02-01 18:25:39 +00:00
renovate[bot]
c7750831d0
chore(deps): update dependency @types/react to v18.2.51 2024-02-01 12:01:58 +00:00
renovate[bot]
d849d57d03
chore(deps): update dependency @testing-library/jest-dom to v6.4.1 2024-02-01 01:20:28 +00:00
renovate[bot]
683c33ddc3
chore(deps): update dependency @types/node to v20.11.14 2024-01-31 22:44:31 +00:00
renovate[bot]
e4e0ee1562
chore(deps): update dependency lucide-react to v0.320.0 2024-01-31 10:50:24 +00:00
renovate[bot]
1fe398d914
chore(deps): update dependency @types/node to v20.11.13 2024-01-31 00:28:50 +00:00
renovate[bot]
1176d1cdad
chore(deps): update dependency @testing-library/react to v14.2.0 2024-01-30 22:05:06 +00:00
renovate[bot]
e9c23f215d
chore(deps): update typescript-eslint monorepo to v6.20.0 2024-01-30 18:18:45 +00:00
renovate[bot]
798f288b72
chore(deps): update dependency @sentry/node to v7.99.0 2024-01-30 17:16:44 +00:00
renovate[bot]
953138f382
chore(deps): update dependency lucide-react to v0.319.0 2024-01-30 12:05:20 +00:00
renovate[bot]
accf4bb0ed
chore(deps): update dependency @testing-library/jest-dom to v6.4.0 2024-01-30 08:41:15 +00:00
renovate[bot]
61d9ba4c2e
chore(deps): update dependency @opentelemetry/instrumentation-pino to v0.35.0 2024-01-30 03:30:38 +00:00
renovate[bot]
db2844b37e
chore(deps): update dependency @opentelemetry/instrumentation-pg to v0.38.0 2024-01-30 00:49:14 +00:00
renovate[bot]
f5d880c5b6
chore(deps): update dependency @opentelemetry/instrumentation-nestjs-core to v0.34.0 2024-01-29 23:18:21 +00:00
renovate[bot]
f83e117062
chore(deps): update dependency @opentelemetry/instrumentation-express to v0.35.0 2024-01-29 20:33:40 +00:00
renovate[bot]
b4b90d8d36
chore(deps): update dependency joi to v17.12.1 2024-01-29 16:21:12 +00:00
renovate[bot]
c80ad31cf1
chore(deps): update dependency @opentelemetry/instrumentation-dns to v0.33.0 2024-01-29 12:20:17 +00:00
renovate[bot]
ab5ffdb006
chore(deps): lock file maintenance 2024-01-29 07:00:26 +00:00
renovate[bot]
89edd6ebc4
chore(deps): update dependency @types/node to v20.11.10 2024-01-29 00:57:01 +00:00
renovate[bot]
70e5068d15
chore(deps): update dependency @types/node to v20.11.9 2024-01-28 13:12:20 +00:00
renovate[bot]
3cd97d597a
chore(deps): update dependency @types/node to v20.11.8 2024-01-27 16:19:58 +00:00
renovate[bot]
f65747d580
chore(deps): update dependency recharts to v2.11.0 2024-01-27 13:10:41 +00:00
renovate[bot]
cb71525141
chore(deps): update opentelemetry-js monorepo 2024-01-27 00:31:06 +00:00
renovate[bot]
a82822ee5e
chore(deps): update dependency @nestjs/terminus to v10.2.1 2024-01-26 21:13:38 +00:00
renovate[bot]
1f36e23686
chore(deps): update dependency vitest to v1.2.2 2024-01-26 16:57:35 +00:00
renovate[bot]
a64cf83367
chore(deps): update dependency typeorm to v0.3.20 2024-01-26 14:43:40 +00:00
renovate[bot]
7017294d14
chore(deps): update dependency lucide-react to v0.316.0 2024-01-26 11:16:11 +00:00
renovate[bot]
1cee3538f2 chore(deps): update dependency @types/passport-jwt to v4 2024-01-26 06:49:28 +00:00
renovate[bot]
033bc37d06
chore(deps): update dependency @types/node to v20.11.7 2024-01-26 05:19:05 +00:00
renovate[bot]
6631da6af7
chore(deps): update dependency eslint-plugin-jsdoc to v48.0.4 2024-01-26 02:17:13 +00:00
renovate[bot]
8d4907f4c9
chore(deps): update dependency axios to v1.6.7 2024-01-25 20:16:46 +00:00
renovate[bot]
694d9b0740
chore(deps): update dependency @sentry/node to v7.98.0 2024-01-25 15:45:55 +00:00
renovate[bot]
49fd8040e6
chore(deps): update dependency lucide-react to v0.315.0 2024-01-25 08:02:11 +00:00
renovate[bot]
8168a7d811
chore(deps): update grafana/promtail docker tag to v2.9.4 2024-01-25 04:34:21 +00:00
renovate[bot]
2ae7afb11e
chore(deps): update dependency axios to v1.6.6 2024-01-25 01:10:40 +00:00
renovate[bot]
02d0c850a2
chore(deps): update dependency eslint-plugin-jsdoc to v48.0.3 2024-01-24 22:42:51 +00:00
renovate[bot]
c5664db5a5
chore(deps): update grafana/loki docker tag to v2.9.4 2024-01-24 18:28:45 +00:00
renovate[bot]
e06b14c20f
chore(deps): update dependency @testing-library/jest-dom to v6.3.0 2024-01-24 14:25:43 +00:00
renovate[bot]
61623480ed
chore(deps): update dependency @types/node to v20.11.6 2024-01-24 07:01:01 +00:00
renovate[bot]
4b309ba72e
chore(deps): update dependency @sentry/node to v7.95.0 2024-01-23 21:10:04 +00:00
renovate[bot]
863cefadc1
chore(deps): update grafana/grafana-oss docker tag to v10.3.1 2024-01-23 18:06:23 +00:00
renovate[bot]
50c8852115
chore(deps): update nest monorepo to v10.3.1 2024-01-23 15:43:23 +00:00
renovate[bot]
6ce8ff5365
chore(deps): update typescript-eslint monorepo to v6.19.1 2024-01-22 21:06:42 +00:00
renovate[bot]
51efd75cb2
chore(deps): update dependency @testing-library/jest-dom to v6.2.1 2024-01-22 19:53:19 +00:00
renovate[bot]
605992743d
chore(deps): update dependency ts-jest to v29.1.2 2024-01-22 13:14:42 +00:00
renovate[bot]
ad659a11fb
chore(deps): lock file maintenance 2024-01-22 01:05:35 +00:00
renovate[bot]
3b791fdf6c
chore(deps): update dependency lucide-react to v0.314.0 2024-01-21 18:33:51 +00:00
renovate[bot]
728af1b0be
chore(deps): update dependency @sentry/node to v7.94.1 2024-01-19 18:55:35 +00:00
renovate[bot]
88da7afa6c
chore(deps): update dependency vite to v5.0.12 2024-01-19 15:24:37 +00:00
renovate[bot]
11f9ab90c1
chore(deps): update dependency react-router-dom to v6.21.3 2024-01-18 22:54:41 +00:00
renovate[bot]
ec92de06b6
chore(deps): update dependency joi to v17.12.0 2024-01-18 02:06:44 +00:00
renovate[bot]
5e535ee1d7
chore(deps): update dependency lucide-react to v0.312.0 2024-01-17 21:55:55 +00:00
renovate[bot]
07d6e75f4f
chore(deps): update dependency autoprefixer to v10.4.17 2024-01-17 19:29:43 +00:00
renovate[bot]
13bd5b57c2
chore(deps): update dependency vitest to v1.2.1 2024-01-17 16:36:27 +00:00
renovate[bot]
49649aa7e7
chore(deps): update dependency prettier to v3.2.4 2024-01-17 14:12:30 +00:00
renovate[bot]
aa3d5093b7
chore(deps): update dependency @types/node to v20.11.5 2024-01-17 09:38:59 +00:00
renovate[bot]
7506c21736
chore(deps): update dependency prettier to v3.2.3 2024-01-17 04:38:05 +00:00
renovate[bot]
2aaaeecd63
chore(deps): update dependency lucide-react to v0.311.0 2024-01-16 16:19:03 +00:00
renovate[bot]
b752afe21e
chore(deps): update dependency @types/node to v20.11.4 2024-01-16 10:39:41 +00:00
renovate[bot]
2d79ffee4e
chore(deps): update typescript-eslint monorepo to v6.19.0 2024-01-16 06:57:57 +00:00
renovate[bot]
2ef1b81329
chore(deps): update prom/prometheus docker tag to v2.49.1 2024-01-16 03:14:52 +00:00
renovate[bot]
067cdbb80e
chore(deps): update dependency @types/node to v20.11.3 2024-01-16 01:13:16 +00:00
renovate[bot]
503c49395e
chore(deps): update opentelemetry-js monorepo 2024-01-15 21:03:01 +00:00
renovate[bot]
d074e24947
chore(deps): update dependency @nestjs/swagger to v7.2.0 2024-01-15 19:32:24 +00:00
renovate[bot]
9abaac1cf9
chore(deps): update dependency joi to v17.11.1 2024-01-15 15:51:42 +00:00
renovate[bot]
d88762edc5
chore(deps): update dependency @types/node to v20.11.2 2024-01-15 13:21:49 +00:00
renovate[bot]
5b72304092
chore(deps): update dependency @types/react to v18.2.48 2024-01-15 10:14:37 +00:00
renovate[bot]
85c37d4bf7
chore(deps): update dependency @types/node to v20.11.1 2024-01-15 05:30:57 +00:00
renovate[bot]
e2c46d1a9e
chore(deps): lock file maintenance 2024-01-15 02:33:54 +00:00
renovate[bot]
3481c263d2
chore(deps): update dependency supertest to v6.3.4 2024-01-14 20:05:04 +00:00
renovate[bot]
ee80c0cd7e
chore(deps): update dependency prettier to v3.2.2 2024-01-14 05:34:24 +00:00
renovate[bot]
6ec8f1b0a3
chore(deps): update dependency prettier to v3.2.1 2024-01-12 23:29:43 +00:00
renovate[bot]
afa770b3aa
chore(deps): update dependency vitest to v1.2.0 2024-01-12 19:11:50 +00:00
renovate[bot]
943b7f8203
chore(deps): update dependency class-validator to v0.14.1 2024-01-12 14:43:51 +00:00
renovate[bot]
5895b655dd
chore(deps): update dependency react-router-dom to v6.21.2 2024-01-11 18:11:43 +00:00
renovate[bot]
a006143e37
chore(deps): update dependency @types/node to v20.11.0 2024-01-11 06:40:41 +00:00
renovate[bot]
8c3cf443e6
chore(deps): update dependency @sentry/node to v7.93.0 2024-01-10 14:25:33 +00:00
renovate[bot]
51038d6d32
chore(deps): update dependency lucide-react to v0.309.0 2024-01-10 10:56:22 +00:00
renovate[bot]
79100f461a
chore(deps): update dependency lucide-react to v0.308.0 2024-01-10 02:01:03 +00:00
renovate[bot]
bc541bbe90
chore(deps): update dependency recharts to v2.10.4 2024-01-09 21:54:20 +00:00
renovate[bot]
23d5c06cdb
chore(deps): update dependency @types/node to v20.10.8 2024-01-09 19:34:42 +00:00
renovate[bot]
25846e083f
chore(deps): update typescript-eslint monorepo to v6.18.1 2024-01-08 23:02:33 +00:00
renovate[bot]
6ae5a14c47
chore(deps): update dependency @nestjs/schematics to v10.1.0 2024-01-08 14:31:47 +00:00
renovate[bot]
00c5447940
chore(deps): update dependency @nestjs/cli to v10.3.0 2024-01-08 10:40:21 +00:00
renovate[bot]
cc1a279b40
chore(deps): lock file maintenance 2024-01-08 02:13:21 +00:00
renovate[bot]
24460d1ec2
chore(deps): update dependency @types/node to v20.10.7 2024-01-07 17:02:47 +00:00
renovate[bot]
61fc4ab180
chore(deps): update dependency lucide-react to v0.307.0 2024-01-07 00:19:57 +00:00
renovate[bot]
602d48b532
chore(deps): update typescript-eslint monorepo to v6.18.0 2024-01-06 16:21:05 +00:00
renovate[bot]
2b910dcf6d
chore(deps): update dependency @types/react to v18.2.47 2024-01-06 10:27:06 +00:00
renovate[bot]
f7535059cd
chore(deps): update dependency lucide-react to v0.306.0 2024-01-06 06:31:55 +00:00
renovate[bot]
0853c92791
chore(deps): update dependency @sentry/node to v7.92.0 2024-01-06 03:45:05 +00:00
renovate[bot]
c89bbaa240
chore(deps): update dependency tailwindcss to v3.4.1 2024-01-06 00:19:34 +00:00
renovate[bot]
7e6f033ede
chore(deps): update dependency axios to v1.6.5 2024-01-05 23:18:37 +00:00
renovate[bot]
6757f6d4f8
chore(deps): update dependency vitest to v1.1.3 2024-01-05 19:28:20 +00:00
renovate[bot]
eea28e42c9
chore(deps): update dependency vite to v5.0.11 2024-01-05 15:19:05 +00:00
renovate[bot]
f3f94bfeee
chore(deps): update dependency postcss to v8.4.33 2024-01-05 14:35:22 +00:00
renovate[bot]
40501f21ee
chore(deps): update dependency @opentelemetry/instrumentation-pino to v0.34.5 2024-01-05 11:05:24 +00:00
renovate[bot]
c3eaaf5b99
chore(deps): update dependency @opentelemetry/instrumentation-pg to v0.37.2 2024-01-05 08:12:53 +00:00
renovate[bot]
6e9519c051
chore(deps): update dependency @opentelemetry/instrumentation-nestjs-core to v0.33.4 2024-01-05 03:21:09 +00:00
renovate[bot]
66e6557fc1
chore(deps): update dependency @opentelemetry/instrumentation-express to v0.34.1 2024-01-05 01:32:58 +00:00
renovate[bot]
56d37986c3
chore(deps): update dependency @opentelemetry/instrumentation-dns to v0.32.5 2024-01-04 23:18:38 +00:00
renovate[bot]
f2a41241ae
chore(deps): update dependency lucide-react to v0.304.0 2024-01-04 18:01:03 +00:00
renovate[bot]
6095375f6b
chore(deps): update dependency vitest to v1.1.2 2024-01-04 17:46:25 +00:00
renovate[bot]
7903975fcf
chore(deps): update dependency axios to v1.6.4 2024-01-04 01:26:31 +00:00
renovate[bot]
e310802a53
chore(deps): update dependency @testing-library/jest-dom to v6.2.0 2024-01-03 19:20:26 +00:00
renovate[bot]
b0e1cc5c0f
chore(deps): update dependency typeorm to v0.3.19 2024-01-03 16:11:46 +00:00
renovate[bot]
961a8928c1
chore(deps): update dependency eslint-plugin-jsdoc to v48.0.2 2024-01-03 01:34:50 +00:00
renovate[bot]
ba05034f91
chore(deps): update dependency eslint-plugin-jsdoc to v48 2024-01-02 19:34:55 +00:00
renovate[bot]
efa6abfca4
chore(deps): update typescript-eslint monorepo to v6.17.0 2024-01-01 19:17:05 +00:00
renovate[bot]
7e526128b0
chore(deps): update dependency eslint-plugin-jsdoc to v47.0.2 2024-01-01 09:42:41 +00:00
renovate[bot]
5998c39090
chore(deps): lock file maintenance 2024-01-01 00:58:32 +00:00
renovate[bot]
54fecfb482
chore(deps): update dependency eslint-plugin-jsdoc to v47 2023-12-31 22:03:29 +00:00
renovate[bot]
64de0d8df1
chore(deps): update dependency vitest to v1.1.1 2023-12-31 16:22:28 +00:00
renovate[bot]
fc4e14f1e3
chore(deps): update dependency eslint-plugin-jsdoc to v46.10.1 2023-12-30 16:18:01 +00:00
renovate[bot]
1bd3542ecf
chore(deps): update dependency @types/node to v20.10.6 2023-12-30 01:27:21 +00:00
renovate[bot]
1ed26fe252
chore(deps): update dependency clsx to v2.1.0 2023-12-29 22:18:58 +00:00
renovate[bot]
051761e6c7
chore(deps): update dependency @testing-library/user-event to v14.5.2 2023-12-29 20:02:53 +00:00
renovate[bot]
5a07521720
chore(deps): update dependency lucide-react to v0.303.0 2023-12-28 19:55:08 +00:00
renovate[bot]
0b4785b41c
chore(deps): update dependency @types/react to v18.2.46 2023-12-28 15:41:03 +00:00
renovate[bot]
391307852b
chore(deps): update dependency @types/supertest to v6.0.2 2023-12-27 22:13:31 +00:00
renovate[bot]
d3ec56363d
chore(deps): update dependency @testing-library/jest-dom to v6.1.6 2023-12-27 16:02:10 +00:00
renovate[bot]
7c64e8c2b9
chore(deps): update pino 2023-12-27 13:56:29 +00:00
renovate[bot]
d2e2dd76ea
chore(deps): update dependency axios to v1.6.3 2023-12-27 01:09:12 +00:00
renovate[bot]
b51963b1db
chore(deps): update dependency pino to v8.17.2 2023-12-26 16:28:47 +00:00
renovate[bot]
afb7d5321e
chore(deps): update typescript-eslint monorepo to v6.16.0 2023-12-25 20:01:03 +00:00
renovate[bot]
cb79ed56e9
chore(deps): update dependency lucide-react to v0.302.0 2023-12-25 13:53:08 +00:00
renovate[bot]
fd1e7e261a
chore(deps): lock file maintenance 2023-12-25 02:03:31 +00:00
renovate[bot]
ee909563c3
chore(deps): update dependency lucide-react to v0.301.0 2023-12-24 13:38:44 +00:00
renovate[bot]
ac89b15133 chore(deps): update dependency @types/supertest to v6 2023-12-22 19:43:00 +00:00
renovate[bot]
9a26b85588
chore(deps): update dependency lucide-react to v0.300.0 2023-12-22 16:15:15 +00:00
renovate[bot]
8915f25c5c
chore(deps): update dependency @sentry/node to v7.91.0 2023-12-22 12:50:50 +00:00
renovate[bot]
8763ed33fc
chore(deps): update dependency react-router-dom to v6.21.1 2023-12-21 19:41:59 +00:00
renovate[bot]
5b3bdb339e
chore(deps): update dependency @sentry/node to v7.90.0 2023-12-20 16:15:04 +00:00
renovate[bot]
ad2f690cf6
chore(deps): update dependency lucide-react to v0.299.0 2023-12-20 10:18:54 +00:00
renovate[bot]
253360c01a
chore(deps): update dependency vitest to v1.1.0 2023-12-19 22:51:07 +00:00
renovate[bot]
97bc4375d6
chore(deps): update dependency tailwindcss to v3.4.0 2023-12-19 19:47:05 +00:00
renovate[bot]
9de059348d
chore(deps): update dependency @sentry/node to v7.89.0 2023-12-19 17:26:51 +00:00
renovate[bot]
bfa83ef354
chore(deps): update grafana/grafana-oss docker tag to v10.2.3 2023-12-19 12:03:08 +00:00
renovate[bot]
222a4bfd8e
chore(deps): update typescript-eslint monorepo to v6.15.0 2023-12-18 23:06:49 +00:00
renovate[bot]
aad6bae46c
chore(deps): update dependency tailwindcss to v3.3.7 2023-12-18 19:54:38 +00:00
renovate[bot]
13218e6402
chore(deps): update nest monorepo to v10.3.0 2023-12-18 09:37:00 +00:00
renovate[bot]
7a93cadde4
chore(deps): lock file maintenance 2023-12-18 04:20:32 +00:00
renovate[bot]
df66666921
chore(deps): update dependency @types/node to v20.10.5 2023-12-18 01:34:39 +00:00
renovate[bot]
16956c89b5
chore(deps): update dependency lucide-react to v0.298.0 2023-12-16 15:07:45 +00:00
renovate[bot]
843ad41cb6
chore(deps): update dependency eslint to v8.56.0 2023-12-16 03:42:57 +00:00
renovate[bot]
19ab536cf0
chore(deps): update dependency @types/react-dom to v18.2.18 2023-12-16 00:03:48 +00:00
renovate[bot]
27ef4cc0c8 chore(deps): update pino 2023-12-15 19:59:10 +00:00
renovate[bot]
7e94806f63
chore(deps): update dependency vite to v5.0.10 2023-12-15 17:01:32 +00:00
renovate[bot]
c40c27aa8e
chore(deps): update dependency lucide-react to v0.297.0 2023-12-15 13:55:47 +00:00
renovate[bot]
a8dad23613
chore(deps): update opentelemetry-js monorepo 2023-12-15 11:16:45 +00:00
renovate[bot]
c1bf104735
chore(deps): update dependency react-router-dom to v6.21.0 2023-12-15 07:53:16 +00:00
renovate[bot]
c4c9e9d777
chore(deps): update dependency @sentry/node to v7.88.0 2023-12-15 05:01:38 +00:00
renovate[bot]
b83526a3d3
chore(deps): update dependency vite to v5.0.9 2023-12-15 00:44:56 +00:00
renovate[bot]
bcfca028a9
chore(deps): update dependency eslint-plugin-import to v2.29.1 2023-12-14 23:10:44 +00:00
renovate[bot]
bb002389e2
chore(deps): update dependency @nestjs/swagger to v7.1.17 2023-12-14 20:10:51 +00:00
renovate[bot]
c35907637d
chore(deps): update dependency lucide-react to v0.295.0 2023-12-14 03:47:02 +00:00
renovate[bot]
d93cd6e833
chore(deps): update dependency eslint-plugin-jsdoc to v46.9.1 2023-12-13 23:36:24 +00:00
renovate[bot]
39fe04fd00
chore(deps): update dependency @sentry/node to v7.87.0 2023-12-13 13:49:50 +00:00
renovate[bot]
2ba25417d7
chore(deps): update dependency @types/react to v18.2.45 2023-12-13 03:11:06 +00:00
renovate[bot]
8b8685b288
chore(deps): update dependency @types/react to v18.2.44 2023-12-12 22:47:00 +00:00
renovate[bot]
01b6416ba0
chore(deps): update dependency vite to v5.0.8 2023-12-12 12:07:00 +00:00
renovate[bot]
b401cd7a2f
chore(deps): update grafana/promtail docker tag to v2.9.3 2023-12-12 00:02:35 +00:00
renovate[bot]
400f51dd2c
chore(deps): update grafana/loki docker tag to v2.9.3 2023-12-11 21:35:01 +00:00
renovate[bot]
cf97a54d5d
chore(deps): update typescript-eslint monorepo to v6.14.0 2023-12-11 18:53:03 +00:00
renovate[bot]
141775dfb7
chore(deps): update dependency @types/recharts to v1.8.29 2023-12-11 13:44:26 +00:00
renovate[bot]
0df10184a6
chore(deps): lock file maintenance 2023-12-11 01:09:45 +00:00
renovate[bot]
fbdbbb2c6b
chore(deps): update dependency prettier to v3.1.1 2023-12-10 09:51:36 +00:00
renovate[bot]
71b233e553
chore(deps): update dependency vitest to v1.0.4 2023-12-09 21:04:48 +00:00
renovate[bot]
396977497f
chore(deps): update dependency @types/react to v18.2.43 2023-12-09 15:09:55 +00:00
renovate[bot]
e138f79e62
chore(deps): update prom/prometheus docker tag to v2.48.1 2023-12-09 01:13:20 +00:00
renovate[bot]
35bc182f49
chore(deps): update dependency vite to v5.0.7 2023-12-08 15:12:23 +00:00
renovate[bot]
7000354df2
chore(deps): update dependency ts-node to v10.9.2 2023-12-08 13:59:00 +00:00
renovate[bot]
b51979f3f6
chore(deps): update dependency reflect-metadata to v0.1.14 2023-12-08 00:30:40 +00:00
renovate[bot]
a436f2a5e5
chore(deps): update dependency @opentelemetry/instrumentation-express to v0.34.0 2023-12-07 21:46:19 +00:00
renovate[bot]
b4b65022bc
chore(deps): update dependency @opentelemetry/instrumentation-pino to v0.34.4 2023-12-07 18:25:23 +00:00
renovate[bot]
cb729c6251
chore(deps): update dependency @sentry/node to v7.86.0 2023-12-07 16:01:31 +00:00
renovate[bot]
00fb1d0376
chore(deps): update dependency vitest to v1.0.2 2023-12-07 12:52:36 +00:00
renovate[bot]
bf6968006d
chore(deps): update dependency @types/node to v20.10.4 2023-12-07 09:26:05 +00:00
renovate[bot]
65a80421a7
chore(deps): update traefik docker tag to v2.10.7 2023-12-07 01:32:22 +00:00
renovate[bot]
f931c52a70
chore(deps): update dependency typescript to v5.3.3 2023-12-06 21:57:33 +00:00
renovate[bot]
f67cfe0fc0
chore(deps): update dependency vite to v5.0.6 2023-12-06 09:07:21 +00:00
renovate[bot]
ee55436a17
chore(deps): update dependency @types/jest to v29.5.11 2023-12-06 01:22:06 +00:00
renovate[bot]
fde1e52285
chore(deps): update dependency vitest to v1 2023-12-05 14:24:42 +00:00
renovate[bot]
b6f32c4afa
chore(deps): update dependency @sentry/node to v7.85.0 2023-12-05 09:59:00 +00:00
renovate[bot]
ad1e34db41
chore(deps): update typescript-eslint monorepo to v6.13.2 2023-12-05 06:23:25 +00:00
renovate[bot]
120b7148f7
chore(deps): update dependency vite to v5.0.5 2023-12-05 03:49:10 +00:00
renovate[bot]
d9c0ba2f3b
chore(deps): update dependency tailwindcss to v3.3.6 2023-12-05 00:06:12 +00:00
renovate[bot]
20bd61d18a
chore(deps): update dependency @types/react to v18.2.42 2023-12-04 21:36:46 +00:00
renovate[bot]
6b66331e07
chore(deps): update dependency @vitejs/plugin-react to v4.2.1 2023-12-04 18:34:26 +00:00
renovate[bot]
5596cddec0 chore(deps): update dependency passport to v0.7.0 2023-12-04 15:55:39 +00:00
renovate[bot]
122f386f5a
chore(deps): update dependency @nestjs/passport to v10.0.3 2023-12-04 12:14:17 +00:00
renovate[bot]
de8908aa95
chore(deps): lock file maintenance 2023-12-04 01:22:41 +00:00
renovate[bot]
dc7c1f54e9
chore(deps): update dependency @types/node to v20.10.3 2023-12-03 20:19:58 +00:00
renovate[bot]
b0eb189dd0
chore(deps): update dependency nest-raven to v10.0.1 2023-12-03 13:32:53 +00:00
renovate[bot]
878fbc8c22
chore(deps): update dependency @types/react to v18.2.41 2023-12-02 18:33:42 +00:00
renovate[bot]
b9f4fdeb3a
chore(deps): update dependency eslint-config-prettier to v9.1.0 2023-12-02 14:21:16 +00:00
renovate[bot]
07d8fe82b8
chore(deps): update dependency eslint to v8.55.0 2023-12-02 10:36:14 +00:00
renovate[bot]
bb407ebeec
chore(deps): update dependency react-router-dom to v6.20.1 2023-12-02 06:37:01 +00:00
renovate[bot]
814e121cbd
chore(deps): update dependency postcss to v8.4.32 2023-12-02 05:16:29 +00:00
renovate[bot]
43c15a46dd
chore(deps): update dependency @types/react to v18.2.40 2023-12-02 00:38:50 +00:00
renovate[bot]
71bb635d78
chore(deps): update dependency @types/node to v20.10.2 2023-12-01 22:03:05 +00:00
renovate[bot]
6df5236ed1
chore(deps): update dependency recharts to v2.10.3 2023-12-01 01:59:10 +00:00
renovate[bot]
f4aec012d7
chore(deps): update dependency @testing-library/jest-dom to v6.1.5 2023-11-30 20:12:59 +00:00
renovate[bot]
f0234699d0
chore(deps): update dependency @sentry/node to v7.84.0 2023-11-30 13:59:58 +00:00
renovate[bot]
10371f4b94
chore(deps): update dependency lucide-react to v0.294.0 2023-11-30 03:09:50 +00:00
renovate[bot]
6655fe3279
chore(deps): update typescript-eslint monorepo to v6.13.1 2023-11-30 00:08:40 +00:00
renovate[bot]
62fcb25e15
chore(deps): update dependency @types/node to v20.10.1 2023-11-29 22:04:51 +00:00
renovate[bot]
eef6497803
chore(deps): update traefik docker tag to v2.10.6 2023-11-29 19:31:05 +00:00
renovate[bot]
ad8792142b
chore(deps): update grafana/tempo docker tag to v2.3.1 2023-11-29 15:28:03 +00:00
renovate[bot]
7fb5377801
chore(deps): update dependency vite to v5.0.4 2023-11-29 13:37:54 +00:00
renovate[bot]
af0223c2e3
chore(deps): update dependency recharts to v2.10.2 2023-11-29 06:09:01 +00:00
renovate[bot]
a99bc05912
chore(deps): update dependency vite to v5.0.3 2023-11-28 21:14:53 +00:00
renovate[bot]
37b1d88c1a chore(deps): update dependency @sentry/node to v7.83.0 2023-11-28 12:56:29 +00:00
renovate[bot]
e90eb97f5f
chore(deps): update typescript-eslint monorepo to v6.13.0 2023-11-27 22:14:07 +00:00
renovate[bot]
ffdda278a5
chore(deps): update dependency @types/react to v18.2.39 2023-11-27 19:53:29 +00:00
renovate[bot]
331da2f12d
chore(deps): update dependency @nestjs/terminus to v10.2.0 2023-11-27 17:41:10 +00:00
renovate[bot]
661842ae37
chore(deps): lock file maintenance 2023-11-27 01:40:47 +00:00
renovate[bot]
4f6602e6f6
chore(deps): update dependency lucide-react to v0.293.0 2023-11-24 17:06:42 +00:00
renovate[bot]
7c5821c7ec
chore(deps): update dependency @types/node to v20.10.0 2023-11-24 09:20:19 +00:00
renovate[bot]
221e88154e
chore(deps): update dependency vite to v5 (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-24 08:27:22 +01:00
renovate[bot]
dcce338d61
chore(deps): update dependency @types/node to v20.9.5 2023-11-24 01:29:54 +00:00
renovate[bot]
e03d86a607
chore(deps): update dependency react-router-dom to v6.20.0 2023-11-22 18:10:08 +00:00
renovate[bot]
98f1c132dd
chore(deps): update react monorepo 2023-11-22 16:08:54 +00:00
renovate[bot]
dd2ac8deca
chore(deps): update dependency @opentelemetry/instrumentation-pg to v0.37.1 2023-11-22 14:28:51 +00:00
renovate[bot]
8663855e67
chore(deps): update dependency @types/node to v20.9.4 2023-11-22 06:07:50 +00:00
renovate[bot]
edb467c0fb
chore(deps): update dependency @types/jest to v29.5.10 2023-11-22 04:21:53 +00:00
renovate[bot]
69ba853b78
chore(deps): update grafana/grafana-oss docker tag to v10.2.2 2023-11-21 22:09:32 +00:00
renovate[bot]
004b1f8368
chore(deps): update dependency @types/recharts to v1.8.28 2023-11-21 19:12:32 +00:00
renovate[bot]
9d69150820
chore(deps): update dependency @sentry/node to v7.81.1 2023-11-21 15:22:34 +00:00
renovate[bot]
8ceee63c7b
chore(deps): update dependency @types/passport-http-bearer to v1.0.41 2023-11-21 09:28:17 +00:00
renovate[bot]
1c9282346e
chore(deps): update dependency @types/node to v20.9.3 2023-11-21 08:25:24 +00:00
renovate[bot]
836eae8ca8
chore(deps): update dependency @types/lodash to v4.14.202 2023-11-21 05:40:26 +00:00
renovate[bot]
b1e07f49b8
chore(deps): update dependency @types/jest to v29.5.9 2023-11-21 01:41:51 +00:00
renovate[bot]
1d12c00fee
chore(deps): update typescript-eslint monorepo to v6.12.0 2023-11-20 22:53:00 +00:00
renovate[bot]
e94dd3fc09
chore(deps): update dependency typescript to v5.3.2 2023-11-20 19:10:39 +00:00
renovate[bot]
1d1ad2350f
chore(deps): update dependency @sentry/node to v7.81.0 2023-11-20 14:18:09 +00:00
renovate[bot]
fe232da0dd
chore(deps): update nest monorepo to v10.2.10 2023-11-20 10:46:34 +00:00
renovate[bot]
cfc359681d
chore(deps): lock file maintenance 2023-11-20 03:57:16 +00:00
renovate[bot]
678c07a81c
chore(deps): lock file maintenance 2023-11-20 00:13:18 +00:00
renovate[bot]
42d4af1862
chore(deps): update dependency recharts to v2.10.1 2023-11-19 22:22:40 +00:00
renovate[bot]
d75a92f9d3
chore(deps): update dependency recharts to v2.10.0 2023-11-19 15:43:08 +00:00
renovate[bot]
e72d87e7b5
chore(deps): update dependency @types/node to v20.9.2 2023-11-18 21:11:30 +00:00
renovate[bot]
8e74951013
chore(deps): update dependency eslint to v8.54.0 2023-11-17 22:05:05 +00:00
renovate[bot]
687155b325
chore(deps): update dependency @testing-library/react to v14.1.2 2023-11-17 16:06:25 +00:00
renovate[bot]
2cf6a6d629
chore(deps): update nest monorepo to v10.2.9 2023-11-17 12:50:49 +00:00
renovate[bot]
703b14750f
chore(deps): update dependency @types/node to v20.9.1 2023-11-16 23:10:47 +00:00
renovate[bot]
76edebfcf2
chore(deps): update dependency react-router-dom to v6.19.0 2023-11-16 18:19:30 +00:00
renovate[bot]
e5dfaa7b86
chore(deps): update dependency @vitejs/plugin-react to v4.2.0 2023-11-16 16:06:17 +00:00
renovate[bot]
522a33af8e
chore(deps): update docker/dockerfile docker tag to v1.6 2023-11-16 12:23:11 +00:00
renovate[bot]
37962db96a
chore(deps): update prom/prometheus docker tag to v2.48.0 2023-11-16 07:35:10 +00:00
renovate[bot]
25b738a246
chore(deps): update dependency pino to v8.16.2 2023-11-15 18:58:01 +00:00
renovate[bot]
dd060a4c47
chore(deps): update dependency @nestjs/typeorm to v10.0.1 2023-11-15 12:18:46 +00:00
renovate[bot]
f78c00211e
chore(deps): update dependency @nestjs/swagger to v7.1.16 2023-11-15 10:07:59 +00:00
renovate[bot]
f2d112e3be
chore(deps): update dependency ts-loader to v9.5.1 2023-11-15 04:26:07 +00:00
renovate[bot]
0d0071805d
chore(deps): update dependency axios to v1.6.2 2023-11-14 22:03:43 +00:00
renovate[bot]
a8a9375c19
chore(deps): update typescript-eslint monorepo to v6.11.0 2023-11-14 19:15:25 +00:00
renovate[bot]
82bd1fdfac
chore(deps): update grafana/grafana-oss docker tag to v10.2.1 2023-11-14 16:41:36 +00:00
renovate[bot]
955696b39a
chore(deps): update postgres docker tag to v16.1 2023-11-14 14:38:32 +00:00
renovate[bot]
40074c2088
chore(deps): update dependency @sentry/node to v7.80.1 2023-11-14 11:42:49 +00:00
renovate[bot]
3b769086ff
chore(deps): update dependency @opentelemetry/instrumentation-pg to v0.37.0 2023-11-14 06:52:09 +00:00
renovate[bot]
be2c54cded
chore(deps): update dependency @opentelemetry/instrumentation-pino to v0.34.3 2023-11-14 04:08:23 +00:00
renovate[bot]
c77b84f9d9
chore(deps): update dependency @opentelemetry/instrumentation-nestjs-core to v0.33.3 2023-11-14 01:55:17 +00:00
renovate[bot]
83235d944c
chore(deps): update dependency @opentelemetry/instrumentation-express to v0.33.3 2023-11-13 22:14:34 +00:00
renovate[bot]
c2371537b8
chore(deps): update dependency @opentelemetry/instrumentation-dns to v0.32.4 2023-11-13 20:24:08 +00:00
renovate[bot]
4c3a2c44c7
chore(deps): update dependency prettier to v3.1.0 2023-11-13 04:00:28 +00:00
renovate[bot]
7c7b284ad0
chore(deps): lock file maintenance 2023-11-13 00:57:37 +00:00
renovate[bot]
497323f7ad
chore(deps): update dependency eslint-plugin-jsdoc to v46.9.0 2023-11-10 03:34:36 +00:00
renovate[bot]
849410dd96
chore(deps): update dependency @sentry/node to v7.80.0 2023-11-09 19:02:45 +00:00
renovate[bot]
756ac80836
chore(deps): update dependency @nestjs/jwt to v10.2.0 2023-11-09 10:52:31 +00:00
renovate[bot]
c7d7a14d0d
chore(deps): update dependency @types/node to v20.9.0 2023-11-09 04:35:22 +00:00
renovate[bot]
8f444dc653
chore(deps): update dependency @testing-library/react to v14.1.0 2023-11-09 01:45:31 +00:00
renovate[bot]
b943469f3a
chore(deps): update dependency @sentry/node to v7.79.0 2023-11-08 21:41:19 +00:00
renovate[bot]
879cedeafb
chore(deps): update opentelemetry-js monorepo 2023-11-08 19:44:02 +00:00
renovate[bot]
dae0b4f896
chore(deps): update dependency axios to v1.6.1 2023-11-08 15:54:53 +00:00
renovate[bot]
59e5255128
chore(deps): update dependency @nestjs/swagger to v7.1.15 2023-11-08 13:39:59 +00:00
renovate[bot]
a66d02b9e6
chore(deps): update react monorepo 2023-11-08 09:43:35 +00:00
renovate[bot]
4c957afefc
chore(deps): update dependency @types/supertest to v2.0.16 2023-11-08 08:06:37 +00:00
renovate[bot]
41fdac9148
chore(deps): update dependency @types/recharts to v1.8.27 2023-11-08 04:52:48 +00:00
renovate[bot]
a5b6925870
chore(deps): update dependency @types/lodash to v4.14.201 2023-11-08 01:23:36 +00:00
renovate[bot]
2b67cda038
chore(deps): update dependency @types/jest to v29.5.8 2023-11-07 22:05:00 +00:00
renovate[bot]
38234d9979
chore(deps): update dependency @types/passport-jwt to v3.0.13 2023-11-07 18:22:01 +00:00
renovate[bot]
d7a1c2c743
chore(deps): update dependency @types/passport-http-bearer to v1.0.40 2023-11-07 16:39:01 +00:00
renovate[bot]
72c424b4c6
chore(deps): update opentelemetry-js monorepo 2023-11-07 12:08:25 +00:00
renovate[bot]
e576d739a5
chore(deps): update dependency @types/express to v4.17.21 2023-11-07 04:50:43 +00:00
renovate[bot]
ae8a895ee2
chore(deps): update dependency @types/cookie-parser to v1.4.6 2023-11-07 01:44:03 +00:00
renovate[bot]
ec12c71ba7
chore(deps): update dependency recharts to v2.9.3 2023-11-06 22:34:43 +00:00
renovate[bot]
3de1e5df79
chore(deps): update typescript-eslint monorepo to v6.10.0 2023-11-06 18:01:04 +00:00
renovate[bot]
7b1dfb63ab
chore(deps): update dependency @types/react to v18.2.36 2023-11-06 13:57:33 +00:00
renovate[bot]
c7149fba41
chore(deps): lock file maintenance 2023-11-06 01:45:47 +00:00
renovate[bot]
160c8ba2ee
chore(deps): update dependency @types/react to v18.2.35 2023-11-05 13:01:46 +00:00
renovate[bot]
f414c3cc9a
chore(deps): update dependency eslint to v8.53.0 2023-11-04 01:10:37 +00:00
renovate[bot]
ccfdbd1805
chore(deps): update dependency lucide-react to v0.292.0 2023-11-03 10:44:12 +00:00
renovate[bot]
1aa0ffb07c
chore(deps): update nest monorepo to v10.2.8 2023-11-02 14:01:35 +00:00
renovate[bot]
d641731868
chore(deps): update dependency @vitejs/plugin-react to v4.1.1 2023-11-02 09:12:54 +00:00
renovate[bot]
a830ad66b7
chore(deps): update dependency eslint-plugin-jsx-a11y to v6.8.0 2023-11-02 03:20:51 +00:00
renovate[bot]
d0536ad9ad
chore(deps): update dependency @types/react to v18.2.34 2023-11-02 00:27:36 +00:00
renovate[bot]
eec7e89a3f
chore(deps): update dependency recharts to v2.9.2 2023-11-01 06:55:35 +00:00
renovate[bot]
bb6051df9b
chore(deps): update dependency react-router-dom to v6.18.0 2023-10-31 16:51:15 +00:00
renovate[bot]
4acf3df74f
chore(deps): update dependency @sentry/node to v7.77.0 2023-10-31 13:36:36 +00:00
renovate[bot]
c2f598d99a
chore(deps): update dependency @types/node to v20.8.10 2023-10-31 11:57:40 +00:00
renovate[bot]
073f98498b
chore(deps): update dependency lucide-react to v0.291.0 2023-10-31 07:52:35 +00:00
renovate[bot]
369c455ccd
chore(deps): update grafana/tempo docker tag to v2.3.0 2023-10-31 01:52:50 +00:00
renovate[bot]
04fe3f99b1
chore(deps): update dependency @types/jest to v29.5.7 2023-10-30 23:15:25 +00:00
renovate[bot]
1ca0b32ace
chore(deps): update typescript-eslint monorepo to v6.9.1 2023-10-30 18:50:28 +00:00
renovate[bot]
9f34f781da
chore(deps): update dependency recharts to v2.9.1 2023-10-30 15:21:39 +00:00
renovate[bot]
23c42138b4
chore(deps): update dependency @nestjs/schematics to v10.0.3 2023-10-30 12:21:57 +00:00
renovate[bot]
9f8c9cee25
chore(deps): update dependency @nestjs/axios to v3.0.1 2023-10-30 10:30:49 +00:00
renovate[bot]
16b0ef562d
chore(deps): update dependency @nestjs/cli to v10.2.1 2023-10-30 08:05:39 +00:00
renovate[bot]
0b4a4a897c
chore(deps): lock file maintenance 2023-10-30 00:59:12 +00:00
renovate[bot]
ee6c885fb6
chore(deps): update dependency pino-http to v8.5.1 2023-10-28 19:38:10 +00:00
renovate[bot]
ece6cf40fc
chore(deps): update dependency @sentry/node to v7.76.0 2023-10-27 18:50:36 +00:00
renovate[bot]
cd06b8d8b8
chore(deps): update dependency lucide-react to v0.290.0 2023-10-27 09:52:25 +00:00
renovate[bot]
303045fe40
chore(deps): update dependency axios to v1.6.0 2023-10-27 04:36:48 +00:00
renovate[bot]
ddb677df10
chore(deps): update dependency @types/passport-jwt to v3.0.12 2023-10-27 02:00:09 +00:00
renovate[bot]
67a394b1e8
chore(deps): update dependency @types/react to v18.2.33 2023-10-26 06:34:13 +00:00
renovate[bot]
f91b1ccbb7
chore(deps): update dependency lucide-react to v0.289.0 2023-10-26 04:06:42 +00:00
renovate[bot]
b75ae80884
chore(deps): update grafana/tempo docker tag to v2.2.4 2023-10-26 01:08:32 +00:00
renovate[bot]
81beb1ff50
chore(deps): update dependency tailwindcss to v3.3.5 2023-10-25 22:49:51 +00:00
renovate[bot]
485fc53db4
chore(deps): update dependency @types/node to v20.8.9 2023-10-25 19:23:37 +00:00
renovate[bot]
5457984905
chore(deps): update dependency @types/react to v18.2.32 2023-10-25 17:37:00 +00:00
renovate[bot]
7e3a59e7e7
chore(deps): update dependency @sentry/node to v7.75.1 2023-10-25 14:03:01 +00:00
renovate[bot]
b37172cbcd
chore(deps): update dependency tailwindcss to v3.3.4 2023-10-24 20:28:19 +00:00
renovate[bot]
76da8c9892
chore(deps): update grafana/grafana-oss docker tag to v10.2.0 2023-10-24 17:52:34 +00:00
renovate[bot]
82465251e1
chore(deps): update dependency @sentry/node to v7.75.0 2023-10-24 12:49:36 +00:00
renovate[bot]
9f93cebb8d
chore(deps): update dependency @types/node to v20.8.8 2023-10-24 03:57:54 +00:00
renovate[bot]
2e58ff129d
chore(deps): update typescript-eslint monorepo to v6.9.0 2023-10-24 01:21:49 +00:00
renovate[bot]
cbfb9e2ec1
chore(deps): update dependency @nestjs/cli to v10.2.0 2023-10-23 21:57:31 +00:00
renovate[bot]
f1e970f12c
chore(deps): update dependency pino to v8.16.1 2023-10-23 20:03:16 +00:00
renovate[bot]
5bbef6b518
chore(deps): update dependency @nestjs/swagger to v7.1.14 2023-10-23 16:58:44 +00:00
renovate[bot]
325b8c12d4
chore(deps): update dependency eslint-plugin-import to v2.29.0 2023-10-23 07:25:44 +00:00
renovate[bot]
f484d4d2a8
chore(deps): lock file maintenance 2023-10-23 00:14:58 +00:00
renovate[bot]
4b9ce7e0cd
chore(deps): update dependency eslint to v8.52.0 2023-10-20 22:08:55 +00:00
renovate[bot]
12e220d7fc
chore(deps): update dependency @types/react to v18.2.31 2023-10-20 15:36:37 +00:00
renovate[bot]
641506d0cb
chore(deps): update dependency @types/react to v18.2.30 2023-10-19 18:54:18 +00:00
renovate[bot]
84b6847f08
chore(deps): update dependency vite to v4.5.0 2023-10-19 09:23:37 +00:00
renovate[bot]
e0a0d49efb
chore(deps): update react monorepo 2023-10-19 07:03:10 +00:00
renovate[bot]
8742ad58b3
chore(deps): update dependency @types/supertest to v2.0.15 2023-10-19 03:51:54 +00:00
renovate[bot]
47beb1dd04
chore(deps): update dependency @types/recharts to v1.8.26 2023-10-19 00:35:12 +00:00
renovate[bot]
465190be77
chore(deps): update dependency @types/passport-jwt to v3.0.11 2023-10-18 22:17:35 +00:00
renovate[bot]
2fe32137ea
chore(deps): update dependency @types/passport-http-bearer to v1.0.39 2023-10-18 19:48:54 +00:00
renovate[bot]
1f65d051ba
chore(deps): update dependency @types/node to v20.8.7 2023-10-18 15:51:00 +00:00
renovate[bot]
8349a9c936
chore(deps): update dependency @types/lodash to v4.14.200 2023-10-18 11:25:53 +00:00
renovate[bot]
7827d3dca5
chore(deps): update dependency @types/jest to v29.5.6 2023-10-18 08:19:30 +00:00
renovate[bot]
1cacff9143
chore(deps): update dependency @types/express to v4.17.20 2023-10-18 03:27:47 +00:00
renovate[bot]
ef76efe5ca
chore(deps): update dependency @types/cookie-parser to v1.4.5 2023-10-18 01:52:45 +00:00
renovate[bot]
9ffa802fbd
chore(deps): update dependency lucide-react to v0.288.0 2023-10-17 20:05:48 +00:00
renovate[bot]
43a872fe25
chore(deps): update dependency @sentry/node to v7.74.1 2023-10-17 14:59:33 +00:00
renovate[bot]
a5fd7ad945
chore(deps): update typescript-eslint monorepo to v6.8.0 2023-10-17 00:44:21 +00:00
renovate[bot]
d4f220a722
chore(deps): update dependency react-router-dom to v6.17.0 2023-10-16 22:47:09 +00:00
renovate[bot]
871f9e57c1
chore(deps): update grafana/promtail docker tag to v2.9.2 2023-10-16 19:20:44 +00:00
renovate[bot]
0832193bda
chore(deps): update grafana/loki docker tag to v2.9.2 2023-10-16 17:04:34 +00:00
renovate[bot]
e9bf058b63
chore(deps): lock file maintenance 2023-10-16 01:49:53 +00:00
renovate[bot]
d3ac13fe41
chore(deps): update dependency recharts to v2.9.0 2023-10-15 13:29:02 +00:00
semantic-release-bot
44b3b76886 chore(release): 1.31.0 [skip ci]
# [1.31.0](https://github.com/apricote/Listory/compare/v1.30.1...v1.31.0) (2023-10-15)

### Features

* **frontend:** hide revoked api tokens ([#307](https://github.com/apricote/Listory/issues/307)) ([4cef4f7](4cef4f75ac))
2023-10-15 13:25:51 +00:00
4cef4f75ac
feat(frontend): hide revoked api tokens (#307) 2023-10-15 13:19:26 +00:00
renovate[bot]
9a729ed588
chore(deps): update dependency @types/node to v20.8.6 2023-10-14 00:49:49 +00:00
renovate[bot]
112a9c10cd
chore(deps): update dependency lucide-react to v0.287.0 2023-10-13 15:39:07 +00:00
renovate[bot]
0a087c2861
chore(deps): update dependency @sentry/node to v7.74.0 2023-10-13 13:45:20 +00:00
renovate[bot]
43c5748bc8
chore(deps): update prom/prometheus docker tag to v2.47.2 2023-10-13 01:44:03 +00:00
renovate[bot]
985785f15f
chore(deps): update dependency @types/node to v20.8.5 2023-10-12 21:18:52 +00:00
renovate[bot]
99464064f1
chore(deps): update grafana/grafana-oss docker tag to v10.1.5 2023-10-12 16:25:19 +00:00
renovate[bot]
5481a9b3b2
chore(deps): update dependency lucide-react to v0.286.0 2023-10-12 11:00:10 +00:00
renovate[bot]
ba6695719a
chore(deps): update dependency @testing-library/jest-dom to v6.1.4 2023-10-12 07:08:56 +00:00
renovate[bot]
dfb4cf5975
chore(deps): update traefik docker tag to v2.10.5 2023-10-12 00:38:30 +00:00
renovate[bot]
0de28e6e58
chore(deps): update dependency @opentelemetry/instrumentation-pino to v0.34.2 2023-10-11 22:07:21 +00:00
renovate[bot]
65d6c8d381
chore(deps): update dependency @opentelemetry/instrumentation-pg to v0.36.2 2023-10-11 19:50:10 +00:00
renovate[bot]
8775f7629e
chore(deps): update dependency @opentelemetry/instrumentation-nestjs-core to v0.33.2 2023-10-11 16:33:30 +00:00
renovate[bot]
3c031979ee
chore(deps): update dependency @opentelemetry/instrumentation-express to v0.33.2 2023-10-11 13:22:24 +00:00
renovate[bot]
a3b44f4c55
chore(deps): update dependency @opentelemetry/instrumentation-dns to v0.32.3 2023-10-11 09:35:24 +00:00
renovate[bot]
839663b547
chore(deps): update react monorepo 2023-10-10 23:07:00 +00:00
renovate[bot]
f725dbc716
chore(deps): update dependency @types/express to v4.17.19 2023-10-10 18:56:45 +00:00
renovate[bot]
2284592197
chore(deps): update opentelemetry-js monorepo 2023-10-10 16:16:37 +00:00
renovate[bot]
b014a7f1a5
chore(deps): update dependency pino to v8.16.0 2023-10-10 04:50:05 +00:00
renovate[bot]
2f037d8c54
chore(deps): update typescript-eslint monorepo to v6.7.5 2023-10-10 00:39:12 +00:00
renovate[bot]
d357f0a86a
chore(deps): update react monorepo 2023-10-09 22:52:47 +00:00
renovate[bot]
57bc019d21
chore(deps): update dependency @types/node to v20.8.4 2023-10-09 18:51:16 +00:00
renovate[bot]
d369a115a1
chore(deps): update dependency pino to v8.15.7 2023-10-09 09:42:42 +00:00
semantic-release-bot
5260119165 chore(release): 1.30.1 [skip ci]
## [1.30.1](https://github.com/apricote/Listory/compare/v1.30.0...v1.30.1) (2023-10-08)

### Bug Fixes

* no listens are being crawled ([#306](https://github.com/apricote/Listory/issues/306)) ([9af3115](9af3115cab))
2023-10-08 14:02:57 +00:00
9af3115cab
fix: no listens are being crawled (#306)
The supervisor job handler was accidentally disabled in a previous
commit.
2023-10-08 15:56:09 +02:00
renovate[bot]
db240bfa45
chore(deps): update dependency ts-loader to v9.5.0 2023-10-07 12:13:19 +00:00
renovate[bot]
3f904fb310
chore(deps): update dependency @types/react-dom to v18.2.11 2023-10-07 03:05:29 +00:00
renovate[bot]
c1b4e11cce
chore(deps): update dependency @types/node to v20.8.3 2023-10-07 00:38:36 +00:00
renovate[bot]
d50fae9e24
chore(deps): update dependency eslint to v8.51.0 2023-10-06 21:11:30 +00:00
renovate[bot]
d923c393a2
chore(deps): update dependency vite to v4.4.11 2023-10-05 13:54:10 +00:00
renovate[bot]
afbf3ff5ca
chore(deps): update dependency pino to v8.15.6 2023-10-05 09:40:33 +00:00
renovate[bot]
0b305163ab
chore(deps): update nest monorepo to v10.2.7 2023-10-05 07:07:05 +00:00
renovate[bot]
41b77f782b
chore(deps): update react monorepo 2023-10-05 02:04:08 +00:00
renovate[bot]
3c04787877
chore(deps): update pino 2023-10-04 22:56:47 +00:00
renovate[bot]
c98aedb6de
chore(deps): update dependency joi to v17.11.0 2023-10-04 18:24:49 +00:00
renovate[bot]
0f58aead43
chore(deps): update dependency @types/react-dom to v18.2.9 2023-10-04 16:19:26 +00:00
renovate[bot]
dea3ecad61
chore(deps): update prom/prometheus docker tag to v2.47.1 2023-10-04 13:05:20 +00:00
renovate[bot]
863503b150
chore(deps): update dependency @nestjs/swagger to v7.1.13 2023-10-04 08:10:00 +00:00
renovate[bot]
39b2458380
chore(deps): update dependency vite to v4.4.10 2023-10-03 22:00:58 +00:00
renovate[bot]
b511b4b784
chore(deps): update dependency pino-pretty to v10.2.2 2023-10-03 20:05:27 +00:00
renovate[bot]
d5ed16b5cc
chore(deps): update dependency pino to v8.15.4 2023-10-03 14:06:03 +00:00
renovate[bot]
1e5a55d5bd
chore(deps): update dependency lucide-react to v0.284.0 2023-10-03 09:44:27 +00:00
renovate[bot]
b4cb2579a8
chore(deps): update dependency lucide-react to v0.282.0 2023-10-03 04:13:31 +00:00
renovate[bot]
f580fb0146
chore(deps): update dependency @types/supertest to v2.0.14 2023-10-03 00:36:30 +00:00
renovate[bot]
0d971e6b14
chore(deps): update dependency @types/node to v20.8.2 2023-10-02 23:19:19 +00:00
renovate[bot]
c2b2403077
chore(deps): update typescript-eslint monorepo to v6.7.4 2023-10-02 18:38:41 +00:00
renovate[bot]
67bd26acd3
chore(deps): update dependency @sentry/node to v7.73.0 2023-10-02 14:18:07 +00:00
renovate[bot]
69b2b5b657
chore(deps): update dependency pino to v8.15.3 2023-10-01 15:37:05 +00:00
renovate[bot]
acec89ad30
chore(deps): update dependency @types/react to v18.2.24 2023-10-01 13:18:43 +00:00
renovate[bot]
1b719c9de9
chore(deps): update dependency pino to v8.15.2 2023-10-01 07:21:05 +00:00
semantic-release-bot
9eb36a6a78 chore(release): 1.30.0 [skip ci]
# [1.30.0](https://github.com/apricote/Listory/compare/v1.29.0...v1.30.0) (2023-10-01)

### Features

* import listens from spotify extended streaming history ([#305](https://github.com/apricote/Listory/issues/305)) ([7140cb0](7140cb0679))
2023-10-01 01:41:53 +00:00
7140cb0679
feat: import listens from spotify extended streaming history (#305) 2023-10-01 03:35:02 +02:00
renovate[bot]
23d7ea0995
chore(deps): update dependency @radix-ui/react-navigation-menu to v1.1.4 2023-09-30 22:36:13 +00:00
renovate[bot]
deab2526ee
chore(deps): update dependency @radix-ui/react-dropdown-menu to v2.0.6 2023-09-30 21:11:01 +00:00
ecb00e898d ci(renovate): enable lock file maintenance 2023-09-30 23:10:25 +02:00
renovate[bot]
c3eae1ef34
chore(deps): update dependency @radix-ui/react-select to v2 (#304)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-30 23:07:34 +02:00
renovate[bot]
6c90b0b398
chore(deps): update dependency @radix-ui/react-avatar to v1.0.4 2023-09-30 17:52:38 +00:00
semantic-release-bot
8f2ed636ee chore(release): 1.29.0 [skip ci]
# [1.29.0](https://github.com/apricote/Listory/compare/v1.28.2...v1.29.0) (2023-09-30)

### Features

* **frontend:** general revamp of navigation & pages ([#303](https://github.com/apricote/Listory/issues/303)) ([4b1dd10](4b1dd10846))
2023-09-30 17:52:05 +00:00
4b1dd10846
feat(frontend): general revamp of navigation & pages (#303) 2023-09-30 19:44:21 +02:00
renovate[bot]
f08633587d
chore(deps): update dependency @types/node to v20.8.0 2023-09-30 09:23:59 +00:00
renovate[bot]
fd486f49df
chore(deps): update grafana/grafana-oss docker tag to v10.1.4 2023-09-29 22:00:40 +00:00
renovate[bot]
3ee8ea9bdc
chore(deps): update dependency vitest to v0.34.6 2023-09-29 19:54:27 +00:00
renovate[bot]
3618b14068
chore(deps): update dependency @types/node to v20.7.2 2023-09-29 16:02:39 +00:00
renovate[bot]
314626ce8e
chore(deps): update dependency postcss to v8.4.31 2023-09-29 01:46:07 +00:00
renovate[bot]
729984b553
chore(deps): update dependency @types/node to v20.7.1 2023-09-27 21:41:00 +00:00
renovate[bot]
43d88870a5
chore(deps): update dependency rimraf to v5.0.5 2023-09-27 16:54:47 +00:00
renovate[bot]
72c3ca8fd9
chore(deps): update dependency @types/recharts to v1.8.25 2023-09-27 12:23:01 +00:00
dependabot[bot]
1ca4cd463b
chore(deps): bump systeminformation from 5.21.5 to 5.21.9 (#301)
Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.21.5 to 5.21.9.
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.21.5...v5.21.9)

---
updated-dependencies:
- dependency-name: systeminformation
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-27 14:22:31 +02:00
renovate[bot]
332e1ff886
chore(deps): update dependency @types/react-dom to v18.2.8 2023-09-27 08:35:06 +00:00
renovate[bot]
abdc3aa7a1
chore(deps): update dependency nestjs-pino to v3.5.0 2023-09-27 04:11:38 +00:00
renovate[bot]
5982dd9b07
chore(deps): update dependency eslint to v8.50.0 2023-09-27 00:52:46 +00:00
renovate[bot]
7ad2fd3c2a
chore(deps): update dependency @sentry/node to v7.72.0 2023-09-26 21:51:30 +00:00
renovate[bot]
871c65d098
chore(deps): update dependency axios to v1.5.1 2023-09-26 18:49:08 +00:00
renovate[bot]
3862fd4b58
chore(deps): update dependency @types/react to v18.2.23 2023-09-26 16:28:12 +00:00
renovate[bot]
88cf69a2cc
chore(deps): update dependency @vitejs/plugin-react to v4.1.0 2023-09-26 13:31:40 +00:00
renovate[bot]
de7d1d3c00
chore(deps): update dependency @types/node to v20.7.0 2023-09-26 09:52:42 +00:00
renovate[bot]
59cefc7421
chore(deps): update dependency @sentry/node to v7.71.0 2023-09-26 08:28:26 +00:00
renovate[bot]
4bf538aa5c
chore(deps): update typescript-eslint monorepo to v6.7.3 2023-09-26 03:58:08 +00:00
renovate[bot]
2a4bcaf841
chore(deps): update dependency rimraf to v5.0.4 2023-09-26 02:07:09 +00:00
renovate[bot]
36027e4b15
chore(deps): update nest monorepo to v10.2.6 2023-09-25 21:31:57 +00:00
renovate[bot]
3087c444eb
chore(deps): update dependency vitest to v0.34.5 2023-09-25 18:03:24 +00:00
renovate[bot]
9d4ad46b19
chore(deps): update dependency @types/supertest to v2.0.13 2023-09-25 17:05:21 +00:00
renovate[bot]
ad1c207f20
chore(deps): update dependency eslint-plugin-jsdoc to v46.8.2 2023-09-25 12:27:01 +00:00
renovate[bot]
b474c5b3e8
chore(deps): update dependency autoprefixer to v10.4.16 2023-09-25 09:18:03 +00:00
renovate[bot]
8da8e89d3a
chore(deps): update dependency @types/passport-jwt to v3.0.10 2023-09-25 06:49:24 +00:00
renovate[bot]
a206696365
chore(deps): update dependency @types/passport-http-bearer to v1.0.38 2023-09-25 03:55:49 +00:00
renovate[bot]
4317b89e26
chore(deps): update dependency @types/node to v20.6.5 2023-09-25 00:29:56 +00:00
renovate[bot]
b5de05dad7
chore(deps): update dependency @types/lodash to v4.14.199 2023-09-24 21:21:08 +00:00
renovate[bot]
baae603b74
chore(deps): update dependency @types/express to v4.17.18 2023-09-24 19:00:32 +00:00
renovate[bot]
441779d6b9
chore(deps): update dependency @nestjs/swagger to v7.1.12 2023-09-24 16:38:23 +00:00
renovate[bot]
e4f0d872c0
chore(deps): update dependency @nestjs/cli to v10.1.18 2023-09-24 12:20:24 +00:00
dependabot[bot]
ec22990ff0
chore(deps): bump graphql from 16.8.0 to 16.8.1 (#300)
Bumps [graphql](https://github.com/graphql/graphql-js) from 16.8.0 to 16.8.1.
- [Release notes](https://github.com/graphql/graphql-js/releases)
- [Commits](https://github.com/graphql/graphql-js/compare/v16.8.0...v16.8.1)

---
updated-dependencies:
- dependency-name: graphql
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-24 14:19:53 +02:00
renovate[bot]
cfae9c9da7
chore(deps): update typescript-eslint monorepo to v6.7.2 2023-09-19 05:15:17 +00:00
renovate[bot]
47b1b63904
chore(deps): update grafana/grafana-oss docker tag to v10.1.2 2023-09-19 00:26:29 +00:00
renovate[bot]
78ca3a3d64
chore(deps): update dependency postcss to v8.4.30 2023-09-18 22:37:30 +00:00
renovate[bot]
ff66b07040
chore(deps): update dependency @types/react to v18.2.22 2023-09-18 19:23:12 +00:00
semantic-release-bot
35b48740e0 chore(release): 1.28.2 [skip ci]
## [1.28.2](https://github.com/apricote/Listory/compare/v1.28.1...v1.28.2) (2023-09-18)

### Bug Fixes

* slow query taking up 66% of db time ([#298](https://github.com/apricote/Listory/issues/298)) ([625db7d](625db7dbe7))
2023-09-18 19:22:43 +00:00
625db7dbe7
fix: slow query taking up 66% of db time (#298)
Query was scanning the full index on listen table,
when we really only need data from the last hour
2023-09-18 21:15:37 +02:00
renovate[bot]
7f5328e538
chore(deps): update dependency joi to v17.10.2 2023-09-17 16:19:40 +00:00
semantic-release-bot
b63d3f63c4 chore(release): 1.28.1 [skip ci]
## [1.28.1](https://github.com/apricote/Listory/compare/v1.28.0...v1.28.1) (2023-09-16)

### Bug Fixes

* invalid database migration ([9ba47a5](9ba47a560c))
2023-09-16 22:50:03 +00:00
9ba47a560c fix: invalid database migration 2023-09-17 00:43:41 +02:00
semantic-release-bot
faa07c1297 chore(release): 1.28.0 [skip ci]
# [1.28.0](https://github.com/apricote/Listory/compare/v1.27.0...v1.28.0) (2023-09-16)

### Features

* optimize db queries ([#297](https://github.com/apricote/Listory/issues/297)) ([dd57a52](dd57a52ab6))
2023-09-16 22:38:10 +00:00
dd57a52ab6
feat: optimize db queries (#297) 2023-09-17 00:31:39 +02:00
semantic-release-bot
fd28daa37a chore(release): 1.27.0 [skip ci]
# [1.27.0](https://github.com/apricote/Listory/compare/v1.26.2...v1.27.0) (2023-09-16)

### Features

* improve listens report response time ([89440da](89440daf7b))
2023-09-16 21:27:59 +00:00
89440daf7b feat: improve listens report response time
Reduce response time for larger intervals (e.g. all time) from 9 seconds
to 150ms in my tests.
2023-09-16 23:20:15 +02:00
renovate[bot]
26087e4e6d
chore(deps): update postgres docker tag to v16 (#296)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-16 22:17:07 +02:00
renovate[bot]
ab91d1640c
chore(deps): update grafana/grafana-oss docker tag to v10 (#278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-16 22:11:23 +02:00
renovate[bot]
d61e047adf
chore(deps): update grafana/promtail docker tag to v2.9.1 2023-09-16 14:09:13 +00:00
renovate[bot]
a0ac434e94
chore(deps): update grafana/loki docker tag to v2.9.1 2023-09-16 11:08:01 +00:00
renovate[bot]
c42c7fae06
chore(deps): update actions/checkout action to v4 (#295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-16 13:07:44 +02:00
38cf2ff549
chore(deps): bump all (#294) 2023-09-16 13:02:19 +02:00
renovate[bot]
1979d924c9
chore(deps): update dependency @types/lodash to v4.14.198 2023-09-16 10:57:12 +00:00
renovate[bot]
8704e73365
chore(deps): update dependency @types/cookie-parser to v1.4.4 2023-09-16 10:42:45 +00:00
renovate[bot]
f76954b021
chore(deps): update dependency @nestjs/jwt to v10.1.1 2023-09-16 10:15:31 +00:00
renovate[bot]
45f8d762c2
chore(deps): update opentelemetry-js monorepo (#252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Julian Tölle <julian.toelle97@gmail.com>
2023-09-16 12:14:45 +02:00
125 changed files with 12827 additions and 22914 deletions

View file

@ -19,5 +19,6 @@
!frontend/tsconfig.json
!frontend/vite.config.js
!frontend/index.html
!frontend/*.d.ts
!frontend/src/**/*
!frontend/public/**/*

View file

@ -9,18 +9,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: npm Cache
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
@ -38,18 +30,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: npm Cache
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20

View file

@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install chart-releaser
env:

View file

@ -15,27 +15,27 @@ jobs:
needs: tests
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
with:
version: "v0.10.4"
version: "v0.11.2"
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Helm
uses: azure/setup-helm@v3
with:
version: "v3.9.0"
version: "v3.11.1"
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View file

@ -1,3 +1,59 @@
# [1.31.0](https://github.com/apricote/Listory/compare/v1.30.1...v1.31.0) (2023-10-15)
### Features
* **frontend:** hide revoked api tokens ([#307](https://github.com/apricote/Listory/issues/307)) ([4cef4f7](https://github.com/apricote/Listory/commit/4cef4f75ace6a38ba19c1d2f93d81389ec2b7cb8))
## [1.30.1](https://github.com/apricote/Listory/compare/v1.30.0...v1.30.1) (2023-10-08)
### Bug Fixes
* no listens are being crawled ([#306](https://github.com/apricote/Listory/issues/306)) ([9af3115](https://github.com/apricote/Listory/commit/9af3115cab19cc4ac4a6cd0fb680371154069aa2))
# [1.30.0](https://github.com/apricote/Listory/compare/v1.29.0...v1.30.0) (2023-10-01)
### Features
* import listens from spotify extended streaming history ([#305](https://github.com/apricote/Listory/issues/305)) ([7140cb0](https://github.com/apricote/Listory/commit/7140cb0679ec3aac8a2102197d9edb070cf0e6c0))
# [1.29.0](https://github.com/apricote/Listory/compare/v1.28.2...v1.29.0) (2023-09-30)
### Features
* **frontend:** general revamp of navigation & pages ([#303](https://github.com/apricote/Listory/issues/303)) ([4b1dd10](https://github.com/apricote/Listory/commit/4b1dd10846d741cd39fe9b0f41150e68510f2220))
## [1.28.2](https://github.com/apricote/Listory/compare/v1.28.1...v1.28.2) (2023-09-18)
### Bug Fixes
* slow query taking up 66% of db time ([#298](https://github.com/apricote/Listory/issues/298)) ([625db7d](https://github.com/apricote/Listory/commit/625db7dbe71a7315921562bfab82420c04aa6c17))
## [1.28.1](https://github.com/apricote/Listory/compare/v1.28.0...v1.28.1) (2023-09-16)
### Bug Fixes
* invalid database migration ([9ba47a5](https://github.com/apricote/Listory/commit/9ba47a560c9c98866cd2dc34c3997f96f027f65e))
# [1.28.0](https://github.com/apricote/Listory/compare/v1.27.0...v1.28.0) (2023-09-16)
### Features
* optimize db queries ([#297](https://github.com/apricote/Listory/issues/297)) ([dd57a52](https://github.com/apricote/Listory/commit/dd57a52ab66684e713c5d0766a8fed281b472e40))
# [1.27.0](https://github.com/apricote/Listory/compare/v1.26.2...v1.27.0) (2023-09-16)
### Features
* improve listens report response time ([89440da](https://github.com/apricote/Listory/commit/89440daf7ba38ff97fabd19cf3a9d11ab21efb45))
## [1.26.2](https://github.com/apricote/Listory/compare/v1.26.1...v1.26.2) (2023-09-10)

View file

@ -1,9 +1,4 @@
# syntax=docker/dockerfile:1.5
FROM scratch as ignore
WORKDIR /listory
COPY . /listory/
# syntax=docker/dockerfile:1.12
##################
## common

View file

@ -14,8 +14,8 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 1.26.2
version: 1.31.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application.
appVersion: 1.26.2
appVersion: 1.31.0

View file

@ -6,7 +6,7 @@ services:
#####
db:
image: postgres:15.3
image: postgres:16.6
restart: unless-stopped
environment:
POSTGRES_PASSWORD: listory
@ -18,7 +18,7 @@ services:
- db
api:
image: apricote/listory:1.26.2
image: apricote/listory:1.31.0
restart: unless-stopped
environment:
DB_HOST: db
@ -32,7 +32,7 @@ services:
# make sure to restart the container if you made any changes.
env_file: .env
ports:
- 3000:3000 # API
- "3000:3000" # API
networks:
- web
- db

View file

@ -12,7 +12,7 @@ services:
#####
db:
image: postgres:15.3
image: postgres:16.6
environment:
POSTGRES_PASSWORD: listory
POSTGRES_USER: listory
@ -37,7 +37,8 @@ services:
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4318
env_file: .env
volumes:
- ./src:/app/src
- ./src:/app/src:ro
- ./dist:/app/dist # build cache
ports:
- 3000 # API
- "9464:9464" # Metrics
@ -72,7 +73,7 @@ services:
- web
proxy:
image: traefik:v2.10.3
image: traefik:v2.11.15
command:
#- --log.level=debug
#- --accesslog=true
@ -115,7 +116,8 @@ services:
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4318
env_file: .env
volumes:
- ./src:/app/src
- ./src:/app/src:ro
- ./dist:/app/dist # build cache
ports:
- "9464:9464" # Metrics
networks:
@ -129,7 +131,7 @@ services:
prometheus:
profiles: ["observability"]
image: prom/prometheus:v2.45.0
image: prom/prometheus:v2.55.1
volumes:
- ./observability/prometheus:/etc/prometheus
- prometheus_data:/prometheus
@ -139,14 +141,14 @@ services:
- "--storage.tsdb.retention.time=200h"
- "--web.enable-lifecycle"
ports:
- 9090:9090
- "9090:9090"
networks:
- observability
- web
loki:
profiles: ["observability"]
image: grafana/loki:2.8.2
image: grafana/loki:2.9.11
command: ["-config.file=/etc/loki/loki.yaml"]
ports:
- "3100" # loki needs to be exposed so it receives logs
@ -157,7 +159,7 @@ services:
promtail:
profiles: ["observability"]
image: grafana/promtail:2.8.2
image: grafana/promtail:2.9.11
command: ["-config.file=/etc/promtail.yaml"]
volumes:
- ./observability/promtail/promtail.yaml:/etc/promtail.yaml
@ -175,7 +177,7 @@ services:
tempo:
profiles: ["observability"]
image: grafana/tempo:2.1.1
image: grafana/tempo:2.6.1
command: ["-config.file=/etc/tempo.yaml"]
volumes:
- ./observability/tempo/tempo.yaml:/etc/tempo.yaml
@ -189,7 +191,7 @@ services:
grafana:
profiles: ["observability"]
image: grafana/grafana-oss:9.5.5
image: grafana/grafana-oss:10.4.14
volumes:
- ./observability/grafana/provisioning:/etc/grafana/provisioning
environment:
@ -200,7 +202,7 @@ services:
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_HTTP_PORT=2345
ports:
- 2345:2345
- "2345:2345"
networks:
- observability

View file

@ -1,44 +0,0 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

16
frontend/components.json Normal file
View file

@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "./src/index.css",
"baseColor": "gray",
"cssVariables": false
},
"aliases": {
"components": "src/components",
"utils": "src/lib/utils"
}
}

13475
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,32 +8,44 @@
},
"license": "MIT",
"dependencies": {
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@types/jest": "29.5.2",
"@types/node": "18.16.19",
"@types/react": "18.2.14",
"@types/react-dom": "18.2.6",
"@radix-ui/react-avatar": "1.1.2",
"@radix-ui/react-dropdown-menu": "2.1.3",
"@radix-ui/react-label": "2.1.1",
"@radix-ui/react-navigation-menu": "1.2.2",
"@radix-ui/react-select": "2.1.3",
"@radix-ui/react-slot": "1.1.1",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.1.0",
"@testing-library/user-event": "14.5.2",
"@types/jest": "29.5.14",
"@types/node": "20.17.16",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"@types/react-router-dom": "5.3.3",
"@types/recharts": "1.8.24",
"@vitejs/plugin-react": "4.0.1",
"autoprefixer": "10.4.14",
"axios": "0.27.2",
"@types/recharts": "1.8.29",
"@vitejs/plugin-react": "4.3.4",
"autoprefixer": "10.4.20",
"axios": "1.7.9",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"date-fns": "2.30.0",
"eslint-config-react-app": "7.0.1",
"jsdom": "22.1.0",
"lucide-react": "0.468.0",
"npm-run-all": "4.1.5",
"postcss": "8.4.24",
"prettier": "2.8.8",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-router-dom": "6.14.1",
"recharts": "2.7.2",
"tailwindcss": "3.3.2",
"typescript": "5.1.6",
"vite": "4.3.9",
"vitest": "0.32.2"
"postcss": "8.4.49",
"prettier": "3.4.2",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-files": "3.0.3",
"react-router-dom": "6.28.0",
"recharts": "2.15.0",
"tailwind-merge": "1.14.0",
"tailwindcss": "3.4.16",
"tailwindcss-animate": "1.0.7",
"typescript": "5.7.2",
"vite": "5.4.12",
"vitest": "1.6.0"
},
"scripts": {
"format": "prettier --write \"./*.js\" \"src/**/*.(tsx|ts|css)\"",

View file

@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};

View file

@ -2,20 +2,21 @@ import React from "react";
import { Route, Routes } from "react-router-dom";
import { AuthApiTokens } from "./components/AuthApiTokens";
import { Footer } from "./components/Footer";
import { ImportListens } from "./components/ImportListens";
import { LoginFailure } from "./components/LoginFailure";
import { LoginLoading } from "./components/LoginLoading";
import { LoginSuccess } from "./components/LoginSuccess";
import { NavBar } from "./components/NavBar";
import { RecentListens } from "./components/RecentListens";
import { ReportListens } from "./components/ReportListens";
import { ReportTopAlbums } from "./components/ReportTopAlbums";
import { ReportTopArtists } from "./components/ReportTopArtists";
import { ReportTopGenres } from "./components/ReportTopGenres";
import { ReportTopTracks } from "./components/ReportTopTracks";
import { Navigate } from "react-router-dom";
import { RecentListens } from "./components/reports/RecentListens";
import { ReportListens } from "./components/reports/ReportListens";
import { ReportTopAlbums } from "./components/reports/ReportTopAlbums";
import { ReportTopArtists } from "./components/reports/ReportTopArtists";
import { ReportTopGenres } from "./components/reports/ReportTopGenres";
import { ReportTopTracks } from "./components/reports/ReportTopTracks";
import { useAuth } from "./hooks/use-auth";
export function App() {
const { isLoaded } = useAuth();
const { isLoaded, user } = useAuth();
if (!isLoaded) {
return <LoginLoading />;
@ -27,18 +28,43 @@ export function App() {
<NavBar />
</header>
<main className="mb-auto" /* mb-auto is for sticky footer */>
<Routes>
<Route path="/" />
<Route path="/login/success" element={<LoginSuccess />} />
<Route path="/login/failure" element={<LoginFailure />} />
<Route path="/listens" element={<RecentListens />} />
<Route path="/reports/listens" element={<ReportListens />} />
<Route path="/reports/top-artists" element={<ReportTopArtists />} />
<Route path="/reports/top-albums" element={<ReportTopAlbums />} />
<Route path="/reports/top-tracks" element={<ReportTopTracks />} />
<Route path="/reports/top-genres" element={<ReportTopGenres />} />
<Route path="/auth/api-tokens" element={<AuthApiTokens />} />
</Routes>
<div className="md:flex md:justify-center p-4 text-gray-700 dark:text-gray-400">
<div className="md:shrink-0 min-w-full xl:min-w-0 xl:w-2/3 lg:max-w-screen-lg">
{user && (
<Routes>
<Route index element={<Navigate to="/listens" />} />
<Route path="/login/success" element={<Navigate to="/" />} />
<Route path="/login/failure" element={<LoginFailure />} />
<Route path="/listens" element={<RecentListens />} />
<Route path="/reports/listens" element={<ReportListens />} />
<Route
path="/reports/top-artists"
element={<ReportTopArtists />}
/>
<Route
path="/reports/top-albums"
element={<ReportTopAlbums />}
/>
<Route
path="/reports/top-tracks"
element={<ReportTopTracks />}
/>
<Route
path="/reports/top-genres"
element={<ReportTopGenres />}
/>
<Route path="/auth/api-tokens" element={<AuthApiTokens />} />
<Route path="/import" element={<ImportListens />} />
</Routes>
)}
{!user && (
<Routes>
<Route index />
<Route path="*" element={<Navigate to="/" />} />
</Routes>
)}
</div>
</div>
</main>
<footer>
<Footer />

View file

@ -14,12 +14,14 @@ import { TopGenresItem } from "./entities/top-genres-item";
import { TopGenresOptions } from "./entities/top-genres-options";
import { TopTracksItem } from "./entities/top-tracks-item";
import { TopTracksOptions } from "./entities/top-tracks-options";
import { SpotifyExtendedStreamingHistoryItem } from "./entities/spotify-extended-streaming-history-item";
import { ExtendedStreamingHistoryStatus } from "./entities/extended-streaming-history-status";
export class UnauthenticatedError extends Error {}
export const getRecentListens = async (
options: PaginationOptions = { page: 1, limit: 10 },
client: AxiosInstance
client: AxiosInstance,
): Promise<Pagination<Listen>> => {
const { page, limit } = options;
@ -44,7 +46,7 @@ export const getRecentListens = async (
export const getListensReport = async (
options: ListenReportOptions,
client: AxiosInstance
client: AxiosInstance,
): Promise<ListenReportItem[]> => {
const {
timeFrame,
@ -60,7 +62,7 @@ export const getListensReport = async (
customTimeStart: formatISO(customTimeStart),
customTimeEnd: formatISO(customTimeEnd),
},
}
},
);
switch (res.status) {
@ -83,7 +85,7 @@ export const getListensReport = async (
export const getTopArtists = async (
options: TopArtistsOptions,
client: AxiosInstance
client: AxiosInstance,
): Promise<TopArtistsItem[]> => {
const {
time: { timePreset, customTimeStart, customTimeEnd },
@ -97,7 +99,7 @@ export const getTopArtists = async (
customTimeStart: formatISO(customTimeStart),
customTimeEnd: formatISO(customTimeEnd),
},
}
},
);
switch (res.status) {
@ -120,7 +122,7 @@ export const getTopArtists = async (
export const getTopAlbums = async (
options: TopAlbumsOptions,
client: AxiosInstance
client: AxiosInstance,
): Promise<TopAlbumsItem[]> => {
const {
time: { timePreset, customTimeStart, customTimeEnd },
@ -134,7 +136,7 @@ export const getTopAlbums = async (
customTimeStart: formatISO(customTimeStart),
customTimeEnd: formatISO(customTimeEnd),
},
}
},
);
switch (res.status) {
@ -157,7 +159,7 @@ export const getTopAlbums = async (
export const getTopTracks = async (
options: TopTracksOptions,
client: AxiosInstance
client: AxiosInstance,
): Promise<TopTracksItem[]> => {
const {
time: { timePreset, customTimeStart, customTimeEnd },
@ -171,7 +173,7 @@ export const getTopTracks = async (
customTimeStart: formatISO(customTimeStart),
customTimeEnd: formatISO(customTimeEnd),
},
}
},
);
switch (res.status) {
@ -194,7 +196,7 @@ export const getTopTracks = async (
export const getTopGenres = async (
options: TopGenresOptions,
client: AxiosInstance
client: AxiosInstance,
): Promise<TopGenresItem[]> => {
const {
time: { timePreset, customTimeStart, customTimeEnd },
@ -208,7 +210,7 @@ export const getTopGenres = async (
customTimeStart: formatISO(customTimeStart),
customTimeEnd: formatISO(customTimeEnd),
},
}
},
);
switch (res.status) {
@ -230,7 +232,7 @@ export const getTopGenres = async (
};
export const getApiTokens = async (
client: AxiosInstance
client: AxiosInstance,
): Promise<ApiToken[]> => {
const res = await client.get<ApiToken[]>(`/api/v1/auth/api-tokens`);
@ -251,7 +253,7 @@ export const getApiTokens = async (
export const createApiToken = async (
description: string,
client: AxiosInstance
client: AxiosInstance,
): Promise<NewApiToken> => {
const res = await client.post<NewApiToken>(`/api/v1/auth/api-tokens`, {
description,
@ -274,9 +276,9 @@ export const createApiToken = async (
export const revokeApiToken = async (
id: string,
client: AxiosInstance
client: AxiosInstance,
): Promise<void> => {
const res = await client.delete<NewApiToken>(`/api/v1/auth/api-tokens/${id}`);
const res = await client.delete(`/api/v1/auth/api-tokens/${id}`);
switch (res.status) {
case 200: {
@ -290,3 +292,50 @@ export const revokeApiToken = async (
}
}
};
export const importExtendedStreamingHistory = async (
listens: SpotifyExtendedStreamingHistoryItem[],
client: AxiosInstance
): Promise<void> => {
const res = await client.post(`/api/v1/import/extended-streaming-history`, {
listens,
});
switch (res.status) {
case 201: {
break;
}
case 401: {
throw new UnauthenticatedError(`No token or token expired`);
}
default: {
throw new Error(
`Unable to importExtendedStreamingHistory: ${res.status}`
);
}
}
};
export const getExtendedStreamingHistoryStatus = async (
client: AxiosInstance
): Promise<ExtendedStreamingHistoryStatus> => {
const res = await client.get<ExtendedStreamingHistoryStatus>(
`/api/v1/import/extended-streaming-history/status`
);
switch (res.status) {
case 200: {
break;
}
case 401: {
throw new UnauthenticatedError(`No token or token expired`);
}
default: {
throw new Error(
`Unable to getExtendedStreamingHistoryStatus: ${res.status}`
);
}
}
return res.data;
};

View file

@ -0,0 +1,4 @@
export interface ExtendedStreamingHistoryStatus {
total: number;
imported: number;
}

View file

@ -0,0 +1,4 @@
export interface SpotifyExtendedStreamingHistoryItem {
ts: string;
spotify_track_uri: string;
}

View file

@ -2,65 +2,61 @@ import { format, formatDistanceToNow } from "date-fns";
import React, { FormEvent, useCallback, useMemo, useState } from "react";
import { ApiToken, NewApiToken } from "../api/entities/api-token";
import { useApiTokens } from "../hooks/use-api";
import { useAuthProtection } from "../hooks/use-auth-protection";
import { SpinnerIcon } from "../icons/Spinner";
import TrashcanIcon from "../icons/Trashcan";
import { Spinner } from "./Spinner";
import { Spinner } from "./ui/Spinner";
export const AuthApiTokens: React.FC = () => {
const { requireUser } = useAuthProtection();
const { apiTokens, isLoading, createToken, revokeToken } = useApiTokens();
const sortedTokens = useMemo(
() => apiTokens.sort((a, b) => (a.createdAt > b.createdAt ? -1 : 1)),
[apiTokens]
() =>
apiTokens
.filter((token) => !token.revokedAt)
.sort((a, b) => (a.createdAt > b.createdAt ? -1 : 1)),
[apiTokens],
);
requireUser();
return (
<div className="md:flex md:justify-center p-4 text-gray-700 dark:text-gray-400">
<div className="md:shrink-0 min-w-full xl:min-w-0 xl:w-2/3 max-w-screen-lg">
<div className="flex justify-between">
<p className="text-2xl font-normal">API Tokens</p>
<>
<div className="flex justify-between">
<p className="text-2xl font-normal">API Tokens</p>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-4 m-2">
<p className="mb-4">
You can use API Tokens to access the Listory API directly. You can
find the API docs{" "}
<a href="/api/docs" target="_blank" className={"underline"}>
here
</a>
.
</p>
<div className="mb-4">
<NewTokenForm createToken={createToken} />
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-4 m-2">
<p className="mb-4">
You can use API Tokens to access the Listory API directly. You can
find the API docs{" "}
<a href="/api/docs" target="_blank">
here
</a>
.
</p>
<div className="mb-4">
<NewTokenForm createToken={createToken} />
</div>
<div>
<h3 className="text-xl">Manage Existing Tokens</h3>
{isLoading && <Spinner className="m-8" />}
{sortedTokens.length === 0 && (
<div className="text-center m-4">
<p className="">Could not find any api tokens!</p>
</div>
)}
<div>
<h3 className="text-xl">Manage Existing Tokens</h3>
{isLoading && <Spinner className="m-8" />}
{sortedTokens.length === 0 && (
<div className="text-center m-4">
<p className="">Could not find any api tokens!</p>
{sortedTokens.length > 0 && (
<div className="table-auto w-full">
{sortedTokens.map((apiToken) => (
<ApiTokenItem
apiToken={apiToken}
revokeToken={revokeToken}
key={apiToken.id}
/>
))}
</div>
)}
<div>
{sortedTokens.length > 0 && (
<div className="table-auto w-full">
{sortedTokens.map((apiToken) => (
<ApiTokenItem
apiToken={apiToken}
revokeToken={revokeToken}
key={apiToken.id}
/>
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
</>
);
};
@ -97,7 +93,7 @@ const NewTokenForm: React.FC<{
createToken,
setNewToken,
setNewTokenDescription,
]
],
);
return (

View file

@ -0,0 +1,280 @@
import React, { useCallback, useEffect, useState } from "react";
import Files from "react-files";
import type { ReactFile } from "react-files";
import {
useSpotifyImportExtendedStreamingHistory,
useSpotifyImportExtendedStreamingHistoryStatus,
} from "../hooks/use-api";
import { SpotifyExtendedStreamingHistoryItem } from "../api/entities/spotify-extended-streaming-history-item";
import { ErrorIcon } from "../icons/Error";
import { numberToPercent } from "../util/numberToPercent";
import { Button } from "./ui/button";
import { Table, TableBody, TableCell, TableRow } from "./ui/table";
import { Badge } from "./ui/badge";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "./ui/card";
import { Code } from "./ui/code";
export const ImportListens: React.FC = () => {
return (
<>
<div className="flex justify-between">
<p className="text-2xl font-normal">
Import Listens from Spotify Extended Streaming History
</p>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-4 m-2">
<p className="my-4">
Here you can import your full Spotify Listen history that was exported
from the{" "}
<a
target="blank"
href="https://www.spotify.com/us/account/privacy/"
className="underline"
>
Extended streaming history
</a>
.
</p>
<p className="my-4">
The extended streaming history contains additional personally
identifiable data such as the IP address of the listen (which can be
linked to locations). To avoid saving this on the server, the data is
preprocessed in your web browser and only the necessary data
(timestamp & track ID) are sent to the server.
</p>
<p className="my-4">
If an error occurs, you can always retry uploading the file, Listory
deduplicates any listens to make sure that everything is saved only
once.
</p>
<FileUpload />
<ImportProgress />
</div>
</>
);
};
interface FileData {
file: ReactFile;
status: Status;
error?: Error;
}
enum Status {
Select,
Import,
Finished,
Error,
}
const FileUpload: React.FC = () => {
// Using a map is ... meh, need to wrap all state updates in `new Map()` so react re-renders
const [fileMap, setFileMap] = useState<Map<ReactFile["id"], FileData>>(
new Map(),
);
const [status, setStatus] = useState<Status>(Status.Select);
const addFiles = useCallback(
(files: ReactFile[]) => {
setFileMap((_fileMap) => {
files.forEach((file) =>
_fileMap.set(file.id, { file, status: Status.Select }),
);
return new Map(_fileMap);
});
},
[setFileMap],
);
const updateFile = useCallback((data: FileData) => {
setFileMap((_fileMap) => new Map(_fileMap.set(data.file.id, data)));
}, []);
const clearFiles = useCallback(() => {
setFileMap(new Map());
}, [setFileMap]);
const { importHistory } = useSpotifyImportExtendedStreamingHistory();
const handleImport = useCallback(async () => {
setStatus(Status.Import);
let errorOccurred = false;
for (const data of fileMap.values()) {
data.status = Status.Import;
updateFile(data);
let items: SpotifyExtendedStreamingHistoryItem[];
// Scope so these tmp variables can be GC-ed ASAP
{
const fileContent = await data.file.text();
const rawItems = JSON.parse(
fileContent,
) as SpotifyExtendedStreamingHistoryItem[];
items = rawItems
.filter(({ spotify_track_uri }) => spotify_track_uri !== null)
.map(({ ts, spotify_track_uri }) => ({
ts,
spotify_track_uri,
}));
}
try {
await importHistory(items);
data.status = Status.Finished;
} catch (err) {
data.error = err as Error;
data.status = Status.Error;
errorOccurred = true;
}
updateFile(data);
}
if (!errorOccurred) {
setStatus(Status.Finished);
}
}, [fileMap, importHistory, updateFile]);
return (
<Card className="mb-5">
<CardHeader>
<CardTitle>File Upload</CardTitle>
<CardDescription>
Select <Code>endsong_XY.json</Code> files here and start the import.
</CardDescription>
</CardHeader>
<CardContent>
<Files
className="shadow-inner bg-gray-200 dark:bg-gray-700 rounded p-4 text-center cursor-pointer"
dragActiveClassName=""
onChange={addFiles}
accepts={["application/json"]}
multiple
clickable
>
Drop files here or click to upload
</Files>
<Table>
<TableBody>
{Array.from(fileMap.values()).map((data) => (
<File key={data.file.id} data={data} />
))}
</TableBody>
</Table>
</CardContent>
<CardFooter className="flex gap-x-2">
<Button
onClick={() =>
handleImport().catch((e) => console.error("Import Failed:", e))
}
variant="secondary"
disabled={status !== Status.Select}
>
Start Import
</Button>
<Button
onClick={clearFiles}
variant="secondary"
disabled={status !== Status.Select}
>
Remove All Files
</Button>
</CardFooter>
</Card>
);
};
const File: React.FC<{ data: FileData }> = ({ data }) => {
const hasErrors = data.status === Status.Error && data.error;
return (
<TableRow>
<TableCell>{data.file.name}</TableCell>
<TableCell className="text-sm font-thin">
{data.file.sizeReadable}
</TableCell>
<TableCell className="text-right">
{data.status === Status.Select && <Badge>Prepared for import!</Badge>}
{data.status === Status.Import && <Badge>Loading!</Badge>}
{data.status === Status.Finished && <Badge>Check!</Badge>}
{hasErrors && (
<Badge variant="destructive">
<ErrorIcon />
{data.error?.message}
</Badge>
)}
</TableCell>
</TableRow>
);
};
const ImportProgress: React.FC = () => {
const {
importStatus: { total, imported },
isLoading,
reload,
} = useSpotifyImportExtendedStreamingHistoryStatus();
useEffect(() => {
const interval = setInterval(() => {
if (!isLoading) {
reload();
}
}, 1000);
return () => clearInterval(interval);
}, [isLoading, reload]);
return (
<Card>
<CardHeader>
<CardTitle>Import Progress</CardTitle>
<CardDescription>
Shows how many of the submitted listens are already imported and
visible to you. This will take a while, and the process might halt for
a few minutes if we hit the Spotify API rate limit. If this is not
finished after a few hours, please contact your Listory administrator.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex pb-2">
<div className="md:flex w-10/12">
<div className={`md:w-full font-bold`}>
Imported
<br />
{imported}
</div>
</div>
<div className="w-2/12 text-right">
Total
<br />
{total}
</div>
</div>
{total > 0 && (
<div className="h-2 w-full bg-gradient-to-r from-teal-200/25 via-green-400 to-violet-400 dark:from-teal-700/25 dark:via-green-600/85 dark:to-amber-500 flex flex-row-reverse">
<div
style={{ width: numberToPercent(1 - imported / total) }}
className="h-full bg-gray-100 dark:bg-gray-900"
></div>
</div>
)}
</CardContent>
</Card>
);
};

View file

@ -1,5 +1,5 @@
import React from "react";
import { Spinner } from "./Spinner";
import { Spinner } from "./ui/Spinner";
export const LoginLoading: React.FC = () => (
<main className="sm:flex sm:justify-center p-4 dark:bg-gray-900 h-screen">

View file

@ -1,7 +0,0 @@
import React from "react";
import { useNavigate } from "react-router-dom";
export const LoginSuccess: React.FC = () => {
useNavigate()("/", { replace: false });
return null;
};

View file

@ -1,55 +1,124 @@
import React, { useCallback, useRef, useState } from "react";
import React from "react";
import { Link } from "react-router-dom";
import { User } from "../api/entities/user";
import { useAuth } from "../hooks/use-auth";
import { useOutsideClick } from "../hooks/use-outside-click";
import { CogwheelIcon } from "../icons/Cogwheel";
import { ImportIcon } from "../icons/Import";
import { SpotifyLogo } from "../icons/Spotify";
import { Avatar, AvatarImage, AvatarFallback } from "./ui/avatar";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "./ui/navigation-menu";
import { cn } from "../lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import { Button } from "./ui/button";
export const NavBar: React.FC = () => {
const { user, loginWithSpotifyProps } = useAuth();
return (
<div className="flex items-center justify-between flex-wrap bg-green-500 dark:bg-gray-800 p-6">
<div className="flex items-center shrink-0 text-white mr-6">
<span className="font-semibold text-xl tracking-tight">Listory</span>
<div className="flex items-center justify-between flex-wrap py-3 px-6 bg-green-500 dark:bg-gray-800 dark:text-gray-100">
<div className="flex items-center shrink-0 mr-6">
<span className="font-semibold text-xl tracking-tight text-white">
Listory
</span>
</div>
<nav className="w-full block grow lg:flex lg:items-center lg:w-auto ">
<div className="text-sm lg:grow">
<nav className="w-full grow sm:flex sm:items-center sm:w-auto">
<div className="sm:grow">
{user && (
<>
<Link to="/">
<NavItem>Home</NavItem>
</Link>
<Link to="/listens">
<NavItem>Your Listens</NavItem>
</Link>
<Link to="/reports/listens">
<NavItem>Listens Report</NavItem>
</Link>
<Link to="/reports/top-artists">
<NavItem>Top Artists</NavItem>
</Link>
<Link to="/reports/top-albums">
<NavItem>Top Albums</NavItem>
</Link>
<Link to="/reports/top-tracks">
<NavItem>Top Tracks</NavItem>
</Link>
<Link to="/reports/top-genres">
<NavItem>Top Genres</NavItem>
</Link>
</>
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuLink
asChild
className={navigationMenuTriggerStyle()}
>
<Link to="/">Home</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
asChild
className={navigationMenuTriggerStyle()}
>
<Link to="/listens">Your Listens</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Reports</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid gap-3 p-4 grid-flow-row grid-cols-1 sm:grid-cols-2 w-6 min-w-max sm:min-w-fit sm:w-[500px]">
<NavListItem title="Listens" to={"/reports/listens"}>
When did you listen how much music?
</NavListItem>
<NavListItem
title="Top Artists"
to={"/reports/top-artists"}
>
What are your top artists in the last week/month/year?
</NavListItem>
<NavListItem
title="Top Albums"
to={"/reports/top-albums"}
>
What are your top albums in the last week/month/year?
</NavListItem>
<NavListItem
title="Top Tracks"
to={"/reports/top-tracks"}
>
What are your top tracks in the last week/month/year?
</NavListItem>
<NavListItem
title="Top Genres"
to={"/reports/top-genres"}
>
What are your top genres in the last week/month/year?
</NavListItem>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
)}
</div>
<div>
{!user && (
<a {...loginWithSpotifyProps()}>
<NavItem>
Login with Spotify{" "}
<SpotifyLogo className="w-6 h-6 ml-2 mb-1 inline fill-current text-white" />
</NavItem>
</a>
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuLink
asChild
className={navigationMenuTriggerStyle()}
>
<a {...loginWithSpotifyProps()}>
<span>Login with Spotify </span>
<SpotifyLogo className="w-6 h-6 ml-2 mb-1 inline fill-current text-white" />
</a>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
)}
{user && <NavUserInfo user={user} />}
</div>
@ -58,58 +127,76 @@ export const NavBar: React.FC = () => {
);
};
const NavItem: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const NavListItem = React.forwardRef<
React.ElementRef<typeof Link>,
React.ComponentPropsWithoutRef<typeof Link>
>(({ className, title, children, ...props }, ref) => {
return (
<span className="block mt-4 lg:inline-block lg:mt-0 text-green-200 hover:text-white mr-4">
{children}
</span>
<li>
<NavigationMenuLink asChild>
<Link
ref={ref}
className={cn(
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
className,
)}
{...props}
>
<div className="text-sm font-medium leading-none">{title}</div>
<p className="line-clamp-3 text-sm leading-snug text-muted-foreground">
{children}
</p>
</Link>
</NavigationMenuLink>
</li>
);
};
});
NavListItem.displayName = "NavListItem";
const NavUserInfo: React.FC<{ user: User }> = ({ user }) => {
const [menuOpen, setMenuOpen] = useState<boolean>(false);
const closeMenu = useCallback(() => setMenuOpen(false), [setMenuOpen]);
const wrapperRef = useRef(null);
useOutsideClick(wrapperRef, closeMenu);
return (
<div ref={wrapperRef}>
<div
className="flex items-center mr-4 mt-4 lg:mt-0 cursor-pointer"
onClick={() => setMenuOpen(!menuOpen)}
>
<span className="text-green-200 text-sm">{user.displayName}</span>
{user.photo && (
<img
className="w-6 h-6 rounded-full ml-4"
src={user.photo}
alt="Profile of logged in user"
></img>
)}
</div>
{menuOpen ? <NavUserInfoMenu closeMenu={closeMenu} /> : null}
</div>
);
};
const NavUserInfoMenu: React.FC<{ closeMenu: () => void }> = ({
closeMenu,
}) => {
return (
<div className="relative">
<div className="drop-down w-48 overflow-hidden bg-green-100 dark:bg-gray-700 text-gray-700 dark:text-green-200 rounded-md shadow absolute top-3 right-3">
<ul>
<li className="px-3 py-3 text-sm font-medium flex items-center space-x-2 hover:bg-green-200 hover:text-gray-800 dark:hover:text-white">
<span>
<CogwheelIcon className="w-5 h-5 fill-current" />
</span>
<Link to="/auth/api-tokens" onClick={closeMenu}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant={"ghost"}
className="flex flex-row-reverse sm:flex-row px-0 mt-2 sm:px-8"
>
<span className="text-green-200 pl-2 sm:pr-2">
{user.displayName}
</span>
<Avatar>
<AvatarImage
src={user.photo}
alt="Profile picture of logged in user"
/>
<AvatarFallback>
{user.displayName
.split(" ")
.filter((name) => name.length > 0)
.map((name) => name[0].toUpperCase())
.join("")}
</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link to="/auth/api-tokens">
<CogwheelIcon className="w-5 h-5 fill-current pr-2" />
API Tokens
</Link>
</li>
</ul>
</div>
</div>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/import">
<ImportIcon className="w-5 h-5 fill-current pr-2" />
Import Listens
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};

View file

@ -1,85 +0,0 @@
import React, { useMemo, useState } from "react";
import { Album } from "../api/entities/album";
import { TimeOptions } from "../api/entities/time-options";
import { TimePreset } from "../api/entities/time-preset.enum";
import { useTopAlbums } from "../hooks/use-api";
import { useAuthProtection } from "../hooks/use-auth-protection";
import { getMaxCount } from "../util/getMaxCount";
import { ReportTimeOptions } from "./ReportTimeOptions";
import { Spinner } from "./Spinner";
import { TopListItem } from "./TopListItem";
export const ReportTopAlbums: React.FC = () => {
const { requireUser } = useAuthProtection();
const [timeOptions, setTimeOptions] = useState<TimeOptions>({
timePreset: TimePreset.LAST_90_DAYS,
customTimeStart: new Date("2020"),
customTimeEnd: new Date(),
});
const options = useMemo(
() => ({
time: timeOptions,
}),
[timeOptions]
);
const { topAlbums, isLoading } = useTopAlbums(options);
const reportHasItems = topAlbums.length !== 0;
const maxCount = getMaxCount(topAlbums);
requireUser();
return (
<div className="md:flex md:justify-center p-4">
<div className="md:shrink-0 min-w-full xl:min-w-0 xl:w-2/3 max-w-screen-lg">
<div className="flex justify-between">
<p className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Top Albums
</p>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-5 m-2">
<ReportTimeOptions
timeOptions={timeOptions}
setTimeOptions={setTimeOptions}
/>
{isLoading && <Spinner className="m-8" />}
{!reportHasItems && !isLoading && (
<div>
<p>Report is emtpy! :(</p>
</div>
)}
{reportHasItems &&
topAlbums.map(({ album, count }) => (
<ReportItem
key={album.id}
album={album}
count={count}
maxCount={maxCount}
/>
))}
</div>
</div>
</div>
);
};
const ReportItem: React.FC<{
album: Album;
count: number;
maxCount: number;
}> = ({ album, count, maxCount }) => {
const artists = album.artists?.map((artist) => artist.name).join(", ") || "";
return (
<TopListItem
key={album.id}
title={album.name}
subTitle={artists}
count={count}
maxCount={maxCount}
/>
);
};

View file

@ -1,67 +0,0 @@
import React, { useMemo, useState } from "react";
import { TimeOptions } from "../api/entities/time-options";
import { TimePreset } from "../api/entities/time-preset.enum";
import { useTopArtists } from "../hooks/use-api";
import { useAuthProtection } from "../hooks/use-auth-protection";
import { getMaxCount } from "../util/getMaxCount";
import { ReportTimeOptions } from "./ReportTimeOptions";
import { Spinner } from "./Spinner";
import { TopListItem } from "./TopListItem";
export const ReportTopArtists: React.FC = () => {
const { requireUser } = useAuthProtection();
const [timeOptions, setTimeOptions] = useState<TimeOptions>({
timePreset: TimePreset.LAST_90_DAYS,
customTimeStart: new Date("2020"),
customTimeEnd: new Date(),
});
const options = useMemo(
() => ({
time: timeOptions,
}),
[timeOptions]
);
const { topArtists, isLoading } = useTopArtists(options);
const reportHasItems = topArtists.length !== 0;
const maxCount = getMaxCount(topArtists);
requireUser();
return (
<div className="md:flex md:justify-center p-4">
<div className="md:shrink-0 min-w-full xl:min-w-0 xl:w-2/3 max-w-screen-lg">
<div className="flex justify-between">
<p className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Top Artists
</p>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-5 m-2">
<ReportTimeOptions
timeOptions={timeOptions}
setTimeOptions={setTimeOptions}
/>
{isLoading && <Spinner className="m-8" />}
{!reportHasItems && !isLoading && (
<div>
<p>Report is emtpy! :(</p>
</div>
)}
{reportHasItems &&
topArtists.map(({ artist, count }) => (
<TopListItem
key={artist.id}
title={artist.name}
count={count}
maxCount={maxCount}
/>
))}
</div>
</div>
</div>
);
};

View file

@ -1,102 +0,0 @@
import React, { useMemo, useState } from "react";
import { Artist } from "../api/entities/artist";
import { Genre } from "../api/entities/genre";
import { TimeOptions } from "../api/entities/time-options";
import { TimePreset } from "../api/entities/time-preset.enum";
import { TopArtistsItem } from "../api/entities/top-artists-item";
import { useTopGenres } from "../hooks/use-api";
import { useAuthProtection } from "../hooks/use-auth-protection";
import { capitalizeString } from "../util/capitalizeString";
import { getMaxCount } from "../util/getMaxCount";
import { ReportTimeOptions } from "./ReportTimeOptions";
import { Spinner } from "./Spinner";
import { TopListItem } from "./TopListItem";
export const ReportTopGenres: React.FC = () => {
const { requireUser } = useAuthProtection();
const [timeOptions, setTimeOptions] = useState<TimeOptions>({
timePreset: TimePreset.LAST_90_DAYS,
customTimeStart: new Date("2020"),
customTimeEnd: new Date(),
});
const options = useMemo(
() => ({
time: timeOptions,
}),
[timeOptions]
);
const { topGenres, isLoading } = useTopGenres(options);
const reportHasItems = topGenres.length !== 0;
requireUser();
const maxCount = getMaxCount(topGenres);
return (
<div className="md:flex md:justify-center p-4">
<div className="md:shrink-0 min-w-full xl:min-w-0 xl:w-2/3 max-w-screen-lg">
<div className="flex justify-between">
<p className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Top Genres
</p>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-5 m-2">
<ReportTimeOptions
timeOptions={timeOptions}
setTimeOptions={setTimeOptions}
/>
{isLoading && <Spinner className="m-8" />}
{!reportHasItems && !isLoading && (
<div>
<p>Report is emtpy! :(</p>
</div>
)}
{reportHasItems &&
topGenres.map(({ genre, artists, count }) => (
<ReportItem
key={genre.id}
genre={genre}
count={count}
artists={artists}
maxCount={maxCount}
/>
))}
</div>
</div>
</div>
);
};
const ReportItem: React.FC<{
genre: Genre;
artists: TopArtistsItem[];
count: number;
maxCount: number;
}> = ({ genre, artists, count, maxCount }) => {
const artistList = artists
.map(({ artist, count: artistCount }) => (
<ArtistItem key={artist.id} artist={artist} count={artistCount} />
))
// @ts-expect-error
.reduce((acc, curr) => (acc === null ? [curr] : [acc, ", ", curr]), null);
return (
<TopListItem
title={capitalizeString(genre.name)}
subTitle={artistList}
count={count}
maxCount={maxCount}
/>
);
};
const ArtistItem: React.FC<{
artist: Artist;
count: number;
}> = ({ artist, count }) => (
<span title={`Listens: ${count}`}>{artist.name}</span>
);

View file

@ -1,85 +0,0 @@
import React, { useMemo, useState } from "react";
import { TimeOptions } from "../api/entities/time-options";
import { TimePreset } from "../api/entities/time-preset.enum";
import { Track } from "../api/entities/track";
import { useTopTracks } from "../hooks/use-api";
import { useAuthProtection } from "../hooks/use-auth-protection";
import { getMaxCount } from "../util/getMaxCount";
import { ReportTimeOptions } from "./ReportTimeOptions";
import { Spinner } from "./Spinner";
import { TopListItem } from "./TopListItem";
export const ReportTopTracks: React.FC = () => {
const { requireUser } = useAuthProtection();
const [timeOptions, setTimeOptions] = useState<TimeOptions>({
timePreset: TimePreset.LAST_90_DAYS,
customTimeStart: new Date("2020"),
customTimeEnd: new Date(),
});
const options = useMemo(
() => ({
time: timeOptions,
}),
[timeOptions]
);
const { topTracks, isLoading } = useTopTracks(options);
const reportHasItems = topTracks.length !== 0;
requireUser();
const maxCount = getMaxCount(topTracks);
return (
<div className="md:flex md:justify-center p-4">
<div className="md:shrink-0 min-w-full xl:min-w-0 xl:w-2/3 max-w-screen-lg">
<div className="flex justify-between">
<p className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Top Tracks
</p>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-5 m-2">
<ReportTimeOptions
timeOptions={timeOptions}
setTimeOptions={setTimeOptions}
/>
{isLoading && <Spinner className="m-8" />}
{!reportHasItems && !isLoading && (
<div>
<p>Report is emtpy! :(</p>
</div>
)}
{reportHasItems &&
topTracks.map(({ track, count }) => (
<ReportItem
key={track.id}
track={track}
count={count}
maxCount={maxCount}
/>
))}
</div>
</div>
</div>
);
};
const ReportItem: React.FC<{
track: Track;
count: number;
maxCount: number;
}> = ({ track, count, maxCount }) => {
const artists = track.artists?.map((artist) => artist.name).join(", ") || "";
return (
<TopListItem
title={track.name}
subTitle={artists}
count={count}
maxCount={maxCount}
/>
);
};

View file

@ -0,0 +1,73 @@
import React, { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View file

@ -1,17 +1,16 @@
import { format, formatDistanceToNow } from "date-fns";
import React, { useEffect, useMemo, useState } from "react";
import { Listen } from "../api/entities/listen";
import { useRecentListens } from "../hooks/use-api";
import { useAuthProtection } from "../hooks/use-auth-protection";
import { ReloadIcon } from "../icons/Reload";
import { getPaginationItems } from "../util/getPaginationItems";
import { Spinner } from "./Spinner";
import { Listen } from "../../api/entities/listen";
import { useRecentListens } from "../../hooks/use-api";
import { ReloadIcon } from "../../icons/Reload";
import { getPaginationItems } from "../../util/getPaginationItems";
import { Spinner } from "../ui/Spinner";
import { Table, TableBody, TableCell, TableRow } from "../ui/table";
import { Button } from "../ui/button";
const LISTENS_PER_PAGE = 15;
export const RecentListens: React.FC = () => {
const { requireUser } = useAuthProtection();
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
@ -26,44 +25,43 @@ export const RecentListens: React.FC = () => {
}
}, [totalPages, paginationMeta]);
requireUser();
return (
<div className="md:flex md:justify-center p-4">
<div className="md:shrink-0 min-w-full xl:min-w-0 xl:w-2/3 max-w-screen-lg">
<div className="flex justify-between">
<p className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Recent listens
</p>
<button
className="shrink-0 mx-2 bg-transparent hover:bg-green-500 text-green-500 hover:text-white font-semibold py-2 px-4 border border-green-500 hover:border-transparent rounded"
onClick={reload}
>
<ReloadIcon className="w-5 h-5 fill-current" />
</button>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-2 m-2">
{isLoading && <Spinner className="m-8" />}
{recentListens.length === 0 && (
<div className="text-center m-4">
<p className="text-gray-700 dark:text-gray-400">
Could not find any listens!
</p>
</div>
)}
<div>
{recentListens.length > 0 && (
<div className="table-auto w-full">
<>
<div className="flex justify-between">
<h2 className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Recent listens
</h2>
<Button
className="shrink-0 mx-2 bg-transparent hover:bg-green-500 text-green-500 hover:text-white font-semibold py-2 px-4 border border-green-500 hover:border-transparent rounded"
onClick={reload}
variant="outline"
>
<ReloadIcon className="w-5 h-5 fill-current" />
</Button>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-2 m-2">
{isLoading && <Spinner className="m-8" />}
{recentListens.length === 0 && (
<div className="text-center m-4">
<p className="text-gray-700 dark:text-gray-400">
Could not find any listens!
</p>
</div>
)}
<div>
{recentListens.length > 0 && (
<Table className="table-auto w-full">
<TableBody>
{recentListens.map((listen) => (
<ListenItem listen={listen} key={listen.id} />
))}
</div>
)}
</div>
<Pagination page={page} totalPages={totalPages} setPage={setPage} />
</TableBody>
</Table>
)}
</div>
<Pagination page={page} totalPages={totalPages} setPage={setPage} />
</div>
</div>
</>
);
};
@ -111,7 +109,7 @@ const Pagination: React.FC<{
>
...
</div>
)
),
)}
<button
className={`${
@ -134,15 +132,19 @@ const ListenItem: React.FC<{ listen: Listen }> = ({ listen }) => {
});
const dateTime = format(new Date(listen.playedAt), "PP p");
return (
<div className="hover:bg-gray-100 dark:hover:bg-gray-700 border-b border-gray-200 dark:border-gray-700/25 md:flex md:justify-around text-gray-700 dark:text-gray-300 px-2 py-2">
<div className="md:w-1/2 font-bold">{trackName}</div>
<div className=" md:w-1/3">{artists}</div>
<div
className="md:w-1/6 text-gray-500 font-extra-light text-sm"
<TableRow className="sm:flex sm:justify-around sm:hover:bg-gray-100 sm:dark:hover:bg-gray-700 border-b border-gray-200 dark:border-gray-700/25 text-gray-700 dark:text-gray-300 px-2 py-2">
<TableCell className="block py-1 sm:p-1 sm:table-cell sm:w-1/2 font-bold text-l">
{trackName}
</TableCell>
<TableCell className="block py-1 sm:p-1 sm:table-cell sm:w-1/3 text-l">
{artists}
</TableCell>
<TableCell
className="block py-1 sm:p-1 sm:table-cell sm:w-1/6 font-extra-light text-sm"
title={dateTime}
>
{timeAgo}
</div>
</div>
</TableCell>
</TableRow>
);
};

View file

@ -10,20 +10,25 @@ import {
XAxis,
YAxis,
} from "recharts";
import { ListenReportItem } from "../api/entities/listen-report-item";
import { ListenReportOptions } from "../api/entities/listen-report-options";
import { TimeOptions } from "../api/entities/time-options";
import { TimePreset } from "../api/entities/time-preset.enum";
import { useListensReport } from "../hooks/use-api";
import { useAuthProtection } from "../hooks/use-auth-protection";
import { ListenReportItem } from "../../api/entities/listen-report-item";
import { ListenReportOptions } from "../../api/entities/listen-report-options";
import { TimeOptions } from "../../api/entities/time-options";
import { TimePreset } from "../../api/entities/time-preset.enum";
import { useListensReport } from "../../hooks/use-api";
import { ReportTimeOptions } from "./ReportTimeOptions";
import { Spinner } from "./Spinner";
import { Spinner } from "../ui/Spinner";
import { Label } from "../ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
export const ReportListens: React.FC = () => {
const { requireUser } = useAuthProtection();
const [timeFrame, setTimeFrame] = useState<"day" | "week" | "month" | "year">(
"day"
"day",
);
const [timeOptions, setTimeOptions] = useState<TimeOptions>({
@ -34,60 +39,60 @@ export const ReportListens: React.FC = () => {
const reportOptions = useMemo(
() => ({ timeFrame, time: timeOptions }),
[timeFrame, timeOptions]
[timeFrame, timeOptions],
);
const { report, isLoading } = useListensReport(reportOptions);
const reportHasItems = report.length !== 0;
requireUser();
return (
<div className="md:flex md:justify-center p-4">
<div className="md:shrink-0 min-w-full xl:min-w-0 xl:w-2/3 max-w-screen-lg">
<div className="flex justify-between">
<p className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Listen Report
</p>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-5 m-2">
<div className="md:flex">
<div className="text-gray-700 dark:text-gray-300 mr-2">
<label className="text-sm">Timeframe</label>
<select
className="block appearance-none min-w-full md:win-w-0 md:w-1/4 bg-white dark:bg-gray-700 border border-gray-400 hover:border-gray-500 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:text-gray-200 p-2 rounded shadow leading-tight focus:outline-none focus:ring"
onChange={(e) =>
setTimeFrame(
e.target.value as "day" | "week" | "month" | "year"
)
}
>
<option value="day">Daily</option>
<option value="week">Weekly</option>
<option value="month">Monthly</option>
<option value="year">Yearly</option>
</select>
</div>
<ReportTimeOptions
timeOptions={timeOptions}
setTimeOptions={setTimeOptions}
/>
</div>
{isLoading && <Spinner className="m-8" />}
{!reportHasItems && !isLoading && (
<div>
<p>Report is emtpy! :(</p>
</div>
)}
{reportHasItems && (
<div className="w-full text-gray-700 dark:text-gray-300 mt-5">
<ReportGraph timeFrame={timeFrame} data={report} />
</div>
)}
</div>
<>
<div className="flex justify-between">
<h2 className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Listen Report
</h2>
</div>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-5 m-2">
<div className="sm:flex">
<div className="text-gray-700 dark:text-gray-300 mr-2">
<Label className="text-sm" htmlFor={"timeframe"}>
Timeframe
</Label>
<Select
onValueChange={(e: "day" | "week" | "month" | "year") =>
setTimeFrame(e)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Choose aggregation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="day">Daily</SelectItem>
<SelectItem value="week">Weekly</SelectItem>
<SelectItem value="month">Monthly</SelectItem>
<SelectItem value="year">Yearly</SelectItem>
</SelectContent>
</Select>
</div>
<ReportTimeOptions
timeOptions={timeOptions}
setTimeOptions={setTimeOptions}
/>
</div>
{isLoading && <Spinner className="m-8" />}
{!reportHasItems && !isLoading && (
<div>
<p>Report is empty! :(</p>
</div>
)}
{reportHasItems && (
<div className="w-full text-gray-700 dark:text-gray-300 mt-5">
<ReportGraph timeFrame={timeFrame} data={report} />
</div>
)}
</div>
</>
);
};
@ -128,7 +133,7 @@ const ReportGraph: React.FC<{
<AreaChart
data={dataLocal}
margin={{
left: -20,
left: -5,
}}
>
<defs>
@ -163,7 +168,7 @@ const ReportGraph: React.FC<{
};
const shortDateFormatFromTimeFrame = (
timeFrame: "day" | "week" | "month" | "year"
timeFrame: "day" | "week" | "month" | "year",
): string => {
const FORMAT_DAY = "P";
const FORMAT_WEEK = "'Week' w yyyy";
@ -186,7 +191,7 @@ const shortDateFormatFromTimeFrame = (
};
const dateFormatFromTimeFrame = (
timeFrame: "day" | "week" | "month" | "year"
timeFrame: "day" | "week" | "month" | "year",
): string => {
const FORMAT_DAY = "PPPP";
const FORMAT_WEEK = "'Week starting on' PPPP";

View file

@ -1,7 +1,15 @@
import React from "react";
import { TimeOptions } from "../api/entities/time-options";
import { TimePreset } from "../api/entities/time-preset.enum";
import { DateSelect } from "./inputs/DateSelect";
import { TimeOptions } from "../../api/entities/time-options";
import { TimePreset } from "../../api/entities/time-preset.enum";
import { DateSelect } from "../inputs/DateSelect";
import { Label } from "../ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
interface ReportTimeOptionsProps {
timeOptions: TimeOptions;
@ -23,28 +31,34 @@ export const ReportTimeOptions: React.FC<ReportTimeOptionsProps> = ({
setTimeOptions,
}) => {
return (
<div className="md:flex mb-4">
<div className="sm:flex mb-4">
<div className="text-gray-700 dark:text-gray-300">
<label className="text-sm">Timeframe</label>
<select
className="block appearance-none min-w-full md:w-1/4 bg-white dark:bg-gray-700 border border-gray-400 hover:border-gray-500 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:text-gray-200 p-2 rounded shadow leading-tight focus:outline-none focus:ring"
onChange={(e) =>
<Label className="text-sm" htmlFor={"period"}>
Period
</Label>
<Select
onValueChange={(e: TimePreset) =>
setTimeOptions({
...timeOptions,
timePreset: e.target.value as TimePreset,
timePreset: e,
})
}
value={timeOptions.timePreset}
>
{timePresetOptions.map(({ value, description }) => (
<option value={value} key={value}>
{description}
</option>
))}
</select>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Time period" />
</SelectTrigger>
<SelectContent>
{timePresetOptions.map(({ value, description }) => (
<SelectItem value={value} key={value}>
{description}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{timeOptions.timePreset === TimePreset.CUSTOM && (
<div className="md:flex text-gray-700 dark:text-gray-200">
<div className="sm:flex text-gray-700 dark:text-gray-200">
<div className="pl-2">
<DateSelect
label="Start"

View file

@ -0,0 +1,78 @@
import React, { useMemo, useState } from "react";
import { Album } from "../../api/entities/album";
import { TimeOptions } from "../../api/entities/time-options";
import { TimePreset } from "../../api/entities/time-preset.enum";
import { useTopAlbums } from "../../hooks/use-api";
import { getMaxCount } from "../../util/getMaxCount";
import { ReportTimeOptions } from "./ReportTimeOptions";
import { Spinner } from "../ui/Spinner";
import { TopListItem } from "./TopListItem";
export const ReportTopAlbums: React.FC = () => {
const [timeOptions, setTimeOptions] = useState<TimeOptions>({
timePreset: TimePreset.LAST_90_DAYS,
customTimeStart: new Date("2020"),
customTimeEnd: new Date(),
});
const options = useMemo(
() => ({
time: timeOptions,
}),
[timeOptions],
);
const { topAlbums, isLoading } = useTopAlbums(options);
const reportHasItems = topAlbums.length !== 0;
const maxCount = getMaxCount(topAlbums);
return (
<>
<div className="flex justify-between">
<h2 className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Top Albums
</h2>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-5 m-2">
<ReportTimeOptions
timeOptions={timeOptions}
setTimeOptions={setTimeOptions}
/>
{isLoading && <Spinner className="m-8" />}
{!reportHasItems && !isLoading && (
<div>
<p>Report is emtpy! :(</p>
</div>
)}
{reportHasItems &&
topAlbums.map(({ album, count }) => (
<ReportItem
key={album.id}
album={album}
count={count}
maxCount={maxCount}
/>
))}
</div>
</>
);
};
const ReportItem: React.FC<{
album: Album;
count: number;
maxCount: number;
}> = ({ album, count, maxCount }) => {
const artists = album.artists?.map((artist) => artist.name).join(", ") || "";
return (
<TopListItem
key={album.id}
title={album.name}
subTitle={artists}
count={count}
maxCount={maxCount}
/>
);
};

View file

@ -0,0 +1,60 @@
import React, { useMemo, useState } from "react";
import { TimeOptions } from "../../api/entities/time-options";
import { TimePreset } from "../../api/entities/time-preset.enum";
import { useTopArtists } from "../../hooks/use-api";
import { getMaxCount } from "../../util/getMaxCount";
import { ReportTimeOptions } from "./ReportTimeOptions";
import { Spinner } from "../ui/Spinner";
import { TopListItem } from "./TopListItem";
export const ReportTopArtists: React.FC = () => {
const [timeOptions, setTimeOptions] = useState<TimeOptions>({
timePreset: TimePreset.LAST_90_DAYS,
customTimeStart: new Date("2020"),
customTimeEnd: new Date(),
});
const options = useMemo(
() => ({
time: timeOptions,
}),
[timeOptions],
);
const { topArtists, isLoading } = useTopArtists(options);
const reportHasItems = topArtists.length !== 0;
const maxCount = getMaxCount(topArtists);
return (
<>
<div className="flex justify-between">
<h2 className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Top Artists
</h2>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-5 m-2">
<ReportTimeOptions
timeOptions={timeOptions}
setTimeOptions={setTimeOptions}
/>
{isLoading && <Spinner className="m-8" />}
{!reportHasItems && !isLoading && (
<div>
<p>Report is emtpy! :(</p>
</div>
)}
{reportHasItems &&
topArtists.map(({ artist, count }) => (
<TopListItem
key={artist.id}
title={artist.name}
count={count}
maxCount={maxCount}
/>
))}
</div>
</>
);
};

View file

@ -0,0 +1,95 @@
import React, { useMemo, useState } from "react";
import { Artist } from "../../api/entities/artist";
import { Genre } from "../../api/entities/genre";
import { TimeOptions } from "../../api/entities/time-options";
import { TimePreset } from "../../api/entities/time-preset.enum";
import { TopArtistsItem } from "../../api/entities/top-artists-item";
import { useTopGenres } from "../../hooks/use-api";
import { capitalizeString } from "../../util/capitalizeString";
import { getMaxCount } from "../../util/getMaxCount";
import { ReportTimeOptions } from "./ReportTimeOptions";
import { Spinner } from "../ui/Spinner";
import { TopListItem } from "./TopListItem";
export const ReportTopGenres: React.FC = () => {
const [timeOptions, setTimeOptions] = useState<TimeOptions>({
timePreset: TimePreset.LAST_90_DAYS,
customTimeStart: new Date("2020"),
customTimeEnd: new Date(),
});
const options = useMemo(
() => ({
time: timeOptions,
}),
[timeOptions],
);
const { topGenres, isLoading } = useTopGenres(options);
const reportHasItems = topGenres.length !== 0;
const maxCount = getMaxCount(topGenres);
return (
<>
<div className="flex justify-between">
<h2 className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Top Genres
</h2>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-5 m-2">
<ReportTimeOptions
timeOptions={timeOptions}
setTimeOptions={setTimeOptions}
/>
{isLoading && <Spinner className="m-8" />}
{!reportHasItems && !isLoading && (
<div>
<p>Report is emtpy! :(</p>
</div>
)}
{reportHasItems &&
topGenres.map(({ genre, artists, count }) => (
<ReportItem
key={genre.id}
genre={genre}
count={count}
artists={artists}
maxCount={maxCount}
/>
))}
</div>
</>
);
};
const ReportItem: React.FC<{
genre: Genre;
artists: TopArtistsItem[];
count: number;
maxCount: number;
}> = ({ genre, artists, count, maxCount }) => {
const artistList = artists
.map(({ artist, count: artistCount }) => (
<ArtistItem key={artist.id} artist={artist} count={artistCount} />
))
// @ts-expect-error
.reduce((acc, curr) => (acc === null ? [curr] : [acc, ", ", curr]), null);
return (
<TopListItem
title={capitalizeString(genre.name)}
subTitle={artistList}
count={count}
maxCount={maxCount}
/>
);
};
const ArtistItem: React.FC<{
artist: Artist;
count: number;
}> = ({ artist, count }) => (
<span title={`Listens: ${count}`}>{artist.name}</span>
);

View file

@ -0,0 +1,78 @@
import React, { useMemo, useState } from "react";
import { TimeOptions } from "../../api/entities/time-options";
import { TimePreset } from "../../api/entities/time-preset.enum";
import { Track } from "../../api/entities/track";
import { useTopTracks } from "../../hooks/use-api";
import { getMaxCount } from "../../util/getMaxCount";
import { ReportTimeOptions } from "./ReportTimeOptions";
import { Spinner } from "../ui/Spinner";
import { TopListItem } from "./TopListItem";
export const ReportTopTracks: React.FC = () => {
const [timeOptions, setTimeOptions] = useState<TimeOptions>({
timePreset: TimePreset.LAST_90_DAYS,
customTimeStart: new Date("2020"),
customTimeEnd: new Date(),
});
const options = useMemo(
() => ({
time: timeOptions,
}),
[timeOptions],
);
const { topTracks, isLoading } = useTopTracks(options);
const reportHasItems = topTracks.length !== 0;
const maxCount = getMaxCount(topTracks);
return (
<>
<div className="flex justify-between">
<h2 className="text-2xl font-normal text-gray-700 dark:text-gray-400">
Top Tracks
</h2>
</div>
<div className="shadow-xl bg-gray-100 dark:bg-gray-800 rounded p-5 m-2">
<ReportTimeOptions
timeOptions={timeOptions}
setTimeOptions={setTimeOptions}
/>
{isLoading && <Spinner className="m-8" />}
{!reportHasItems && !isLoading && (
<div>
<p>Report is emtpy! :(</p>
</div>
)}
{reportHasItems &&
topTracks.map(({ track, count }) => (
<ReportItem
key={track.id}
track={track}
count={count}
maxCount={maxCount}
/>
))}
</div>
</>
);
};
const ReportItem: React.FC<{
track: Track;
count: number;
maxCount: number;
}> = ({ track, count, maxCount }) => {
const artists = track.artists?.map((artist) => artist.name).join(", ") || "";
return (
<TopListItem
title={track.name}
subTitle={artists}
count={count}
maxCount={maxCount}
/>
);
};

View file

@ -1,4 +1,5 @@
import React from "react";
import { numberToPercent } from "../../util/numberToPercent";
export interface TopListItemProps {
title: string;
@ -42,9 +43,3 @@ export const TopListItem: React.FC<TopListItemProps> = ({
const isMaxCountValid = (maxCount: number) =>
!(Number.isNaN(maxCount) || maxCount === 0);
const numberToPercent = (ratio: number) =>
ratio.toLocaleString(undefined, {
style: "percent",
minimumFractionDigits: 2,
});

View file

@ -1,5 +1,5 @@
import React from "react";
import { SpinnerIcon } from "../icons/Spinner";
import { SpinnerIcon } from "../../icons/Spinner";
interface SpinnerProps {
className?: string;

View file

@ -0,0 +1,48 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "src/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-gray-100 dark:bg-gray-800",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View file

@ -0,0 +1,36 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "src/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border border-gray-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 dark:border-gray-800 dark:focus:ring-gray-300",
{
variants: {
variant: {
default:
"border-transparent bg-gray-900 text-gray-50 hover:bg-gray-900/80 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/80",
secondary:
"border-transparent bg-gray-100 text-gray-900 hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80",
destructive:
"border-transparent bg-red-500 text-gray-50 hover:bg-red-500/80 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/80",
outline: "text-gray-900 dark:text-gray-50",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

View file

@ -0,0 +1,58 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "src/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-900 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-gray-900 dark:focus-visible:ring-gray-300",
{
variants: {
variant: {
default:
"bg-gray-900 text-gray-50 hover:bg-gray-900/90 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-50/90",
destructive:
"bg-red-500 text-gray-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-gray-50 dark:hover:bg-red-900/90",
outline:
"border border-gray-200 bg-white hover:bg-gray-100 hover:text-gray-900 dark:border-gray-800 dark:bg-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50",
secondary:
"bg-gray-100 text-gray-900 hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80",
ghost:
"hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50",
link: "text-gray-900 underline-offset-4 hover:underline dark:text-gray-50",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };

View file

@ -0,0 +1,86 @@
import * as React from "react";
import { cn } from "src/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border border-gray-200 bg-white text-gray-900 shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-50",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-gray-500 dark:text-gray-400", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View file

@ -0,0 +1,9 @@
import React, { PropsWithChildren } from "react";
export const Code: React.FC<PropsWithChildren> = ({ children }) => {
return (
<code className="tracking-wide font-mono bg-gray-200 dark:bg-gray-600 rounded-md px-1">
{children}
</code>
);
};

View file

@ -0,0 +1,198 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { Check, ChevronRight, Circle } from "lucide-react";
import { cn } from "src/lib/utils";
const DropdownMenu = DropdownMenuPrimitive.Root;
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-gray-100 data-[state=open]:bg-gray-100 dark:focus:bg-gray-800 dark:data-[state=open]:bg-gray-800",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
));
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName;
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 text-gray-900 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-50",
className,
)}
{...props}
/>
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 text-gray-900 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-50",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
));
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
));
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName;
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
));
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-gray-100 dark:bg-gray-800", className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
);
};
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
};

View file

@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "src/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View file

@ -0,0 +1,129 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDown } from "lucide-react";
import { cn } from "src/lib/utils";
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
));
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 flex-col sm:flex-row list-none sm:items-center justify-center space-y-2 sm:space-x-1 sm:space-y-0",
className,
)}
{...props}
/>
));
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
const NavigationMenuItem = NavigationMenuPrimitive.Item;
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-white px-4 py-2 text-sm font-medium transition-colors hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900 focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-gray-100/50 data-[state=open]:bg-gray-100/50 dark:bg-gray-900 dark:hover:bg-gray-800 dark:hover:text-gray-50 dark:focus:bg-gray-800 dark:focus:text-gray-50 dark:data-[active]:bg-gray-800/50 dark:data-[state=open]:bg-gray-800/50",
);
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}
{""}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
));
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
className,
)}
{...props}
/>
));
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
const NavigationMenuLink = NavigationMenuPrimitive.Link;
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border border-gray-200 bg-white text-gray-900 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)] dark:border-gray-800 dark:bg-gray-900 dark:text-gray-50",
className,
)}
ref={ref}
{...props}
/>
</div>
));
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName;
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-gray-200 shadow-md dark:bg-gray-800" />
</NavigationMenuPrimitive.Indicator>
));
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName;
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
};

View file

@ -0,0 +1,119 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown } from "lucide-react";
import { cn } from "src/lib/utils";
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-gray-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-800 dark:bg-gray-900 dark:ring-offset-gray-900 dark:placeholder:text-gray-400 dark:focus:ring-gray-300",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white text-gray-900 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-50",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-gray-100 dark:bg-gray-800", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
};

View file

@ -0,0 +1,117 @@
import * as React from "react";
import { cn } from "src/lib/utils";
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"bg-gray-900 font-medium text-gray-50 dark:bg-gray-50 dark:text-gray-900",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-gray-100/50 data-[state=selected]:bg-gray-100 dark:hover:bg-gray-800/50 dark:data-[state=selected]:bg-gray-800",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-gray-500 [&:has([role=checkbox])]:pr-0 dark:text-gray-400",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
TableCell.displayName = "TableCell";
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-gray-500 dark:text-gray-400", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View file

@ -1,4 +1,8 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import axios, {
AxiosInstance,
InternalAxiosRequestConfig,
AxiosResponse,
} from "axios";
import React, {
createContext,
useContext,
@ -13,7 +17,7 @@ interface ApiClientContext {
}
const apiClientContext = createContext<ApiClientContext>(
undefined as any as ApiClientContext
undefined as any as ApiClientContext,
);
export const ProvideApiClient: React.FC<{ children: React.ReactNode }> = ({
@ -35,13 +39,13 @@ export function useApiClient() {
function useProvideApiClient(): ApiClientContext {
const { accessToken, refreshAccessToken } = useAuth();
// Wrap value to immediatly update when refreshing access token
// Wrap value to immediately update when refreshing access token
// and always having access to newest access token in interceptor
const localAccessToken = useRef(accessToken);
// initialState must be passed as function as return value of axios.create()
// is also callable and react will call it and then use that result (promise)
// as the initial state instead of the axios instace.
// as the initial state instead of the axios instance.
const [client] = useState<AxiosInstance>(() => axios.create());
useEffect(() => {
@ -54,15 +58,11 @@ function useProvideApiClient(): ApiClientContext {
// Setup Axios Interceptors
const requestInterceptor = client.interceptors.request.use(
(config) => {
if (!config.headers) {
config.headers = {};
}
config.headers.Authorization = `Bearer ${localAccessToken.current}`;
return config;
},
(err) => Promise.reject(err)
(err) => Promise.reject(err),
);
const responseInterceptor = client.interceptors.response.use(
(data) => data,
@ -73,7 +73,7 @@ function useProvideApiClient(): ApiClientContext {
const { response, config } = err as {
response: AxiosResponse;
config: AxiosRequestConfig;
config: InternalAxiosRequestConfig;
};
if (response && response.status !== 401) {
@ -84,7 +84,7 @@ function useProvideApiClient(): ApiClientContext {
localAccessToken.current = await refreshAccessToken();
return client.request(config);
}
},
);
return () => {

View file

@ -2,12 +2,14 @@ import { useCallback, useMemo } from "react";
import {
createApiToken,
getApiTokens,
getExtendedStreamingHistoryStatus,
getListensReport,
getRecentListens,
getTopAlbums,
getTopArtists,
getTopGenres,
getTopTracks,
importExtendedStreamingHistory,
revokeApiToken,
} from "../api/api";
import { ListenReportOptions } from "../api/entities/listen-report-options";
@ -18,6 +20,7 @@ import { TopGenresOptions } from "../api/entities/top-genres-options";
import { TopTracksOptions } from "../api/entities/top-tracks-options";
import { useApiClient } from "./use-api-client";
import { useAsync } from "./use-async";
import { SpotifyExtendedStreamingHistoryItem } from "../api/entities/spotify-extended-streaming-history-item";
const INITIAL_EMPTY_ARRAY: [] = [];
Object.freeze(INITIAL_EMPTY_ARRAY);
@ -27,7 +30,7 @@ export const useRecentListens = (options: PaginationOptions) => {
const fetchData = useMemo(
() => () => getRecentListens(options, client),
[options, client]
[options, client],
);
const {
@ -48,7 +51,7 @@ export const useListensReport = (options: ListenReportOptions) => {
const fetchData = useMemo(
() => () => getListensReport(options, client),
[options, client]
[options, client],
);
const {
@ -65,7 +68,7 @@ export const useTopArtists = (options: TopArtistsOptions) => {
const fetchData = useMemo(
() => () => getTopArtists(options, client),
[options, client]
[options, client],
);
const {
@ -82,7 +85,7 @@ export const useTopAlbums = (options: TopAlbumsOptions) => {
const fetchData = useMemo(
() => () => getTopAlbums(options, client),
[options, client]
[options, client],
);
const {
@ -99,7 +102,7 @@ export const useTopTracks = (options: TopTracksOptions) => {
const fetchData = useMemo(
() => () => getTopTracks(options, client),
[options, client]
[options, client],
);
const {
@ -116,7 +119,7 @@ export const useTopGenres = (options: TopGenresOptions) => {
const fetchData = useMemo(
() => () => getTopGenres(options, client),
[options, client]
[options, client],
);
const {
@ -143,13 +146,11 @@ export const useApiTokens = () => {
const createToken = useCallback(
async (description: string) => {
const apiToken = await createApiToken(description, client);
console.log("apiToken created", apiToken);
await reload();
console.log("reloaded data");
return apiToken;
},
[client, reload]
[client, reload],
);
const revokeToken = useCallback(
@ -157,8 +158,43 @@ export const useApiTokens = () => {
await revokeApiToken(id, client);
await reload();
},
[client, reload]
[client, reload],
);
return { apiTokens, isLoading, error, createToken, revokeToken };
};
export const useSpotifyImportExtendedStreamingHistory = () => {
const { client } = useApiClient();
const importHistory = useCallback(
async (listens: SpotifyExtendedStreamingHistoryItem[]) => {
return importExtendedStreamingHistory(listens, client);
},
[client]
);
const getStatus = useCallback(async () => {
return getExtendedStreamingHistoryStatus(client);
}, [client]);
return { importHistory, getStatus };
};
export const useSpotifyImportExtendedStreamingHistoryStatus = () => {
const { client } = useApiClient();
const fetchData = useMemo(
() => () => getExtendedStreamingHistoryStatus(client),
[client]
);
const {
value: importStatus,
pending: isLoading,
error,
reload,
} = useAsync(fetchData, { total: 0, imported: 0 });
return { importStatus, isLoading, error, reload };
};

View file

@ -2,7 +2,7 @@ import { useCallback, useEffect, useState, useTransition } from "react";
type UseAsync = <T>(
asyncFunction: () => Promise<T>,
initialValue: T
initialValue: T,
) => {
pending: boolean;
value: T;
@ -12,7 +12,7 @@ type UseAsync = <T>(
export const useAsync: UseAsync = <T extends any>(
asyncFunction: () => Promise<T>,
initialValue: T
initialValue: T,
) => {
const [pending, setPending] = useState(false);
const [value, setValue] = useState<T>(initialValue);

View file

@ -1,16 +0,0 @@
import { useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "./use-auth";
export function useAuthProtection() {
const { user } = useAuth();
const navigate = useNavigate();
const requireUser = useCallback(async () => {
if (!user) {
navigate("/");
}
}, [user, navigate]);
return { requireUser };
}

View file

@ -1,26 +0,0 @@
import React, { useEffect } from "react";
/**
* Hook that alerts clicks outside of the passed ref
*/
export const useOutsideClick = (
ref: React.MutableRefObject<any>,
callback: () => void
) => {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
};
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref, callback]);
};

View file

@ -1,7 +1,7 @@
import React from "react";
export const CogwheelIcon: React.FC<React.SVGProps<SVGSVGElement>> = (
props
props,
) => {
return (
<svg

View file

@ -0,0 +1,14 @@
import React from "react";
export const ErrorIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 50 50"
fill="#D75A4A"
>
<circle fill="fill" cx="25" cy="25" r="25" />
</svg>
);
};

View file

@ -0,0 +1,12 @@
import * as React from "react";
export const ImportIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
viewBox="0 0 60.903 60.903"
{...props}
>
<path d="M49.561 16.464H39.45v6h10.111c3.008 0 5.341 1.535 5.341 2.857v26.607c0 1.321-2.333 2.858-5.341 2.858H11.34c-3.007 0-5.34-1.537-5.34-2.858V25.324c0-1.322 2.333-2.858 5.34-2.858h10.11v-6H11.34C4.981 16.466 0 20.357 0 25.324v26.605c0 4.968 4.981 8.857 11.34 8.857h38.223c6.357 0 11.34-3.891 11.34-8.857V25.324c-.001-4.969-4.982-8.86-11.342-8.86z" />
<path d="M39.529 29.004a2.99 2.99 0 0 0-2.121.88l-3.756 3.755V3.117a3 3 0 0 0-6 0v30.724l-3.959-3.958a2.992 2.992 0 0 0-4.242 0 2.997 2.997 0 0 0 0 4.241l8.957 8.957a2.988 2.988 0 0 0 2.12.877h.045c.768 0 1.534-.291 2.12-.877l8.957-8.957a2.997 2.997 0 0 0-2.121-5.12z" />
</svg>
);

View file

@ -1,7 +1,7 @@
import React from "react";
export const TrashcanIcon: React.FC<React.SVGProps<SVGSVGElement>> = (
props
props,
) => {
return (
<svg

View file

@ -5,6 +5,7 @@ import { App } from "./App";
import { ProvideApiClient } from "./hooks/use-api-client";
import { ProvideAuth } from "./hooks/use-auth";
import "./index.css";
import { ThemeProvider } from "./components/ThemeProvider";
const root = createRoot(document.getElementById("root")!);
@ -13,9 +14,11 @@ root.render(
<ProvideAuth>
<ProvideApiClient>
<BrowserRouter>
<App />
<ThemeProvider>
<App />
</ThemeProvider>
</BrowserRouter>
</ProvideApiClient>
</ProvideAuth>
</React.StrictMode>
</React.StrictMode>,
);

View file

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

36
frontend/src/react-files.d.ts vendored Normal file
View file

@ -0,0 +1,36 @@
//// <reference types="react" />
declare module "react-files" {
declare const Files: React.FC<
Partial<{
accepts: string[];
children: React.ReactNode;
className: string;
clickable: boolean;
dragActiveClassName: string;
inputProps: unknown;
multiple: boolean;
maxFiles: number;
maxFileSize: number;
minFileSize: number;
name: string;
onChange: (files: ReactFile[]) => void;
onDragEnter: () => void;
onDragLeave: () => void;
onError: (
error: { code: number; message: string },
file: ReactFile
) => void;
style: object;
}>
>;
export type ReactFile = File & {
id: string;
extension: string;
sizeReadable: string;
preview: { type: "image"; url: string } | { type: "file" };
};
export default Files;
}

View file

@ -1,7 +1,7 @@
export const getPaginationItems = (
currentPage: number,
totalPages: number,
delta: number = 1
delta: number = 1,
): (number | null)[] => {
const left = currentPage - delta;
const right = currentPage + delta;

View file

@ -0,0 +1,5 @@
export const numberToPercent = (ratio: number) =>
ratio.toLocaleString(undefined, {
style: "percent",
minimumFractionDigits: 2,
});

View file

@ -5,7 +5,7 @@ export const qs = (parameters: QueryParameters): string => {
const queryParams = new URLSearchParams();
Object.entries(parameters).forEach(([key, value]) =>
queryParams.append(key, value)
queryParams.append(key, value),
);
return queryParams.toString();

View file

@ -1,6 +1,8 @@
const colors = require("tailwindcss/colors");
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
theme: {
colors: {
@ -12,6 +14,7 @@ module.exports = {
// Tailwind v1 Colors
gray: {
50: "#ffffff",
100: "#f7fafc",
200: "#edf2f7",
300: "#e2e8f0",
@ -21,9 +24,11 @@ module.exports = {
700: "#4a5568",
800: "#2d3748",
900: "#1a202c",
950: "#0C0F12",
},
green: {
50: "#FFFFFF",
100: "#f0fff4",
200: "#c6f6d5",
300: "#9ae6b4",
@ -33,6 +38,7 @@ module.exports = {
700: "#2f855a",
800: "#276749",
900: "#22543d",
950: "#1C4A2F",
},
yellow: colors.yellow,
@ -40,5 +46,29 @@ module.exports = {
violet: colors.violet,
amber: colors.amber,
},
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
};

View file

@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@ -21,5 +17,8 @@
},
"include": [
"src"
]
],
"paths": {
"src/*": ["./src/*"]
}
}

View file

@ -1,3 +1,4 @@
import path from "path";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
@ -7,6 +8,11 @@ export default defineConfig(() => {
outDir: "build",
},
plugins: [react()],
resolve: {
alias: {
src: path.resolve(__dirname, "./src"),
},
},
test: {
globals: true,
environment: "jsdom",

17930
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "@listory/api",
"version": "1.26.2",
"version": "1.31.0",
"description": "Track your Spotify Listen History",
"author": {
"name": "Julian Tölle",
@ -26,94 +26,93 @@
"test:e2e": "jest --config ./apps/listory/test/jest-e2e.json"
},
"dependencies": {
"@apricote/nest-pg-boss": "2.0.0",
"@narando/nest-axios-interceptor": "2.2.0",
"@nestjs/axios": "0.1.0",
"@nestjs/common": "9.4.3",
"@nestjs/config": "2.3.4",
"@nestjs/core": "9.4.3",
"@nestjs/jwt": "10.1.0",
"@nestjs/passport": "9.0.3",
"@nestjs/platform-express": "9.4.3",
"@nestjs/serve-static": "3.0.1",
"@nestjs/swagger": "6.3.0",
"@nestjs/terminus": "9.2.2",
"@nestjs/typeorm": "9.0.1",
"@opentelemetry/api": "1.4.0",
"@apricote/nest-pg-boss": "2.1.0",
"@narando/nest-axios-interceptor": "3.0.0",
"@nestjs/axios": "3.1.3",
"@nestjs/common": "10.4.15",
"@nestjs/config": "3.3.0",
"@nestjs/core": "10.4.15",
"@nestjs/jwt": "10.2.0",
"@nestjs/passport": "10.0.3",
"@nestjs/platform-express": "10.4.15",
"@nestjs/serve-static": "4.0.2",
"@nestjs/swagger": "7.4.2",
"@nestjs/terminus": "10.2.3",
"@nestjs/typeorm": "10.0.2",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/api-metrics": "0.33.0",
"@opentelemetry/context-async-hooks": "1.9.0",
"@opentelemetry/exporter-prometheus": "0.35.0",
"@opentelemetry/exporter-trace-otlp-http": "0.35.0",
"@opentelemetry/instrumentation": "0.35.0",
"@opentelemetry/instrumentation-dns": "0.31.5",
"@opentelemetry/instrumentation-express": "0.32.4",
"@opentelemetry/instrumentation-http": "0.35.0",
"@opentelemetry/instrumentation-nestjs-core": "0.32.5",
"@opentelemetry/instrumentation-pg": "0.35.3",
"@opentelemetry/instrumentation-pino": "0.33.4",
"@opentelemetry/resources": "1.9.0",
"@opentelemetry/sdk-metrics": "1.9.0",
"@opentelemetry/sdk-node": "0.35.0",
"@opentelemetry/sdk-trace-base": "1.9.0",
"@opentelemetry/semantic-conventions": "1.9.0",
"@sentry/node": "7.57.0",
"@opentelemetry/context-async-hooks": "1.29.0",
"@opentelemetry/exporter-prometheus": "0.56.0",
"@opentelemetry/exporter-trace-otlp-http": "0.56.0",
"@opentelemetry/instrumentation": "0.56.0",
"@opentelemetry/instrumentation-dns": "0.42.0",
"@opentelemetry/instrumentation-express": "0.46.0",
"@opentelemetry/instrumentation-http": "0.56.0",
"@opentelemetry/instrumentation-nestjs-core": "0.43.0",
"@opentelemetry/instrumentation-pg": "0.49.0",
"@opentelemetry/instrumentation-pino": "0.45.0",
"@opentelemetry/resources": "1.29.0",
"@opentelemetry/sdk-metrics": "1.29.0",
"@opentelemetry/sdk-node": "0.56.0",
"@opentelemetry/sdk-trace-base": "1.29.0",
"@opentelemetry/semantic-conventions": "1.28.0",
"@sentry/node": "7.120.3",
"class-transformer": "0.5.1",
"class-validator": "0.14.0",
"cookie-parser": "1.4.6",
"class-validator": "0.14.1",
"cookie-parser": "1.4.7",
"date-fns": "2.30.0",
"joi": "17.9.2",
"lodash": "^4.17.21",
"nest-raven": "9.2.0",
"nestjs-otel": "5.1.4",
"nestjs-pino": "3.3.0",
"joi": "17.13.3",
"lodash": "4.17.21",
"nest-raven": "10.1.0",
"nestjs-otel": "5.1.5",
"nestjs-pino": "4.1.0",
"nestjs-typeorm-paginate": "4.0.4",
"passport": "0.6.0",
"passport-http-bearer": "^1.0.1",
"passport": "0.7.0",
"passport-http-bearer": "1.0.1",
"passport-jwt": "4.0.1",
"passport-spotify": "2.0.0",
"pg": "8.11.1",
"pg-boss": "^9.0.0",
"pino": "8.14.1",
"pino-http": "8.3.3",
"reflect-metadata": "0.1.13",
"rimraf": "5.0.1",
"pg": "8.13.1",
"pg-boss": "9.0.3",
"pino": "8.21.0",
"pino-http": "9.0.0",
"reflect-metadata": "0.1.14",
"rimraf": "5.0.10",
"rxjs": "7.8.1",
"typeorm": "0.3.17"
"typeorm": "0.3.20"
},
"devDependencies": {
"@nestjs/cli": "9.5.0",
"@nestjs/schematics": "9.2.0",
"@nestjs/testing": "9.4.3",
"@types/cookie-parser": "1.4.3",
"@types/express": "4.17.17",
"@types/jest": "29.5.2",
"@types/lodash": "^4.14.194",
"@types/long": "4.0.2",
"@types/node": "18.16.19",
"@types/passport-http-bearer": "^1.0.37",
"@types/passport-jwt": "3.0.8",
"@types/supertest": "2.0.12",
"@typescript-eslint/eslint-plugin": "5.60.1",
"@typescript-eslint/parser": "5.60.1",
"eslint": "8.44.0",
"@nestjs/cli": "10.4.9",
"@nestjs/schematics": "10.2.3",
"@nestjs/testing": "10.4.15",
"@types/cookie-parser": "1.4.8",
"@types/express": "5.0.0",
"@types/jest": "29.5.14",
"@types/lodash": "4.17.14",
"@types/node": "20.17.16",
"@types/passport-http-bearer": "1.0.41",
"@types/passport-jwt": "4.0.1",
"@types/supertest": "6.0.2",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"eslint": "8.57.1",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.0.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsdoc": "46.4.3",
"eslint-plugin-jsx-a11y": "6.7.1",
"eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jsdoc": "48.11.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-prefer-arrow": "1.2.3",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0",
"jest": "29.5.0",
"pino-pretty": "10.0.0",
"prettier": "2.8.8",
"supertest": "6.3.3",
"ts-jest": "29.1.1",
"ts-loader": "9.4.4",
"ts-node": "10.9.1",
"eslint-plugin-react": "7.37.4",
"eslint-plugin-react-hooks": "4.6.2",
"jest": "29.7.0",
"pino-pretty": "10.3.1",
"prettier": "3.4.2",
"supertest": "6.3.4",
"ts-jest": "29.2.5",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"typescript": "5.1.6"
"typescript": "5.7.2"
},
"jest": {
"moduleFileExtensions": [

View file

@ -7,7 +7,8 @@
":automergeBranch",
":automergeLinters",
":automergeTesters",
":automergeTypes"
":automergeTypes",
":maintainLockFilesWeekly"
],
"packageRules": [
{

View file

@ -1,5 +1,5 @@
import { Test, TestingModule } from "@nestjs/testing";
import type { Response } from "express";
import type { Response as ExpressResponse } from "express";
import { User } from "../users/user.entity";
import { AuthSession } from "./auth-session.entity";
import { AuthController } from "./auth.controller";
@ -27,7 +27,7 @@ describe("AuthController", () => {
describe("spotifyCallback", () => {
let user: User;
let res: Response;
let res: ExpressResponse;
let refreshToken: string;
beforeEach(() => {
@ -36,7 +36,7 @@ describe("AuthController", () => {
statusCode: 200,
cookie: jest.fn(),
redirect: jest.fn(),
} as unknown as Response;
} as unknown as ExpressResponse;
refreshToken = "REFRESH_TOKEN";
authService.createSession = jest.fn().mockResolvedValue({ refreshToken });
@ -56,7 +56,7 @@ describe("AuthController", () => {
expect(res.cookie).toHaveBeenCalledWith(
COOKIE_REFRESH_TOKEN,
refreshToken,
{ httpOnly: true }
{ httpOnly: true },
);
});
@ -65,7 +65,7 @@ describe("AuthController", () => {
expect(res.redirect).toHaveBeenCalledTimes(1);
expect(res.redirect).toHaveBeenCalledWith(
"/login/success?source=spotify"
"/login/success?source=spotify",
);
});
});

View file

@ -1,5 +1,5 @@
import {
Body,
Body as NestBody,
Controller,
Delete,
Get,
@ -10,7 +10,7 @@ import {
UseGuards,
} from "@nestjs/common";
import { ApiBody, ApiTags } from "@nestjs/swagger";
import type { Response } from "express";
import type { Response as ExpressResponse } from "express";
import { User } from "../users/user.entity";
import { AuthSession } from "./auth-session.entity";
import { AuthService } from "./auth.service";
@ -42,7 +42,7 @@ export class AuthController {
@Get("spotify/callback")
@UseFilters(SpotifyAuthFilter)
@UseGuards(SpotifyAuthGuard)
async spotifyCallback(@ReqUser() user: User, @Res() res: Response) {
async spotifyCallback(@ReqUser() user: User, @Res() res: ExpressResponse) {
const { refreshToken } = await this.authService.createSession(user);
// Refresh token should not be accessible to frontend to reduce risk
@ -57,7 +57,7 @@ export class AuthController {
@UseGuards(RefreshTokenAuthGuard)
async refreshAccessToken(
// With RefreshTokenAuthGuard the session is available instead of user
@ReqUser() session: AuthSession
@ReqUser() session: AuthSession,
): Promise<RefreshAccessTokenResponseDto> {
const { accessToken } = await this.authService.createAccessToken(session);
@ -69,7 +69,7 @@ export class AuthController {
@AuthAccessToken()
async createApiToken(
@ReqUser() user: User,
@Body("description") description: string
@NestBody("description") description: string,
): Promise<NewApiTokenDto> {
const apiToken = await this.authService.createApiToken(user, description);
@ -100,7 +100,7 @@ export class AuthController {
@AuthAccessToken()
async revokeApiToken(
@ReqUser() user: User,
@Param("id") id: string
@Param("id") id: string,
): Promise<void> {
return this.authService.revokeApiToken(user, id);
}

View file

@ -38,7 +38,7 @@ describe("AuthService", () => {
usersService = module.get<UsersService>(UsersService);
jwtService = module.get<JwtService>(JwtService);
authSessionRepository = module.get<AuthSessionRepository>(
AuthSessionRepository
AuthSessionRepository,
);
apiTokenRepository = module.get<ApiTokenRepository>(ApiTokenRepository);
});
@ -84,7 +84,7 @@ describe("AuthService", () => {
expect(service.allowedByUserFilter).toHaveBeenCalledTimes(1);
expect(service.allowedByUserFilter).toHaveBeenCalledWith(
loginDto.profile.id
loginDto.profile.id,
);
});
@ -92,7 +92,7 @@ describe("AuthService", () => {
service.allowedByUserFilter = jest.fn().mockReturnValue(false);
await expect(service.spotifyLogin(loginDto)).rejects.toThrow(
ForbiddenException
ForbiddenException,
);
});
@ -197,7 +197,7 @@ describe("AuthService", () => {
{
jwtid: session.id,
expiresIn: "EXPIRATION_TIME",
}
},
);
});
});
@ -231,7 +231,7 @@ describe("AuthService", () => {
session.revokedAt = new Date("2020-01-01T00:00:00Z");
await expect(service.createAccessToken(session)).rejects.toThrow(
ForbiddenException
ForbiddenException,
);
});
@ -258,7 +258,7 @@ describe("AuthService", () => {
it("returns the session", async () => {
await expect(service.findSession("AUTH_SESSION")).resolves.toEqual(
session
session,
);
expect(authSessionRepository.findOneBy).toHaveBeenCalledTimes(1);

View file

@ -20,11 +20,11 @@ export class AuthService {
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
private readonly authSessionRepository: AuthSessionRepository,
private readonly apiTokenRepository: ApiTokenRepository
private readonly apiTokenRepository: ApiTokenRepository,
) {
this.userFilter = this.config.get<string>("SPOTIFY_USER_FILTER");
this.sessionExpirationTime = this.config.get<string>(
"SESSION_EXPIRATION_TIME"
"SESSION_EXPIRATION_TIME",
);
}
@ -69,7 +69,7 @@ export class AuthService {
* @param session
*/
private async createRefreshToken(
session: AuthSession
session: AuthSession,
): Promise<{ refreshToken: string }> {
const payload = {
sub: session.user.id,
@ -86,7 +86,7 @@ export class AuthService {
}
async createAccessToken(
session: AuthSession
session: AuthSession,
): Promise<{ accessToken: string }> {
if (session.revokedAt) {
throw new ForbiddenException("SessionIsRevoked");
@ -115,7 +115,7 @@ export class AuthService {
// TODO demagic 20
const tokenBuffer = await new Promise<Buffer>((resolve, reject) =>
randomBytes(20, (err, buf) => (err ? reject(err) : resolve(buf)))
randomBytes(20, (err, buf) => (err ? reject(err) : resolve(buf))),
);
apiToken.token = `lis${tokenBuffer.toString("hex")}`;

View file

@ -6,6 +6,6 @@ export function AuthAccessToken() {
return applyDecorators(
UseGuards(ApiAuthGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: "Unauthorized" })
ApiUnauthorizedResponse({ description: "Unauthorized" }),
);
}

View file

@ -4,5 +4,5 @@ export const ReqUser = createParamDecorator<void>(
(_: void, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
}
},
);

View file

@ -5,14 +5,14 @@ import {
ForbiddenException,
Logger,
} from "@nestjs/common";
import type { Response } from "express";
import type { Response as ExpressResponse } from "express";
@Catch()
export class SpotifyAuthFilter implements ExceptionFilter {
private readonly logger = new Logger(this.constructor.name);
catch(exception: Error, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse<Response>();
const response = host.switchToHttp().getResponse<ExpressResponse>();
let reason = "unknown";
@ -29,7 +29,7 @@ export class SpotifyAuthFilter implements ExceptionFilter {
this.logger.error(
`Login with Spotify failed: ${exception}`,
exception.stack
exception.stack,
);
response.redirect(`/login/failure?reason=${reason}&source=spotify`);

View file

@ -8,11 +8,11 @@ import { AuthStrategy } from "./strategies.enum";
@Injectable()
export class AccessTokenStrategy extends PassportStrategy(
Strategy,
AuthStrategy.AccessToken
AuthStrategy.AccessToken,
) {
constructor(
private readonly authService: AuthService,
config: ConfigService
config: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),

View file

@ -12,7 +12,7 @@ import { AuthStrategy } from "./strategies.enum";
@Injectable()
export class ApiTokenStrategy extends PassportStrategy(
Strategy,
AuthStrategy.ApiToken
AuthStrategy.ApiToken,
) {
constructor(private readonly authService: AuthService) {
super();

View file

@ -54,7 +54,7 @@ describe("RefreshTokenStrategy", () => {
authService.findSession = jest.fn().mockResolvedValue(undefined);
await expect(strategy.validate(payload)).rejects.toThrow(
UnauthorizedException
UnauthorizedException,
);
});
@ -62,7 +62,7 @@ describe("RefreshTokenStrategy", () => {
session.revokedAt = "2021-01-01";
await expect(strategy.validate(payload)).rejects.toThrow(
ForbiddenException
ForbiddenException,
);
});
});

View file

@ -19,11 +19,11 @@ const extractJwtFromCookie: JwtFromRequestFunction = (req) => {
@Injectable()
export class RefreshTokenStrategy extends PassportStrategy(
Strategy,
AuthStrategy.RefreshToken
AuthStrategy.RefreshToken,
) {
constructor(
private readonly authService: AuthService,
config: ConfigService
config: ConfigService,
) {
super({
jwtFromRequest: extractJwtFromCookie,

View file

@ -8,17 +8,17 @@ import { AuthStrategy } from "./strategies.enum";
@Injectable()
export class SpotifyStrategy extends PassportStrategy(
Strategy,
AuthStrategy.Spotify
AuthStrategy.Spotify,
) {
constructor(
private readonly authService: AuthService,
config: ConfigService
config: ConfigService,
) {
super({
clientID: config.get<string>("SPOTIFY_CLIENT_ID"),
clientSecret: config.get<string>("SPOTIFY_CLIENT_SECRET"),
callbackURL: `${config.get<string>(
"APP_URL"
"APP_URL",
)}/api/v1/auth/spotify/callback`,
scope: [
"user-read-private",

View file

@ -27,7 +27,7 @@ import * as Joi from "joi";
SPOTIFY_UPDATE_INTERVAL_SEC: Joi.number().default(60),
SPOTIFY_WEB_API_URL: Joi.string().default("https://api.spotify.com/"),
SPOTIFY_AUTH_API_URL: Joi.string().default(
"https://accounts.spotify.com/"
"https://accounts.spotify.com/",
),
SPOTIFY_USER_FILTER: Joi.string(),
@ -53,14 +53,14 @@ import * as Joi from "joi";
{
is: Joi.valid(true),
then: Joi.required(),
}
},
),
PROMETHEUS_BASIC_AUTH_PASSWORD: Joi.string().when(
"PROMETHEUS_BASIC_AUTH",
{
is: Joi.valid(true),
then: Joi.required(),
}
},
),
}),
}),

View file

@ -27,7 +27,7 @@ export const DatabaseModule = TypeOrmModule.forRootAsync({
// Debug/Development Options
//
// logging: true,
//logging: true,
//
// synchronize: true,
// migrationsRun: false,

View file

@ -5,7 +5,7 @@ import { TYPEORM_ENTITY_REPOSITORY } from "./entity-repository.decorator";
export class TypeOrmRepositoryModule {
public static for<T extends new (...args: any[]) => any>(
repositories: T[]
repositories: T[],
): DynamicModule {
const providers: Provider[] = [];
@ -24,7 +24,7 @@ export class TypeOrmRepositoryModule {
return new repository(
baseRepository.target,
baseRepository.manager,
baseRepository.queryRunner
baseRepository.queryRunner,
);
},
});

View file

@ -39,7 +39,7 @@ export class CreateUsersTable0000000000001 implements MigrationInterface {
}),
],
}),
true
true,
);
}

View file

@ -43,7 +43,7 @@ export class CreateLibraryTables0000000000002 implements MigrationInterface {
}),
],
}),
true
true,
);
await queryRunner.createTable(
@ -64,7 +64,7 @@ export class CreateLibraryTables0000000000002 implements MigrationInterface {
isUnique: true,
}),
],
})
}),
);
await queryRunner.createTable(
@ -94,7 +94,7 @@ export class CreateLibraryTables0000000000002 implements MigrationInterface {
referencedTableName: "album",
}),
],
})
}),
);
await queryRunner.createTable(
@ -137,7 +137,7 @@ export class CreateLibraryTables0000000000002 implements MigrationInterface {
}),
],
}),
true
true,
);
await queryRunner.createTable(
@ -180,7 +180,7 @@ export class CreateLibraryTables0000000000002 implements MigrationInterface {
}),
],
}),
true
true,
);
}

View file

@ -65,7 +65,7 @@ export class CreateListensTable0000000000003 implements MigrationInterface {
}),
],
}),
true
true,
);
}

View file

@ -60,7 +60,7 @@ export class CreateAuthSessionsTable0000000000004
}),
],
}),
true
true,
);
}

View file

@ -34,7 +34,7 @@ export class CreateGenreTables0000000000005 implements MigrationInterface {
}),
],
}),
true
true,
);
await queryRunner.createTable(
@ -77,7 +77,7 @@ export class CreateGenreTables0000000000005 implements MigrationInterface {
}),
],
}),
true
true,
);
}

View file

@ -8,7 +8,7 @@ export class AddUpdatedAtColumnes0000000000006 implements MigrationInterface {
name: "updatedAt",
type: "timestamp",
default: "NOW()",
})
}),
);
}

View file

@ -67,7 +67,7 @@ export class CreateApiTokensTable0000000000007 implements MigrationInterface {
}),
],
}),
true
true,
);
}

View file

@ -0,0 +1,72 @@
import { MigrationInterface, QueryRunner, TableIndex } from "typeorm";
export class OptimizeDBIndices0000000000008 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createIndices("artist", [
new TableIndex({
// This index helps with the "update artist" job
name: "IDX_ARTIST_UPDATED_AT",
columnNames: ["updatedAt"],
}),
]);
await queryRunner.createIndices("listen", [
new TableIndex({
// This index helps with the "getCrawlableUserInfo" query
name: "IDX_LISTEN_USER_ID_PLAYED_AT",
columnNames: ["userId", "playedAt"],
}),
]);
// handled by Primary Key on (albumId, artistId)
await queryRunner.dropIndex("album_artists", "IDX_ALBUM_ARTISTS_ALBUM_ID");
// handled by Primary Key on (artistId, genreId)
await queryRunner.dropIndex("artist_genres", "IDX_ARTIST_GENRES_ARTIST_ID");
// handled by IDX_LISTEN_UNIQUE on (trackId, userId, playedAt)
await queryRunner.dropIndex("listen", "IDX_LISTEN_TRACK_ID");
// handled by IDX_LISTEN_USER_ID_PLAYED_AT on (userId, playedAt)
await queryRunner.dropIndex("listen", "IDX_LISTEN_USER_ID");
// handled by Primary Key on (trackId, artistId)
await queryRunner.dropIndex("track_artists", "IDX_TRACK_ARTISTS_TRACK_ID");
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createIndices("album_artists", [
new TableIndex({
name: "IDX_ALBUM_ARTISTS_ALBUM_ID",
columnNames: ["albumId"],
}),
]);
await queryRunner.createIndices("artist_genres", [
new TableIndex({
name: "IDX_ARTIST_GENRES_ARTIST_ID",
columnNames: ["artistId"],
}),
]);
await queryRunner.createIndices("listen", [
new TableIndex({
name: "IDX_LISTEN_TRACK_ID",
columnNames: ["trackId"],
}),
new TableIndex({
name: "IDX_LISTEN_USER_ID",
columnNames: ["userId"],
}),
]);
await queryRunner.createIndices("track_artists", [
new TableIndex({
name: "IDX_TRACK_ARTISTS_TRACK_ID",
columnNames: ["trackId"],
}),
]);
await queryRunner.dropIndex("artist", "IDX_ARTIST_UPDATED_AT");
await queryRunner.dropIndex("listen", "IDX_LISTEN_USER_ID_PLAYED_AT");
}
}

View file

@ -0,0 +1,68 @@
import {
MigrationInterface,
QueryRunner,
Table,
TableIndex,
TableForeignKey,
} from "typeorm";
import { TableColumnOptions } from "typeorm/schema-builder/options/TableColumnOptions";
const primaryUUIDColumn: TableColumnOptions = {
name: "id",
type: "uuid",
isPrimary: true,
isGenerated: true,
generationStrategy: "uuid",
};
export class CreateSpotifyImportTables0000000000009
implements MigrationInterface
{
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: "spotify_extended_streaming_history_listen",
columns: [
primaryUUIDColumn,
{ name: "userId", type: "uuid" },
{ name: "playedAt", type: "timestamp" },
{ name: "spotifyTrackUri", type: "varchar" },
{ name: "trackId", type: "uuid", isNullable: true },
{ name: "listenId", type: "uuid", isNullable: true },
],
indices: [
new TableIndex({
name: "IDX_SPOTIFY_EXTENDED_STREAMING_HISTORY_LISTEN_USER_PLAYED_AT",
columnNames: ["userId", "playedAt", "spotifyTrackUri"],
isUnique: true,
}),
],
foreignKeys: [
new TableForeignKey({
name: "FK_SPOTIFY_EXTENDED_STREAMING_HISTORY_LISTEN_USER_ID",
columnNames: ["userId"],
referencedColumnNames: ["id"],
referencedTableName: "user",
}),
new TableForeignKey({
name: "FK_SPOTIFY_EXTENDED_STREAMING_HISTORY_LISTEN_TRACK_ID",
columnNames: ["trackId"],
referencedColumnNames: ["id"],
referencedTableName: "track",
}),
new TableForeignKey({
name: "FK_SPOTIFY_EXTENDED_STREAMING_HISTORY_LISTEN_LISTEN_ID",
columnNames: ["listenId"],
referencedColumnNames: ["id"],
referencedTableName: "listen",
}),
],
}),
true,
);
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable("spotify_extended_streaming_history_listen");
}
}

View file

@ -17,7 +17,7 @@ export class HealthCheckController {
private readonly health: HealthCheckService,
private readonly http: HttpHealthIndicator,
private readonly typeorm: TypeOrmHealthIndicator,
private readonly config: ConfigService
private readonly config: ConfigService,
) {}
@Get()
@ -30,7 +30,7 @@ export class HealthCheckController {
this.config.get<string>("SPOTIFY_WEB_API_URL"),
{
validateStatus: () => true,
} // Successful as long as we get a valid HTTP response back }
}, // Successful as long as we get a valid HTTP response back }
),
() => this.typeorm.pingCheck("db"),
]);

View file

@ -3,10 +3,6 @@ import { Repository, SelectQueryBuilder } from "typeorm";
import { EntityRepository } from "../database/entity-repository";
import { Interval } from "../reports/interval";
import { User } from "../users/user.entity";
import {
CreateListenRequestDto,
CreateListenResponseDto,
} from "./dto/create-listen.dto";
import { Listen } from "./listen.entity";
export class ListenScopes extends SelectQueryBuilder<Listen> {
@ -37,52 +33,4 @@ export class ListenRepository extends Repository<Listen> {
get scoped(): ListenScopes {
return new ListenScopes(this.createQueryBuilder("listen"));
}
async insertNoConflict({
user,
track,
playedAt,
}: CreateListenRequestDto): Promise<CreateListenResponseDto> {
const result = await this.createQueryBuilder()
.insert()
.values({
user,
track,
playedAt,
})
.onConflict('("playedAt", "trackId", "userId") DO NOTHING')
.execute();
const [insertedRowIdentifier] = result.identifiers;
if (!insertedRowIdentifier) {
// We did not insert a new listen, it already existed
return {
listen: await this.findOneBy({ user, track, playedAt }),
isDuplicate: true,
};
}
return {
listen: await this.findOneBy({ id: insertedRowIdentifier.id }),
isDuplicate: false,
};
}
/**
*
* @param rows
* @returns A list of all new (non-duplicate) listens
*/
async insertsNoConflict(rows: CreateListenRequestDto[]): Promise<Listen[]> {
const result = await this.createQueryBuilder()
.insert()
.values(rows)
.orIgnore()
.execute();
return this.findBy(
result.identifiers.filter(Boolean).map(({ id }) => ({ id }))
);
}
}

View file

@ -41,7 +41,7 @@ describe("Listens Controller", () => {
it("returns the listens", async () => {
await expect(
controller.getRecentlyPlayed(filter, user, 1, 10)
controller.getRecentlyPlayed(filter, user, 1, 10),
).resolves.toEqual(listens);
expect(listensService.getListens).toHaveBeenCalledTimes(1);
@ -57,7 +57,7 @@ describe("Listens Controller", () => {
await controller.getRecentlyPlayed(filter, user, 1, 1000);
expect(listensService.getListens).toHaveBeenCalledWith(
expect.objectContaining({ limit: 100 })
expect.objectContaining({ limit: 100 }),
);
});
});

View file

@ -19,7 +19,7 @@ export class ListensController {
@Query("filter") filter: GetListensFilterDto,
@ReqUser() user: User,
@Query("page") page: number = 1,
@Query("limit") limit: number = 10
@Query("limit") limit: number = 10,
): Promise<Pagination<Listen>> {
const clampedLimit = limit > 100 ? 100 : limit;

View file

@ -4,9 +4,7 @@ import {
paginate,
PaginationTypeEnum,
} from "nestjs-typeorm-paginate";
import { Track } from "../music-library/track.entity";
import { User } from "../users/user.entity";
import { CreateListenResponseDto } from "./dto/create-listen.dto";
import { GetListensDto } from "./dto/get-listens.dto";
import { Listen } from "./listen.entity";
import { ListenRepository, ListenScopes } from "./listen.repository";
@ -35,39 +33,6 @@ describe("ListensService", () => {
expect(listenRepository).toBeDefined();
});
describe("createListen", () => {
let user: User;
let track: Track;
let playedAt: Date;
let response: CreateListenResponseDto;
beforeEach(() => {
user = { id: "USER" } as User;
track = { id: "TRACK" } as Track;
playedAt = new Date("2021-01-01T00:00:00Z");
response = {
listen: {
id: "LISTEN",
} as Listen,
isDuplicate: true,
};
listenRepository.insertNoConflict = jest.fn().mockResolvedValue(response);
});
it("creates the listen", async () => {
await expect(
service.createListen({ user, track, playedAt })
).resolves.toEqual(response);
expect(listenRepository.insertNoConflict).toHaveBeenCalledTimes(1);
expect(listenRepository.insertNoConflict).toHaveBeenLastCalledWith({
user,
track,
playedAt,
});
});
});
describe("getListens", () => {
let options: GetListensDto & IPaginationOptions;
let user: User;

View file

@ -1,14 +1,12 @@
import { Injectable } from "@nestjs/common";
import { Span } from "nestjs-otel";
import {
IPaginationOptions,
paginate,
Pagination,
PaginationTypeEnum,
} from "nestjs-typeorm-paginate";
import {
CreateListenRequestDto,
CreateListenResponseDto,
} from "./dto/create-listen.dto";
import { CreateListenRequestDto } from "./dto/create-listen.dto";
import { GetListensDto } from "./dto/get-listens.dto";
import { Listen } from "./listen.entity";
import { ListenRepository, ListenScopes } from "./listen.repository";
@ -17,22 +15,9 @@ import { ListenRepository, ListenScopes } from "./listen.repository";
export class ListensService {
constructor(private readonly listenRepository: ListenRepository) {}
async createListen({
user,
track,
playedAt,
}: CreateListenRequestDto): Promise<CreateListenResponseDto> {
const response = await this.listenRepository.insertNoConflict({
user,
track,
playedAt,
});
return response;
}
@Span()
async createListens(
listensData: CreateListenRequestDto[]
listensData: CreateListenRequestDto[],
): Promise<Listen[]> {
const existingListens = await this.listenRepository.findBy(listensData);
@ -42,17 +27,19 @@ export class ListensService {
(existingListen) =>
newListen.user.id === existingListen.user.id &&
newListen.track.id === existingListen.track.id &&
newListen.playedAt.getTime() === existingListen.playedAt.getTime()
)
newListen.playedAt.getTime() === existingListen.playedAt.getTime(),
),
);
return this.listenRepository.save(
missingListens.map((entry) => this.listenRepository.create(entry))
const newListens = await this.listenRepository.save(
missingListens.map((entry) => this.listenRepository.create(entry)),
);
return [...existingListens, ...newListens];
}
async getListens(
options: GetListensDto & IPaginationOptions
options: GetListensDto & IPaginationOptions,
): Promise<Pagination<Listen>> {
const { page, limit, user, filter } = options;
@ -77,16 +64,6 @@ export class ListensService {
});
}
async getMostRecentListenPerUser(): Promise<Listen[]> {
return this.listenRepository
.createQueryBuilder("listen")
.leftJoinAndSelect("listen.user", "user")
.distinctOn(["user.id"])
.orderBy({ "user.id": "ASC", "listen.playedAt": "DESC" })
.limit(1)
.getMany();
}
getScopedQueryBuilder(): ListenScopes {
return this.listenRepository.scoped;
}

View file

@ -13,7 +13,7 @@ import { Scope } from "@sentry/node";
function setupSentry(
app: NestExpressApplication,
configService: ConfigService
configService: ConfigService,
) {
Sentry.init({
dsn: configService.get<string>("SENTRY_DSN"),
@ -34,7 +34,7 @@ function setupSentry(
}
},
],
})
}),
);
}
@ -43,14 +43,19 @@ async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
bufferLogs: true,
rawBody: true,
});
app.useLogger(app.get(Logger));
app.useGlobalPipes(
new ValidationPipe({
transform: true,
transformOptions: { enableImplicitConversion: true },
})
}),
);
app.useBodyParser("json", {
limit:
"10mb" /* Need large bodies for Spotify Extended Streaming History */,
});
app.enableShutdownHooks();
const configService = app.get<ConfigService>(ConfigService);

View file

@ -1,5 +1,6 @@
export class FindTrackDto {
spotify: {
id: string;
id?: string;
uri?: string;
};
}

Some files were not shown because too many files have changed in this diff Show more