Compare commits

...

152 Commits

Author SHA1 Message Date
4e089f53e5 Update dependency @types/node to v24.0.10
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-02 02:08:07 +00:00
f87fcb27dc Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-29 02:11:13 +00:00
dfac15e4b9 Update dependency @types/node to v24.0.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-29 02:10:51 +00:00
0c60473a3f Update dependency undici to v7.11.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-28 02:04:52 +00:00
e18a5f1d58 Update dependency @types/node to v24.0.6
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-28 02:04:37 +00:00
86d704aa16 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-21 02:05:22 +00:00
b0184893e3 Update dependency @types/node to v24.0.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-17 02:01:53 +00:00
b6e7a52451 Update dependency @types/node to v24
All checks were successful
continuous-integration/drone/push Build is passing
2025-06-15 02:04:51 +00:00
23148f8751 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-15 02:04:44 +00:00
f8cb71398f Update dependency fastify to v5.4.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-14 02:04:00 +00:00
5464bd2d20 Update dependency @types/node to v22.15.31
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-11 02:04:05 +00:00
610681ae0a Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-07 02:05:28 +00:00
693180b8a5 Update dependency @types/node to v22.15.30
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-06 02:03:19 +00:00
6a8df01264 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-06-01 02:04:12 +00:00
3d57292561 Update dependency esbuild to v0.25.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-31 02:05:25 +00:00
5a7759f89b Update dependency @types/node to v22.15.29
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-31 02:05:15 +00:00
7a74106eb2 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-25 02:03:59 +00:00
a6d010a966 Update dependency undici to v7.10.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-25 02:03:38 +00:00
ae7f3ec602 Update dependency @fastify/static to v8.2.0
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-24 02:04:47 +00:00
e3008815bf Update dependency @types/node to v22.15.21
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-21 02:01:52 +00:00
3d58ecf99b Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-18 02:04:10 +00:00
c6a3cb322f Update src/server/api.ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-17 08:43:07 +02:00
619a06629b Update dependency fastify to v5.3.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-17 02:10:46 +00:00
9f6dd77fb4 Update dependency @types/node to v22.15.18
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-15 02:06:55 +00:00
38d895a959 WIP
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 00:55:06 +02:00
6690b055b4 Fix noversion
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 00:47:04 +02:00
09b9830fd0 Update
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-15 00:35:39 +02:00
d1c4a23e26 Update dependency undici to v7.9.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-11 02:07:41 +00:00
ae9c653ea9 Update dependency @fastify/view to v11.1.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-11 02:07:26 +00:00
8cec4c4d6c Update dependency esbuild to v0.25.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-10 02:09:10 +00:00
4095738cbc Update dependency @types/node to v22.15.17
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-09 02:03:50 +00:00
fdb5f04c9a Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-04 02:04:36 +00:00
ee69f6a101 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-05-03 02:05:15 +00:00
5389670cfd Update dependency @types/node to v22.15.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-29 02:01:51 +00:00
910afba8e5 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-27 02:05:48 +00:00
9912cd14ae Update dependency nodemon to v3.1.10
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-27 02:05:32 +00:00
da8fb171b9 Update dependency @types/node to v22.15.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-26 02:05:49 +00:00
ba62d6bddf Update dependency esbuild to v0.25.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-24 02:02:52 +00:00
6811b16ec4 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-20 02:04:31 +00:00
fe96dbc5c2 Update dependency jsdom to v26.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-19 02:05:06 +00:00
94161d5be6 Update dependency fastify to v5.3.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-19 02:04:54 +00:00
73394bc922 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-13 02:05:25 +00:00
ee8d898fe8 Update dependency fastify to v5.3.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-13 02:05:04 +00:00
f47b52cd43 Update dependency @fastify/leveldb to v6.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2025-04-12 02:04:30 +00:00
21cc72e1a4 Update dependency @types/node to v22.14.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-12 02:04:20 +00:00
3cf8a5137b Update dependency undici to v7.7.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-06 02:04:39 +00:00
cde4adccdc Update dependency @types/node to v22.14.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-04-05 02:05:32 +00:00
041f4c1e74 Update dependency esbuild to v0.25.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-31 02:02:09 +00:00
47e4dda875 Lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-30 02:05:32 +00:00
3026ec0b0f Update dependency undici to v7.6.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-30 02:05:09 +00:00
b021786b1a Update dependency fastify to v5.2.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-29 02:05:54 +00:00
ad3effe687 Update dependency @types/node to v22.13.14
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-28 02:02:58 +00:00
b307271c73 Update dependency @types/node to v22.13.11
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-22 02:09:48 +00:00
0360f78669 Update dependency @fastify/view to v11
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-19 02:03:02 +00:00
6588e27c00 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-16 02:04:16 +00:00
c57444bb53 Update dependency undici to v7.5.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-15 02:03:49 +00:00
c8483dc0fc Update dependency esbuild to v0.25.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-11 02:01:34 +00:00
b2a17d18ce Lock file maintenance
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2025-03-09 02:04:23 +00:00
7e881d1dea Lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-08 02:05:44 +00:00
d53e3e56b5 Style & flight
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-07 23:30:49 +01:00
803dd84858 dev merge (#163)
All checks were successful
continuous-integration/drone/push Build is passing
Co-authored-by: soraefir
Co-authored-by: sora-ext
Reviewed-on: #163
2025-03-05 00:11:11 +01:00
78b080dc05 fix css (#162)
All checks were successful
continuous-integration/drone/push Build is passing
Co-authored-by: soraefir <soraefir+git@helcel>
Reviewed-on: #162
2025-03-03 00:48:27 +01:00
6ccc3a5667 Update Dockerfile
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-02 01:16:43 +01:00
f6162016c6 Update Dockerfile
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-02 01:15:51 +01:00
e743483039 Update Dockerfile
Some checks failed
continuous-integration/drone/push Build is failing
2025-03-02 01:11:58 +01:00
aee79aac75 dev (#160)
Some checks failed
continuous-integration/drone/push Build is failing
Co-authored-by: sora-ext
Co-authored-by: soraefir
Reviewed-on: #160
2025-03-02 01:09:29 +01:00
977751d517 Update dependency axios to v1.8.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-03-01 02:05:37 +00:00
fa3267b30c Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-25 02:02:08 +00:00
90f6ae7695 Migrate to esbuild & ts
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-24 08:38:25 +01:00
96c12c8da4 Fix dockerfile
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-22 15:39:40 +01:00
9f6c3d2c21 Update dockerfile
Some checks failed
continuous-integration/drone/push Build is failing
2025-02-22 15:33:38 +01:00
4e7687d5b7 Fix js
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-22 15:25:18 +01:00
1731d673d0 Delete yarn.lock
All checks were successful
continuous-integration/drone/push Build is passing
2025-02-21 12:48:55 +01:00
f8c3b2cc91 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-15 02:09:09 +00:00
2c8f3ca234 Update dependency prettier to v3.5.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-14 02:03:28 +00:00
3509bf24d8 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-09 02:07:57 +00:00
5f18d88b5c Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-08 02:08:22 +00:00
6ca18e3ebc Update dependency @prettier/plugin-pug to v3.2.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-08 02:07:58 +00:00
772deecc59 Update dependency @fastify/static to v8.1.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-03 02:02:08 +00:00
0288bbbcd2 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-01 02:08:26 +00:00
15d6a63d76 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-25 02:06:50 +00:00
41747af04b Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-18 01:05:38 +00:00
8ca9659c09 Update dependency @fastify/view to v10.0.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-13 01:01:45 +00:00
4e239db55d Update dependency @fastify/static to v8.0.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-12 01:04:11 +00:00
f23f42b12d Lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-11 01:04:52 +00:00
9bb2aab33e Update dependency @fastify/leveldb to v6.0.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-08 01:02:30 +00:00
d31fabf7d7 Update dependency fastify to v5.2.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-07 01:01:58 +00:00
9a63f9138b Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-04 01:04:18 +00:00
1867b69868 Lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-12-28 01:04:56 +00:00
393f6c7c7f Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-22 01:04:30 +00:00
7815d39a33 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-21 01:05:32 +00:00
ba6818ec3d Lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-15 01:04:39 +00:00
e118742835 Update dependency fastify to v5.2.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-15 01:04:16 +00:00
1b0b68a1ae Update dependency nodemon to v3.1.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-14 07:48:57 +00:00
dd9f21c1a3 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-07 01:04:08 +00:00
3bac51785e Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-06 01:02:09 +00:00
4a3c4aab79 Update dependency prettier to v3.4.2
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-05 02:01:19 +01:00
49e0283a0a Update dependency axios to v1.7.9
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-12-05 01:01:02 +00:00
91cebfddbf Update dependency prettier to v3.4.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-27 01:00:59 +00:00
44da8c6218 Update dependency axios to v1.7.8
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-26 01:00:56 +00:00
38661fb871 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-23 01:03:58 +00:00
2d98e0b142 Update dependency @fastify/static to v8.0.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-23 01:03:38 +00:00
932edda067 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-16 01:03:45 +00:00
052cce47f4 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-09 01:04:06 +00:00
53545c4f63 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-02 01:04:28 +00:00
0e96214280 Update dependency fastify to v5.1.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-11-01 01:00:51 +00:00
d717abcd4b Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-26 00:04:16 +00:00
4f3bc43a77 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-19 00:03:39 +00:00
f266ecc871 Update dependency @fastify/static to v8.0.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-18 00:00:55 +00:00
d9b9fe120f Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-12 00:04:03 +00:00
838e70c32c Update dependency @prettier/plugin-pug to v3.2.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-12 00:03:46 +00:00
02297c83b1 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-10-05 00:04:33 +00:00
729eb9acad Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-29 00:03:53 +00:00
85e30ce052 Lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-28 00:04:18 +00:00
4b198837ad Update dependency @fastify/view to v10
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-22 00:03:28 +00:00
36907aadf5 Update dependency @fastify/static to v8.0.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-22 00:03:16 +00:00
f94667a77b Update dependency @fastify/static to v8
All checks were successful
continuous-integration/drone/push Build is passing
2024-09-21 00:04:40 +00:00
2be918c03d Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-21 00:04:30 +00:00
6feceda2d5 Update dependency fastify to v5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-20 00:01:34 +00:00
01d5956178 Update dependency @fastify/leveldb to v6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-18 00:02:11 +00:00
edeaf8d371 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-14 00:03:30 +00:00
e2281deb5d Update dependency @prettier/plugin-pug to v3.1.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-08 00:01:31 +00:00
c3d4ab4c72 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-07 00:01:38 +00:00
c87c455917 Update dependency axios to v1.7.7
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-09-01 00:01:34 +00:00
a1b3582c1f Lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-31 00:02:00 +00:00
a1a376ace5 Update dependency axios to v1.7.6
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-31 00:01:36 +00:00
2ba8e6e475 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-24 00:02:09 +00:00
b317ad886a Update dependency axios to v1.7.5
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-24 00:01:48 +00:00
73ad70be63 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-15 00:01:43 +00:00
43cd1bf3e0 Update dependency axios to v1.7.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-14 00:01:26 +00:00
f1d8c0090d Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-10 00:02:23 +00:00
d2a728add0 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-03 00:02:15 +00:00
7196e79a00 Update dependency axios to v1.7.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-08-02 00:01:18 +00:00
3ad4e9b3b6 Lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-07-27 00:02:52 +00:00
d9be092617 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-20 00:03:46 +00:00
a255d9eb28 Lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-14 00:04:21 +00:00
dcd75e5ad3 Update dependency prettier to v3.3.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-14 00:03:53 +00:00
208efc22ef Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-07-07 00:04:10 +00:00
072e898b4a Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-30 00:04:08 +00:00
d8fc0142ff Update dependency fastify to v4.28.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-30 00:03:44 +00:00
b8e5dc5568 Lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-22 00:02:35 +00:00
ede7b5091a Update dependency nodemon to v3.1.4
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-21 00:01:24 +00:00
d182ecf814 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-15 00:03:43 +00:00
b967ecace9 Update dependency fastify to v4.28.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-15 00:03:19 +00:00
72961d308b Update dependency prettier to v3.3.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-12 00:02:08 +00:00
a31802e2e7 Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-08 00:02:42 +00:00
a751d70d0c Update dependency prettier to v3.3.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-06 00:01:04 +00:00
d45e62f5fc Update dependency nodemon to v3.1.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-04 00:01:38 +00:00
71db5343bf Update dependency prettier to v3.3.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-02 00:02:50 +00:00
0a08a4ebfe Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-01 00:04:25 +00:00
02d01803fc Lock file maintenance
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-31 00:01:43 +00:00
9e00cc8689 Update dependency nodemon to v3.1.2
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-30 00:01:58 +00:00
76 changed files with 4419 additions and 4563 deletions

7
.gitignore vendored
View File

@ -3,3 +3,10 @@ yarn-error.log
node_modules/
auth/
db/
.yarn/
public/*.js
public/*.map
public/*.css
.yarnrc.yml
.pnp*
build/

View File

@ -1,15 +1,17 @@
FROM node:lts-alpine
FROM node:current-alpine
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install
COPY *.json ./
COPY src ./src
RUN yarn
RUN yarn build
# If you are building your code for production
# RUN npm ci --only=production
# Bundle app source
COPY . .
EXPOSE 8080
CMD [ "node", "server.js" ]
CMD [ "yarn", "demon" ]

View File

@ -2,11 +2,14 @@
"name": "volp",
"version": "1.0.0",
"description": "Open Travel Mapper",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"demon": "nodemon server.js"
"build": "yarn build-server && yarn build-client && yarn build-style",
"build-style": "esbuild src/style/index.css --outfile=public/index.css --bundle --minify ",
"build-client": "esbuild src/client/main.ts --outfile=public/main.js --tree-shaking=true --bundle --minify --sourcemap --tsconfig=tsconfig-client.json",
"build-server": "esbuild src/server/**/*.ts --outdir=build --platform=node --format=cjs",
"start": "node build/main.js",
"demon": "nodemon -e ts,js,css --watch src --watch template --watch router --exec \"yarn build && yarn start\""
},
"repository": {
"type": "git",
@ -15,14 +18,16 @@
"author": "",
"license": "ISC",
"dependencies": {
"@fastify/leveldb": "^5.0.1",
"@fastify/static": "^7.0.0",
"@fastify/view": "^9.0.0",
"@prettier/plugin-pug": "^3.0.0",
"axios": "^1.4.0",
"fastify": "^4.18.0",
"@fastify/leveldb": "^6.0.0",
"@fastify/static": "^8.0.0",
"@fastify/view": "^11.0.0",
"@types/node": "^24.0.0",
"esbuild": "^0.25.0",
"fastify": "^5.2.1",
"jsdom": "^26.0.0",
"nodemon": "^3.0.1",
"prettier": "^3.0.0",
"pug": "^3.0.2"
"prettier": "^3.5.2",
"pug": "^3.0.2",
"undici": "^7.3.0"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

BIN
public/js/.DS_Store vendored

Binary file not shown.

View File

@ -1,495 +0,0 @@
const gen_id = (length) => {
var result = "";
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const len = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * len));
}
return result;
};
Date.prototype.toJSONLocal = (function () {
function addZ(n) {
return (n < 10 ? "0" : "") + n;
}
return function () {
return (
this.getFullYear() +
"-" +
addZ(this.getMonth() + 1) +
"-" +
addZ(this.getDate())
);
};
})();
function toEncoded(string) {
const codeUnits = Uint16Array.from(
{ length: string.length },
(element, index) => string.charCodeAt(index),
);
const charCodes = new Uint8Array(codeUnits.buffer);
let result = "";
charCodes.forEach((char) => {
result += String.fromCharCode(char);
});
return window.btoa(result);
}
function toDecoded(string) {
let binary = window.atob(string);
const bytes = Uint8Array.from({ length: binary.length }, (element, index) =>
binary.charCodeAt(index),
);
const charCodes = new Uint16Array(bytes.buffer);
let result = "";
charCodes.forEach((char) => {
result += String.fromCharCode(char);
});
return result;
}
const query_nominatim = (q, f) =>
axios
.get("/api/place/" + q)
.then((res) => res.data)
.then((res) => res.filter(f));
const query_flight = (q) =>
axios.get("/api/flight/" + q).then((res) => res.data);
const is_restauration_type = (e) =>
["restaurant", "cafe", "pub", "bar", "fast_food", "food_court"].indexOf(
e.type,
) != -1;
const is_attraction_type = (e) =>
[
"tourism",
"leisure",
"place",
"amenity",
"highway",
"historic",
"natural",
"waterway",
].indexOf(e.category) != -1 ||
[
"place_of_worship",
"national_park",
"nature_reserve",
"protected_area",
].indexOf(e.type != -1);
const icon_type = (item) => {
let t = item.type;
let c = item.category;
const arr = ["restaurant", "cafe", "pub", "bar", "fast_food", "food_court"];
if (arr.indexOf(t) != -1) {
return "utensils";
} else if (t == "hotel" || t == "hostel" || t == "guest_house") {
return "bed";
} else if (t == "museum" || c == "historic" || t == "place_of_worship") {
return "landmark";
} else if (t == "peak" || t == "viewpoint") {
return "mountain";
} else if (t == "parking") {
return "parking";
} else if (
t == "water" ||
t == "river" ||
t == "lake" ||
t == "torrent" ||
t == "aquarium"
) {
return "water";
} else if (t == "community_centre" || t == "locality") {
return "building";
} else if (t == "attraction") {
return "landmark";
} else if (t == "information" || t == "university") {
return "landmark";
} else if (t == "bridge") {
return "archway";
} else if (
t == "woodland" ||
t == "shieling" ||
t == "national_park" ||
t == "zoo" ||
t == "park" ||
t == "garden" ||
0
) {
return "tree";
} else if ((t == "water_park", t == "theme_park")) {
return "dice-five";
} else if (
t == "?" ||
t == "neighbourhood" ||
t == "quarter" ||
c == "highway"
) {
return "";
} else {
console.log(item.display_name, item.category, item.type);
return "question";
}
};
Vue.component("l-map", window.Vue2Leaflet.LMap);
Vue.component("l-tile-layer", window.Vue2Leaflet.LTileLayer);
Vue.component("l-marker", window.Vue2Leaflet.LMarker);
Vue.component("l-icon", window.Vue2Leaflet.LIcon);
Vue.component("l-popup", window.Vue2Leaflet.LPopup);
Vue.component("l-tooltip", window.Vue2Leaflet.LTooltip);
Vue.component("l-control-scale", window.Vue2Leaflet.LControlScale);
Vue.component("multiselect", window.VueMultiselect.default);
Vue.use(window.VueTextareaAutosize);
const app = new Vue({
el: "#app",
data: {
journey_edit:
["view", "short"].indexOf(window.location.pathname.split("/")[1]) == -1,
journey_id: window.location.pathname.split("/").pop() || gen_id(16),
journey_step_data: { day: 1, section: 0 },
journey_data: {
name: "New Journey",
main: [],
},
query: { hotel: [], flight: [], nominatim: [] },
querying: { hotel: false, flight: false, place: false, food: false },
impexp: "",
lang: {
format: "ddd D MMM",
formatLocale: {
firstDayOfWeek: 1,
},
monthBeforeYear: true,
},
},
methods: {
start_journey: function (event) {
window.location.href = "/" + this.journey_id;
},
add_section: function (event) {
if (this.journey_data.main == undefined) this.journey_data.main = [];
this.journey_data.main.push({
title: "?",
step_title: [],
map: { zoom: 2 },
hotel: { latlon: [0, 0] },
places: { restaurants: [], places: [] },
});
},
step_len: function (idx) {
return this.journey_data.main[idx].dateRange
? (this.journey_data.main[idx].dateRange[1] -
this.journey_data.main[idx].dateRange[0]) /
(1000 * 60 * 60 * 24) +
1
: 1;
},
next_step: function () {
this.journey_step_data.day += 1;
let s = this.journey_step_data.section;
let cd = this.step_len(s);
if (this.journey_step_data.day > cd) {
this.journey_step_data.section += 1;
if (this.journey_step_data.section >= this.journey_data.main.length) {
this.journey_step_data.section = this.journey_data.main.length - 1;
this.journey_step_data.day = cd;
} else {
this.journey_step_data.day = 1;
}
}
},
prev_step: function () {
this.journey_step_data.day -= 1;
if (this.journey_step_data.day <= 0) {
this.journey_step_data.section -= 1;
if (this.journey_step_data.section < 0) {
this.first_step();
} else {
let s = this.journey_step_data.section;
let cd = this.step_len(s);
this.journey_step_data.day = cd;
}
}
},
nextnext_step: function () {
this.journey_step_data.section += 1;
this.journey_step_data.day = 1;
if (this.journey_step_data.section >= this.journey_data.main.length)
this.first_step();
},
prevprev_step: function () {
this.journey_step_data.section -= 1;
this.journey_step_data.day = 1;
if (this.journey_step_data.section < 0) this.first_step();
},
first_step: function () {
this.journey_step_data.section = 0;
this.journey_step_data.day = 1;
},
active_date: function () {
if (this.journey_step_data.day < 0) return "?";
if (!this.journey_data.main[this.journey_step_data.section].dateRange)
return "?";
var date = new Date(
this.journey_data.main[this.journey_step_data.section].dateRange[0],
);
date.setDate(date.getDate() + this.journey_step_data.day - 1);
return this.format_date(date);
},
format_date: function (d) {
return (
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()] +
" " +
d.getDate() +
" " +
[
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
][d.getMonth()]
);
},
total_days: function () {
if (this.journey_data.main.length == 0) return 0;
try {
return (
(this.journey_data.main[this.journey_data.main.length - 1]
.dateRange[1] -
this.journey_data.main[0].dateRange[0]) /
(1000 * 60 * 60 * 24)
);
} catch {
return "?";
}
},
total_date: function () {
if (this.journey_data.main.length == 0) return "";
try {
return `${this.format_date(
this.journey_data.main[0].dateRange[0],
)} - ${this.format_date(
this.journey_data.main[this.journey_data.main.length - 1]
.dateRange[1],
)}`;
} catch {
return "?";
}
},
update_date: function (idx) {
let dateRange = this.journey_data.main[idx].dateRange;
let start_end = [0, 0];
let step_len = 0;
let last_start = dateRange[0];
for (let i = idx - 1; i >= 0; --i) {
step_len = this.step_len(i) - 1;
if (this.journey_data.main[i].dateRange) {
start_end = [last_start.getDate() - step_len, last_start.getDate()];
} else {
this.journey_data.main[i].dateRange = [new Date(), new Date()];
start_end = [last_start.getDate() - step_len, last_start.getDate()];
}
this.journey_data.main[i].dateRange[0].setTime(last_start.getTime());
this.journey_data.main[i].dateRange[0].setDate(start_end[0]);
this.journey_data.main[i].dateRange[1].setTime(last_start.getTime());
this.journey_data.main[i].dateRange[1].setDate(start_end[1]);
last_start = this.journey_data.main[i].dateRange[0];
}
let last_end = dateRange[1];
for (let i = idx + 1; i < this.journey_data.main.length; ++i) {
step_len = this.step_len(i) - 1;
if (this.journey_data.main[i].dateRange) {
start_end = [last_end.getDate(), last_end.getDate() + step_len];
} else {
this.journey_data.main[i].dateRange = [new Date(), new Date()];
start_end = [last_end.getDate(), last_end.getDate() + step_len];
}
this.journey_data.main[i].dateRange[0].setTime(last_end.getTime());
this.journey_data.main[i].dateRange[0].setDate(start_end[0]);
this.journey_data.main[i].dateRange[1].setTime(last_end.getTime());
this.journey_data.main[i].dateRange[1].setDate(start_end[1]);
last_end = this.journey_data.main[i].dateRange[1];
}
},
rm_section: function (idx) {
this.journey_data.main.splice(idx, 1);
if (this.journey_step_data.section == idx) {
this.prevprev_step();
}
},
sel_section: function (idx) {
this.journey_step_data.section = idx;
this.journey_step_data.day = 1;
},
search_nominatim: function (txt, f) {
if (txt == "") {
this.query.nominatim = [];
return Promise.resolve([]);
}
return query_nominatim(txt, f).then((results) => {
results.forEach((r) => {
r.latlon = [parseFloat(r.lat), parseFloat(r.lon)];
r.sname = r.display_name.split(",")[0];
});
this.query.nominatim = results;
});
},
search_flight: function (txt) {
if (txt == "") return;
this.querying.flight = true;
query_flight(txt.replace(" ", "")).then((results) => {
if (results.results == "") {
this.query.flight = [];
this.querying.flight = false;
return;
}
this.query.flight = results.results;
this.querying.flight = false;
});
},
generate_icon: function (item, fcolor) {
return L.AwesomeMarkers.icon({
icon: icon_type(item) || "star",
prefix: "fa",
markerColor: fcolor || item.color || "blue",
}).createIcon().outerHTML;
},
save_data: function () {
this.impexp = toEncoded(JSON.stringify(this.journey_data));
axios
.post("/api/" + this.journey_id, this.journey_data)
.then((response) => {
console.log("Saved...");
})
.catch((error) => {
console.warn("Error! Could not reach the API.");
});
},
import_data: function () {
this.journey_data = Object.assign({}, JSON.parse(toDecoded(this.impexp)));
this.journey_data.main.forEach((e) => {
if (e.dateRange) {
e.dateRange[0] = new Date(e.dateRange[0]);
e.dateRange[1] = new Date(e.dateRange[1]);
}
});
},
export_data: function () {
this.impexp = toEncoded(JSON.stringify(this.journey_data));
},
filter_selected: function (list, step) {
return list.filter((e) =>
step ? e.step == this.journey_step_data.day : e.step >= 0,
);
},
filter_unselected: function (list) {
return list.filter((e) => e.step == undefined || e.step < 0);
},
remove_item: function (list, idx) {
list[idx].step = -1;
list.splice(idx, 1);
},
log: function (e) {
console.log(e);
},
keyboardEvent(e) {
if (e.which === 13) {
}
},
},
created: function () {
window.addEventListener("keydown", (e) => {
switch (e.key) {
case "ArrowLeft":
this.prev_step();
break;
case "ArrowRight":
this.next_step();
break;
default:
console.log(e.key);
}
});
axios.get("/api/" + this.journey_id).then((response) => {
if (response.data == "") throw "Invalid Journey Data Received";
app.journey_data = response.data;
for (let e of app.journey_data.main) {
if (e.dateRange) {
e.dateRange[0] = new Date(e.dateRange[0]);
e.dateRange[1] = new Date(e.dateRange[1]);
}
e.step_title = e.step_title || [];
}
});
this.debounceSave = _.debounce(this.save_data, 500);
this.debounceSearch = {
hotel: _.debounce((q) => {
this.querying.hotel = true;
this.search_nominatim(
q,
(r) =>
r.type == "hotel" || r.type == "hostel" || r.type == "guest_house",
).then((r) => {
this.querying.hotel = false;
});
}, 500),
restaurants: _.debounce((q) => {
this.querying.food = true;
this.search_nominatim(q, (r) => is_restauration_type(r)).then((r) => {
this.querying.food = false;
});
}, 500),
places: _.debounce((q) => {
this.querying.place = true;
this.search_nominatim(q, (r) => is_attraction_type(r)).then((r) => {
this.querying.place = false;
});
}, 500),
other: _.debounce((q) => {
this.querying.any = true;
this.search_nominatim(q, (r) => true).then((r) => {
this.querying.any = false;
});
}, 500),
flight: _.debounce((q) => this.search_flight(q), 500),
};
},
watch: {
journey_data: {
handler: function (ndata, odata) {
this.debounceSave();
},
deep: true,
},
},
});

View File

@ -1,103 +0,0 @@
const axios = require("axios");
module.exports = (fastify, opts, done) => {
fastify.get("/flight/:id", async (req, reply) => {
const ENDPOINT = "https://www.flightradar24.com/v1/search/web/find";
const FORMAT = "-";
if (req.params.id) {
axios
.get(ENDPOINT, {
params: {
format: FORMAT,
query: req.params.id,
limit: 16,
type: "schedule",
},
})
.then((res) => reply.send(res.data));
} else {
return reply.send([]);
}
return reply;
});
fastify.get("/place/:id", async (req, reply) => {
const ENDPOINT = "https://nominatim.openstreetmap.org/";
const FORMAT = "jsonv2";
if (req.params.id) {
axios
.get(ENDPOINT, {
params: {
format: FORMAT,
q: req.params.id,
},
})
.then((res) => reply.send(res.data));
} else {
return reply.send([]);
}
return reply;
});
fastify.get("/gpx/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
fastify.level.db.get(req.params.id, (err, val) => {
if (err) {
console.warn(err);
reply.code(500).send();
} else {
let file = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="EMTAC BTGPS Trine II DataLog Dump 1.0 - http://www.ayeltd.biz" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
const data = JSON.parse(val);
const gen_wpt = (name,desc,latlon,icon="Flag") => `<wpt lat="${latlon[0]}" lon="${latlon[1]}"><ele>0</ele><name>${name}</name><cmt>-</cmt><desc>${desc}</desc><sym>${icon}</sym></wpt>`
const esc_str = (str) => (str||"Undefined").replace('"',"&quot;").replace("'","&apos;").replace("<","&lt;").replace(">","&gt;").replace("&","&amp;").replace("\n","...")
data.main.forEach(a => {
file+= gen_wpt(esc_str(a.hotel.name), esc_str(a.hotel.notes), a.hotel.latlon, icon="Hotel");
a.places.restaurants.forEach(b => {
file+= gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, icon="Restaurant");
});
a.places.activities.forEach(b => {
file+= gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, icon="Tree");
});
});
file+="</gpx>";
reply.header('Content-Type', 'application/gpx+xml');
reply.header('Content-Disposition', `attachment; filename=${req.params.id}.gpx`);
reply.send(file);
}
});
return reply;
});
fastify.get("/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
fastify.level.db.get(req.params.id, (err, val) => {
if (err) {
console.warn(err);
reply.code(500).send();
} else {
reply.send(JSON.parse(val));
}
});
return reply;
});
fastify.post("/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
fastify.level.db.put(req.params.id, JSON.stringify(req.body), (err) => {
if (err) {
console.warn(err);
reply.code(500).send({ error: "Error with DB" });
} else {
reply.send({ content: "ok" });
}
});
return reply;
});
done();
};

View File

@ -1,35 +0,0 @@
const fastify = require("fastify")(); //{ logger: true });
const path = require("path");
fastify.register(require("@fastify/static"), {
root: path.join(__dirname, "public"),
prefix: "/public/",
});
fastify.register(
require("@fastify/leveldb"),
{
name: "db",
},
(err) => {
if (err) throw err;
}
);
fastify.register(require("@fastify/view"), {
engine: {
pug: require("pug"),
},
});
fastify.register(require("./router/api"), { prefix: "/api" });
fastify.get("/", (req, reply) => reply.view("/template/home.pug"));
fastify.get("/:id", (req, reply) => reply.view("/template/journey.pug"));
fastify.get("/view/:id", (req, reply) => reply.view("/template/view.pug"));
fastify.get("/short/:id", (req, reply) => reply.view("/template/short.pug"));
fastify.listen({ port: 8080, host: "0.0.0.0" }, (err, address) => {
if (err) throw err;
console.log("Listening on", address);
});

167
src/client/api.ts Normal file
View File

@ -0,0 +1,167 @@
export const throttle = (func: (...args: any[]) => any, wait: number) => {
var lastTime = 0;
var timeoutId: ReturnType<typeof setTimeout> | undefined;
var lastArgs: any[];
return function (...args: any[]) {
const now = Date.now();
lastArgs = args;
if (timeoutId) clearTimeout(timeoutId);
timeoutId = undefined;
if (now - lastTime >= wait) {
lastTime = now;
func.apply(this, lastArgs);
} else {
timeoutId = setTimeout(
() => {
lastTime = Date.now();
func.apply(this, lastArgs);
},
wait - (now - lastTime)
);
}
};
};
export const load = (id: string) =>
fetch("/api/" + id)
.then((res) => {
if (!res.ok) throw new Error("Error " + res.statusText);
return res.json();
})
.then((res) => {
for (let e of res.main) {
if (e.date_range) {
e.date_range[0] = new Date(e.date_range[0]);
e.date_range[1] = new Date(e.date_range[1]);
}
e.day_title = e.day_title || [];
}
return res;
});
var version_add = 1
export const save = async (id: string, v: journey) => {
let body = JSON.parse(JSON.stringify(v))
body.version +=version_add;
return fetch("/api/" + id, { method: "post", body: JSON.stringify(body) })
.then((res) => {
if (!res.ok) throw new Error("Error " + res.statusText);
return res.json();
})
.then((_res) => {
version_add+=1;
console.log("Saved...");
});
}
export const query_nominatim = (
q: string,
bb: any,
f: (v: NominatimResult) => Boolean = () => true
) => {
if (q.length == 0) return Promise.resolve([])
let url = new URL("/api/place/" + q, window.location.origin);
url.searchParams.append("id", q);
url.searchParams.append("bb", JSON.stringify(bb));
return fetch(url)
.then((res) => (res.status == 200 ? res.json() : []))
.then((res) => res.filter(f));
};
export const query_flight = (q: string) =>
fetch("/api/flight/" + q).then((res) => res.json());
type NominatimResult = {
type: string;
category: string;
display_name: string; // DEBUG ONLY
};
export const is_restauration_type = (e: NominatimResult) =>
["restaurant", "cafe", "pub", "bar", "fast_food", "food_court"].indexOf(
e.type
) != -1;
export const is_attraction_type = (e: NominatimResult): boolean =>
[
"tourism",
"leisure",
"place",
"amenity",
// "highway",
"historic",
"natural",
"waterway",
].indexOf(e.category) != -1 ||
[
"place_of_worship",
"national_park",
"nature_reserve",
"protected_area",
].indexOf(e.type) != -1 || is_travel_type(e);
export const is_hotel_type = (e: NominatimResult): boolean =>
["hotel", "hostel", "guest_house"].indexOf(e.type) != -1
export const is_travel_type = (e: NominatimResult): boolean =>
["bus_stop", "tram_stop", "station", , "aerodrome", "parking"].indexOf(e.type) != -1
export const icon_type = (item: string | NominatimResult): string => {
if (typeof (item) == "string") {
return item
}
let t = item.type;
let c = item.category;
let types = {
utensils: [
"restaurant",
"cafe",
"pub",
"bar",
"fast_food",
"food_court",
],
bed: ["hotel", "hostel", "guest_house"],
landmark: [
"museum",
"historic",
"place_of_worship",
"attraction",
"information",
"university", "science_park", "theatre", "opera"
],
mountain: ["peak", "viewpoint"],
parking: ["parking"],
water: ["water", "river", "lake", "torrent", "aquarium"],
building: ["community_center", "locality"],
archway: ["bridge"],
tree: [
"woodland",
"shieling",
"national_park",
"park",
"zoo",
"garden",
"nature_reserve",
],
"dice-five": ["water_park", "theme_park", "casino"],
"": ["?", "neighbourhood", "quarter", "highway", "place"],
};
for (let k in types) {
if (types[k].indexOf(t) >= 0 || types[k].indexOf(c) >= 0) return k;
}
console.log(item.display_name, item.category, item.type);
return "question";
};
export const get_filter = function (f: string) {
switch (f) {
case "hotel": return is_hotel_type;
case "restaurant": return is_restauration_type;
case "place": return is_attraction_type;
case "other":
default: return () => true;
}
}

96
src/client/helper/api.ts Normal file
View File

@ -0,0 +1,96 @@
import { getGeoLine } from "../types/geom";
import * as api from "../api";
const filter_existing = function (
tpe: "nominatim" | "travel",
leg: leg,
r: geoloc[]
) {
switch (tpe) {
case "nominatim":
return r.filter((e) => {
if (leg.hotel && leg.hotel.osm_id == e.osm_id) return false;
if (leg.places.restaurants.find((i) => i.osm_id == e.osm_id))
return false;
if (leg.places.activities.find((i) => i.osm_id == e.osm_id))
return false;
return true;
});
case "travel":
console.log(r);
return r.filter((e) => {
if (
leg.travel.find(
(i) =>
`${(e as any).from}->${(e as any).to}` ==
`${(i as any).from}->${(i as any).to}`
)
)
return false;
return true;
});
}
};
const process_results = function (tpe: "nominatim" | "travel", r: geoloc[]) {
switch (tpe) {
case "nominatim":
return r.map((rr) => {
rr.latlon = [
parseFloat((rr as any).lat),
parseFloat((rr as any).lon),
];
rr.title = (rr as any).display_name.split(",")[0];
return rr;
});
case "travel":
console.log(r);
return r.map((el) => {
(el as any).path = getGeoLine(
{
lat: (el as any).from_geo.lat,
lng: (el as any).from_geo.lon,
},
{
lat: (el as any).to_geo.lat,
lng: (el as any).to_geo.lon,
},
{ dist: 2_500_000 }
).map((v) => [v.lat, v.lng]);
(el as any).type = "flight";
return el;
});
}
};
var _search_set_results: (...arg: any[]) => any;
export const set_search_set_results = function (f: (...arg: any[]) => any) {
_search_set_results = f;
};
export const search_nominatim = api.throttle(
(
f: string,
q: string,
bb: [[number, number], [number, number]],
leg: leg
) =>
api
.query_nominatim(q, bb, api.get_filter(f))
.catch((_err) => console.log(_err))
.then((r) => {
r = process_results("nominatim", r);
r = filter_existing("nominatim", leg, r);
_search_set_results(r);
}),
1000
);
export const search_flight = api.throttle(
(f: string, q: string, leg: leg) =>
api.query_flight(q).then((r) => {
r = process_results("travel", r);
r = filter_existing("travel", leg, r);
_search_set_results(r);
}),
3000
);

View File

@ -0,0 +1,41 @@
import journey_wrapper from './types/wrapper';
/* LIST HELPERS */
export const filter_selected = function (journey: journey_wrapper, list: geoloc[], step: boolean) {
return list.filter((e) =>
step ? e.step == journey.sel_day : e.step >= 0,
);
}
export const filter_unselected = function (list: geoloc[]) {
return list.filter((e) => e.step == undefined || e.step < 0);
}
export const remove_item = function (list: geoloc[], idx: number) {
list[idx].step = -1;
list.splice(idx, 1);
}
/* JOURNEY ADD/RM ITEM HELPER */
export const journey_add_place = function (journey: journey_wrapper, tpe: String, item: geoloc) {
switch (tpe) {
case 'hotel': return journey.leg_get().hotel = item;
case 'restaurant': return journey.leg_get().places.restaurants.push(item);
case 'place': return journey.leg_get().places.activities.push(item);
case 'other': return;
case 'flight': return journey.leg_get().travel.push(item);
}
}
export const journey_del_place = function (journey: journey_wrapper, tpe: String, idx: number) {
console.log(tpe)
switch (tpe) {
case "hotel": return journey.leg_get().hotel = null;
case "restaurants": return journey.leg_get().places.restaurants.splice(idx, 1);
case "activities": return journey.leg_get().places.activities.splice(idx, 1);
case "other": return;
case "flight": return journey.leg_get().travel.splice(idx, 1);
default: return true;
}
}

49
src/client/helper/nav.ts Normal file
View File

@ -0,0 +1,49 @@
import journey_wrapper from "../types/wrapper";
var nav = {
scrollInterval: 0,
scrollDir: 'none',
};
const sideScroll = function (element: Element, direction: 'left' | 'right' | 'none', speed: number, step: number) {
nav.scrollDir = direction
if (direction == 'none') return;
nav.scrollInterval = setInterval(() => {
element.scrollLeft += (direction == 'left') ? -step : step;
}, speed);
}
export const focus_leg = function (journey: journey_wrapper, idx: number | null = null) {
const c = document.querySelector('.scroll-content.nav-leg')!!
console.log(idx, c, journey)
const item = c.children[(idx != null ? idx : journey.sel_leg) + 1];
c.scrollLeft = (item as any).offsetLeft + ((item as any).offsetWidth / 2) - (c as any).offsetWidth / 2
}
export const focus_day = function (journey: journey_wrapper, idx: number | null = null) {
const c = document.querySelector('.scroll-content.nav-day')!!
console.log(idx, c, journey)
const item = c.children[(idx != null ? idx : journey.sel_day) + 1];
c.scrollLeft = (item as any).offsetLeft + ((item as any).offsetWidth / 2) - (c as any).offsetWidth / 2;
//focus_leg(journey) // We dont render both navs anymore
}
export const nav_mousemove = function (e: PointerEvent) {
if (e.pointerType != 'mouse') return;
const c = (e.target as any).closest('.scroll-content') || (e.target as any).firstChild;
const left = e.pageX - c.getBoundingClientRect().left;
const newDir =
left < c.offsetWidth * 0.1 ? 'left' :
(left > c.offsetWidth * 0.9 ? 'right' : 'none')
if (!nav.scrollInterval || nav.scrollDir != newDir) {
if (nav.scrollInterval) clearInterval(nav.scrollInterval)
sideScroll(c, newDir, 25, 10);
}
}
export const nav_mouseleave = function (_e: PointerEvent) {
clearInterval(nav.scrollInterval);
nav.scrollDir = 'none'
nav.scrollInterval = 0
}

4
src/client/main.ts Normal file
View File

@ -0,0 +1,4 @@
import "./types/ext";
import "./types/format";
import "./api";
import "./old";

233
src/client/old.js Normal file
View File

@ -0,0 +1,233 @@
import * as api from "./api";
import journey_wrapper from "./types/wrapper";
import { migrator } from "./types/migration";
import { journey_add_place, journey_del_place } from "./helper/journey";
import { search_nominatim, search_flight } from "./helper/api";
import { focus_day, focus_leg, nav_mouseleave, nav_mousemove } from "./helper/nav";
import { set_search_set_results } from "./helper/api";
Vue.component("l-map", window.Vue2Leaflet.LMap);
Vue.component("l-tile-layer", window.Vue2Leaflet.LTileLayer);
Vue.component("l-marker", window.Vue2Leaflet.LMarker);
Vue.component("l-icon", window.Vue2Leaflet.LIcon);
Vue.component("l-popup", window.Vue2Leaflet.LPopup);
Vue.component("l-tooltip", window.Vue2Leaflet.LTooltip);
Vue.component("l-polyline", window.Vue2Leaflet.LPolyline);
Vue.component("l-control-scale", window.Vue2Leaflet.LControlScale);
const app = new Vue({
el: "#app",
data: {
edit_active: ["view", "short"].indexOf(window.location.pathname.split("/")[1]) == -1,
journey: new journey_wrapper(window.location.pathname.split("/").pop() || String.gen_id(16)),
map_override: { active: false, elements: [] },
query: {
type: "", query: "", res: [], load: false, sub: false, note: false, drawer: false, addmarker: false,
},
useDay: false,
toast: {
show: false,
title: '',
desc: '',
special: '',
acceptText: '',
cancelText: '',
func: () => { },
},
lang: {
format: "ddd D MMM",
formatLocale: {
firstDayOfWeek: 1,
},
monthBeforeYear: true,
},
},
methods: {
start_journey: function () { window.location.href = "/" + this.journey.id },
compute_bb: function () {
if (!this.$refs.map) return undefined
const bounds = this.$refs.map.mapObject.getBounds();
return [[bounds.getSouthWest().lng, bounds.getSouthWest().lat],
[bounds.getNorthEast().lng, bounds.getNorthEast().lat]]
},
generate_rotation: function (index, list) {
if (index < 0 || index >= list.length) return 0;
const c0 = list[(index == 0) ? index : (index - 1)]
const c1 = list[(index == list.length - 1) ? index : (index + 1)]
const brng = Math.atan2(c1[1] - c0[1], c1[0] - c0[0]);
return `rotate:${brng - Math.PI / 2}rad`;
},
generate_marker: function (item, fcolor) {
return `
<div style="position: absolute;top: -30px;left: -6px;">
<i class=" fa fa-${api.icon_type(item) || "star"} fa-lg icon-white" style="position: absolute;text-align:center; width:24px;height:24px; line-height:1.5em"></i>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" width="24" height="36">
<circle cx="12" cy="12" r="12" fill="${fcolor || item.color || "blue"}"/>
<polygon points="4,12 20,12 12,36" fill="${fcolor || item.color || "blue"}" /></svg>`
},
generate_icon: function (item, fcolor = "", styling = "", classes = "") {
return `<i class="fa fa-${api.icon_type(item) || "star"} fa-2x ${classes}" style="${styling}; color:${fcolor || "white"}; text-align:center; align-content:center;"></i>`;
},
import_data: function (v) {
this.journey.data = Object.assign(
{},
JSON.parse(v.toDecoded()),
);
this.journey.data.main.forEach((e) => {
if (e.date_range) {
e.date_range[0] = new Date(e.date_range[0]);
e.date_range[1] = new Date(e.date_range[1]);
}
});
},
toast_reset: function () {
this.toast.show = false;
this.toast.title = '';
this.toast.desc = ''
this.toast.special = '';
this.toast.acceptText = ''
this.toast.cancelText = ''
this.toast.func = () => { }
},
toast_impexp: function (is_import) {
this.toast.show = true;
this.toast.title = is_import ? 'Import' : 'Export';
this.toast.desc = ''
this.toast.special = JSON.stringify(this.journey.data).toEncoded();
this.toast.acceptText = is_import ? 'load' : ''
this.toast.cancelText = 'cancel'
this.toast.func = () => { this.import_data(this.toast.special); this.toast_reset(); }
},
search_set_results(r) {
this.query.load = false;
this.query.res = r;
return r
},
search_set_clear(keep_drawer) {
this.query.res = [];
this.query.note = false;
this.query.type = null;
this.query.addmarker = false;
this.query.sub = false;
this.query.drawer = keep_drawer;
setTimeout(() => this.$refs.map.mapObject.invalidateSize(), 500);
},
search_set_active: function (is_notes, tpe) {
this.query.drawer = true;
this.query.note = is_notes;
this.query.type = !is_notes ? tpe : null;
this.query.load = !is_notes;
setTimeout(() => this.$refs.map.mapObject.invalidateSize(), 500);
},
drawer_hover_item: function (item) {
if (item) {
this.map_override.active = true
if (item.type == 'flight') {
this.map_override.elements = [[item.from_geo.lat, item.from_geo.lon], [item.to_geo.lat, item.to_geo.lon]]
} else {
this.map_override.elements = [[item.lat, item.lon]]
}
} else {
this.map_override.active = false
}
},
drawer_click_item: function (item) {
const tpe = this.query.type
this.search_set_clear(item ? true : false);
this.drawer_hover_item();
if (item) {
item.step = -1;
journey_add_place(this.journey, tpe, item)
}
},
search_active: function (_e) {
const tpe = this.query.type;
const query = this.query.query;
switch (tpe) {
case 'hotel':
case 'restaurant': ;
case 'place':
case 'other':
return search_nominatim(tpe, query, this.compute_bb(), this.journey.leg_get())
case 'flight':
return search_flight(tpe, query, this.journey.leg_get());
}
},
search_enable: function (f) {
const is_notes = f == 'notes';
this.search_set_active(is_notes, f)
setTimeout(() => document.getElementById(is_notes ? 'query_note' : 'query_input').focus(), 500);
if (!is_notes) this.search_active()
},
refreshTextAreaHeight: function (e) {
e.target.style['height'] = 'auto';
e.target.style['height'] = e.target.scrollHeight + 'px';
e.target.style['max-height'] = "100%";
},
onMapClick(e) {
if (this.query.addmarker) {
const newMarker = {
latlon: [e.latlng.lat, e.latlng.lng],
sname: this.query.query,
type: this.query.type,
};
this.drawer_click_item(newMarker)
}
},
},
created: function () {
set_search_set_results(this.search_set_results);
this.nav_mouseleave = nav_mouseleave;
this.nav_mousemove = nav_mousemove;
this.focus_day = focus_day;
this.focus_leg = focus_leg;
this.place_delete = (tpe, idx) => journey_del_place(this.journey, tpe, idx);
this.save_data = api.throttle(() => {
api.save(this.journey.id, this.journey.data);
}, 1000);
this.pressed_prev = api.throttle(() => {
if (this.useDay) this.journey.day_prev();
else this.journey.leg_prev();
}, 250)
this.pressed_next = api.throttle(() => {
if (this.useDay) this.journey.day_next();
else this.journey.leg_next();
}, 250)
window.addEventListener("keydown", (e) => {
switch (e.key) {
case "ArrowLeft": return this.pressed_prev()
case "ArrowRight": return this.pressed_next()
default: return console.log(e.key);
}
});
api.load(this.journey.id).then((r) => {
app.journey.data = migrator(r)
});
},
watch: {
journey: {
handler: function (ndata, odata) {
if (this.edit_active) this.save_data();
},
deep: true,
},
},
});

94
src/client/types/ext.ts Normal file
View File

@ -0,0 +1,94 @@
// DATE EXTENTION
declare global {
interface Date {
toJSONLocal: () => string;
toLocal: () => string;
}
}
Date.prototype.toJSONLocal = function () {
function addZ(n: number): string {
return n <= 9 ? `0${n}` : `${n}`;
}
return [
this.getFullYear(),
addZ(this.getMonth() + 1),
addZ(this.getDate()),
].join("-");
}
Date.prototype.toLocal = function () {
return [["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][this.getDay()],
this.getDate(),
[
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
][this.getMonth()]].join(" ")
;
}
// ARRAY EXTENTION
declare global {
interface Array<T> {
foldl<B>(f: (x: T, acc: B) => B, acc: B): B;
foldr<B>(f: (x: T, acc: B) => B, acc: B): B;
}
}
Array.prototype.foldr = function <T, B>(f: (x: T, acc: B) => B, acc: B): B {
return this.reverse().foldl(f, acc);
};
Array.prototype.foldl = function <T, B>(f: (x: T, acc: B) => B, acc: B): B {
for (let i = 0; i < this.length; i++) acc = f(this[i], acc);
return acc;
};
// STRING EXTENTION
declare global {
interface String {
toEncoded: () => String;
toDecoded: () => String;
}
}
String.prototype.toEncoded = function () {
return window.btoa(encodeURIComponent(Array.from(this as string, (c) => c.charCodeAt(0)).foldl(
(e, v) => v + String.fromCharCode(e),
"",
)))
};
String.prototype.toDecoded = function () {
return Array.from(decodeURIComponent(window.atob(this as string)), (c) => c.charCodeAt(0)).foldl(
(e, v) => v + String.fromCharCode(e),
"",
);
};
declare global {
interface StringConstructor {
gen_id: (l: Number) => String;
}
}
String.gen_id = function (length) {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from(Array(length))
.map((_v) =>
characters.charAt(Math.floor(Math.random() * characters.length)),
)
.join("");
};
export { };

View File

@ -0,0 +1,63 @@
declare global {
interface LatLng {
lat: number
lng: number
}
interface geoloc {
title: string
osm_id: number
latlon: [number, number]
notes: string
step: -1
}
interface map {
zoom: number
center: LatLng
}
interface leg {
title: string
day_title: string[]
date_range: [Date, Date] | null
map: map
travel: geoloc[]
hotel: geoloc | null
places: {
restaurants: geoloc[]
activities: geoloc[]
}
notes: string
}
interface journey {
fmt_ver: number
version: number
title: string
main: leg[]
}
}
const leg_template: leg = {
title: "",
day_title: [],
map: { zoom: 2, center: { lng: 0, lat: 0 } },
travel: [],
hotel: null,
places: { restaurants: [], activities: [] },
notes: "",
date_range: null
}
const journey_template: journey = {
fmt_ver: 1,
version: 0,
title: "New Journey",
main: [leg_template],
}
export { map, geoloc, leg, journey }
export { journey_template, leg_template }

165
src/client/types/geom.ts Normal file
View File

@ -0,0 +1,165 @@
const ellipsoid = {
a: 6378137,
b: 6356752.3142,
f: 1 / 298.257223563
};
function mod(n: number, p: number): number {
const r = n % p;
return r < 0 ? r + p : r;
}
function wrap(degrees: number, max = 360) {
if (-max <= degrees && degrees <= max) {
return degrees;
} else {
return mod(degrees + max, 2 * max) - max;
}
}
function dist(src: LatLng, dst: LatLng, itr = 100, mit = true): number {
const p1 = src,
p2 = dst;
const φ1 = toRadians(p1.lat),
λ1 = toRadians(p1.lng);
const φ2 = toRadians(p2.lat),
λ2 = toRadians(p2.lng);
const π = Math.PI;
const ε = Number.EPSILON;
// allow alternative ellipsoid to be specified
const { a, b, f } = ellipsoid;
const dL = λ2 - λ1; // L = difference in longitude, U = reduced latitude, defined by tan U = (1-f)·tanφ.
const tanU1 = (1 - f) * Math.tan(φ1),
cosU1 = 1 / Math.sqrt(1 + tanU1 * tanU1),
sinU1 = tanU1 * cosU1;
const tanU2 = (1 - f) * Math.tan(φ2),
cosU2 = 1 / Math.sqrt(1 + tanU2 * tanU2),
sinU2 = tanU2 * cosU2;
const antipodal = Math.abs(dL) > π / 2 || Math.abs(φ2 - φ1) > π / 2;
let λ = dL,
sinλ: number | null = null,
cosλ: number | null = null; // λ = difference in longitude on an auxiliary sphere
let σ = antipodal ? π : 0,
sinσ = 0,
cosσ = antipodal ? -1 : 1,
sinSqσ: number | null = null; // σ = angular distance P₁ P₂ on the sphere
let cos2σ = 1; // σₘ = angular distance on the sphere from the equator to the midpoint of the line
let sinα: number | null = null,
cosSqα = 1; // α = azimuth of the geodesic at the equator
let C: number | null = null;
let λʹ: number | null = null,
iterations = 0;
do {
sinλ = Math.sin(λ);
cosλ = Math.cos(λ);
sinSqσ =
cosU2 * sinλ * (cosU2 * sinλ) +
(cosU1 * sinU2 - sinU1 * cosU2 * cosλ) * (cosU1 * sinU2 - sinU1 * cosU2 * cosλ);
if (Math.abs(sinSqσ) < ε) {
break; // co-incident/antipodal points (falls back on λ/σ = L)
}
sinσ = Math.sqrt(sinSqσ);
cosσ = sinU1 * sinU2 + cosU1 * cosU2 * cosλ;
σ = Math.atan2(sinσ, cosσ);
sinα = (cosU1 * cosU2 * sinλ) / sinσ;
cosSqα = 1 - sinα * sinα;
cos2σ = cosSqα !== 0 ? cosσ - (2 * sinU1 * sinU2) / cosSqα : 0; // on equatorial line cos²α = 0 (§6)
C = (f / 16) * cosSqα * (4 + f * (4 - 3 * cosSqα));
λʹ = λ;
λ = dL + (1 - C) * f * sinα * (σ + C * sinσ * (cos2σ + C * cosσ * (-1 + 2 * cos2σ * cos2σ)));
const iterationCheck = antipodal ? Math.abs(λ) - π : Math.abs(λ);
if (iterationCheck > π) {
throw new EvalError("λ > π");
}
} while (Math.abs(λ - λʹ) > 1e-12 && ++iterations < itr);
if (iterations >= itr) {
if (mit) {
return dist(
src,
{ lat: dst.lat, lng: dst.lng - 0.01 },
itr,
mit
);
} else {
throw new EvalError(`Inverse vincenty formula failed to converge after ${itr} iterations
(start=${src.lat}/${src.lng}; dest=${dst.lat}/${dst.lng})`);
}
}
const uSq = (cosSqα * (a * a - b * b)) / (b * b);
const A = 1 + (uSq / 16384) * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
const B = (uSq / 1024) * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
const Δσ =
B *
sinσ *
(cos2σ +
(B / 4) *
(cosσ * (-1 + 2 * cos2σ * cos2σ) -
(B / 6) * cos2σ * (-3 + 4 * sinσ * sinσ) * (-3 + 4 * cos2σ * cos2σ)));
const s = b * A * (σ - Δσ); // s = length of the geodesic
return s
}
function pointDistance(src: LatLng, dst: LatLng): number {
return dist(
{ lat: src.lat, lng: wrap(src.lng, 180) },
{ lat: dst.lat, lng: wrap(dst.lng, 180) }
);
}
function toRadians(degree: number): number {
return (degree * Math.PI) / 180;
}
function toDegrees(radians: number): number {
return (radians * 180) / Math.PI;
}
function midpoint(src: LatLng, dst: LatLng): LatLng {
// φm = atan2( sinφ1 + sinφ2, √( (cosφ1 + cosφ2⋅cosΔλ)² + cos²φ2⋅sin²Δλ ) )
// λm = λ1 + atan2(cosφ2⋅sinΔλ, cosφ1 + cosφ2⋅cosΔλ)
// midpoint is sum of vectors to two points: mathforum.org/library/drmath/view/51822.html
const φ1 = toRadians(src.lat);
const λ1 = toRadians(src.lng);
const φ2 = toRadians(dst.lat);
const Δλ = toRadians(dst.lng - src.lng);
// get cartesian coordinates for the two points
const A = { x: Math.cos(φ1), y: 0, z: Math.sin(φ1) }; // place point A on prime meridian y=0
const B = { x: Math.cos(φ2) * Math.cos(Δλ), y: Math.cos(φ2) * Math.sin(Δλ), z: Math.sin(φ2) };
// vector to midpoint is sum of vectors to two points (no need to normalise)
const C = { x: A.x + B.x, y: A.y + B.y, z: A.z + B.z };
const φm = Math.atan2(C.z, Math.sqrt(C.x * C.x + C.y * C.y));
const λm = λ1 + Math.atan2(C.y, C.x);
return { lat: toDegrees(φm), lng: toDegrees(λm) };
}
function recursiveMidPoint(src: LatLng, dst: LatLng, opt: { step?: number, dist?: number } = {}, curr = 0): LatLng[] {
const geom: LatLng[] = [src, dst];
const mp = midpoint(src, dst);
const split_step = (opt.step != undefined && (opt.step > 0 || curr == 0))
const split_dist = (opt.dist != undefined && (pointDistance(src, dst) > opt.dist || curr == 0))
const next_opt = split_step ? { step: (opt.step || 0) - 1 } : { dist: opt.dist };
if (split_step || split_dist) {
geom.splice(0, 1, ...recursiveMidPoint(src, mp, next_opt, curr + 1));
geom.splice(geom.length - 2, 2, ...recursiveMidPoint(mp, dst, next_opt, curr + 1));
} else {
geom.splice(1, 0, mp);
}
return geom;
}
export function getGeoLine(src: LatLng, dst: LatLng, opt: { step?: number, dist?: number }) {
return recursiveMidPoint(src, dst, opt, 1)
}

View File

@ -0,0 +1,26 @@
const FMT_VER_0 = 0
const FMT_VER_LATEST = FMT_VER_0
function migrate_A_to_0(e: journey): journey {
e.title = (e as any).name;
e.main.forEach((v) => {
v.date_range = v.date_range || (v as any).dateRange;
v.day_title = v.day_title || (v as any).step_title;
v.places.activities = v.places.activities || (v as any).places.places;
v.travel = v.travel || [];
v.day_title = typeof (v.day_title) == "string" ? [v.day_title] : []
})
e.version = e.version || 0;
console.log(e)
return e;
}
export const migrator = (e: journey): journey => {
if (e.fmt_ver == FMT_VER_LATEST) return e;
switch (e.fmt_ver) {
case FMT_VER_0: break; // Update when FMT_VER_1 releases
default:
return migrate_A_to_0(e)
}
return e;
}

156
src/client/types/wrapper.ts Normal file
View File

@ -0,0 +1,156 @@
import './ext.ts'
import { journey_template, leg_template } from "./format"
const date_day_diff = (d0: Date, d1: Date): number =>
(d1.getTime() - d0.getTime()) / (1000 * 60 * 60 * 24)
class journey_wrapper {
id: String
data: journey = journey_template;
sel_leg: number = 0;
sel_day: number = 0;
constructor(id: String) {
this.id = id;
}
leg_first = () => this.data.main[0]
leg_last = () => this.data.main[this.leg_count() - 1]
leg_count(): number {
return this.data.main.length;
}
leg_len(idx?: number): number {
let d = this.leg_get(idx == undefined ? this.sel_leg : idx).date_range;
return d ? date_day_diff(d[0], d[1]) + 1 : 1;
}
add_leg(): void {
if (this.data.main == undefined) this.data.main = [];
this.data.main.push(leg_template);
}
rm_leg(idx: number): void {
this.data.main.splice(idx, 1);
if (this.sel_leg == idx) this.leg_prev();
if (this.sel_leg > this.data.main.length - 1) this.leg_next();
}
tot_len(): number | "?" {
if (this.leg_count() == 0) return 0;
let lf = this.leg_first(), ll = this.leg_last();
if (lf.date_range && ll.date_range) {
let d0 = lf.date_range[0]
let d1 = ll.date_range[1]
return date_day_diff(d0, d1);
}
return "?";
}
leg_sel(idx: number): void {
this.sel_leg = idx;
this.sel_day = 0;
}
leg_get(idx?: number): leg {
return this.data.main[idx != undefined ? idx : this.sel_leg]
}
leg_next(): void {
this.sel_leg = Math.min(this.sel_leg + 1, this.leg_count() - 1);
this.sel_day = 0;
}
leg_prev(): void {
this.sel_leg = Math.max(this.sel_leg - 1, 0);
this.sel_day = 0;
}
day_sel(idx: number): void {
this.sel_day = idx;
}
day_next() {
this.sel_day += 1
if (this.sel_day > this.leg_len() - 1) {
if (this.sel_leg < this.leg_count() - 1) {
this.leg_next()
this.sel_day = 0;
} else {
this.sel_day = this.leg_len() - 1;
}
}
}
day_prev() {
this.sel_day -= 1
if (this.sel_day < 0) {
if (this.sel_leg > 0) {
this.leg_prev()
this.sel_day = this.leg_len() - 1;
} else {
this.sel_day = 0;
}
}
}
day_cycle() {
if (this.sel_day >= this.leg_len() - 1) {
this.sel_day = 0;
} else {
this.day_next()
}
}
date_sel(): string {
if (this.sel_day < 0) return "?";
let leg = this.leg_get()
if (!leg.date_range)
return "?";
var date = new Date(leg.date_range[0]);
date.setDate(date.getDate() + this.sel_day);
return date.toLocal();
}
date_tot() {
if (this.leg_count() == 0) return "";
let lf = this.leg_first(), ll = this.leg_last();
if (lf.date_range && ll.date_range)
return `${lf.date_range[0].toLocal()} - ${ll.date_range[1].toLocal()}`;
return "?";
}
date_update(idx: number) {
let date_range = this.leg_get(idx).date_range;
if (!date_range) return;
let start_end = [0, 0];
let step_len = 0;
let last_start = date_range[0];
for (let i = idx - 1; i >= 0; --i) {
step_len = this.leg_len(i) - 1;
if (this.leg_get(i).date_range) {
start_end = [last_start.getDate() - step_len, last_start.getDate()];
} else {
this.leg_get(i).date_range = [new Date(), new Date()];
start_end = [last_start.getDate() - step_len, last_start.getDate()];
}
let leg = this.leg_get(i)
if (leg.date_range) {
leg.date_range[0].setTime(last_start.getTime());
leg.date_range[0].setDate(start_end[0]);
leg.date_range[1].setTime(last_start.getTime());
leg.date_range[1].setDate(start_end[1]);
last_start = leg.date_range[0];
}
}
let last_end = date_range[1];
for (let i = idx + 1; i < this.leg_count(); ++i) {
step_len = this.leg_len(i) - 1;
if (this.leg_get(i).date_range) {
start_end = [last_end.getDate(), last_end.getDate() + step_len];
} else {
this.leg_get(i).date_range = [new Date(), new Date()];
start_end = [last_end.getDate(), last_end.getDate() + step_len];
}
let leg = this.leg_get(i)
if (leg.date_range) {
leg.date_range[0].setTime(last_end.getTime());
leg.date_range[0].setDate(start_end[0]);
leg.date_range[1].setTime(last_end.getTime());
leg.date_range[1].setDate(start_end[1]);
last_end = leg.date_range[1];
}
}
}
}
export default journey_wrapper;

86
src/server/api.ts Normal file
View File

@ -0,0 +1,86 @@
//import { ProxyAgent, setGlobalDispatcher } from 'undici';
import { flight_get_data } from './api_flight'
import { nominatim_get_data } from './api_nominatim';
//setGlobalDispatcher(new ProxyAgent(process.env.HTTPS_PROXY as string));
export default function (server, opts, done) {
server.get("/flight/:id", async (req, reply) =>
flight_get_data(req.params.id)
.then(res => {
let wait_for_all: Promise<any>[] = []
res.forEach(r => {
wait_for_all.push(nominatim_get_data(r.from).then(geo => (r as any).from_geo = geo[0]));
wait_for_all.push(nominatim_get_data(r.to).then(geo => (r as any).to_geo = geo[0]));
});
return Promise.all(wait_for_all).then(_ => res)
})
.then(res => reply.send(res))
);
server.get("/place/:id", async (req, reply) =>
nominatim_get_data(req.params.id, JSON.parse(req.query.bb))
.then(res => reply.send(res))
);
server.get("/gpx/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
server.level.db.get(req.params.id, (err, val) => {
if (err) {
console.warn(err);
reply.code(500).send();
} else {
let file = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" version="1.1" creator="EMTAC BTGPS Trine II DataLog Dump 1.0 - http://www.ayeltd.biz" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
const data = JSON.parse(val);
const gen_wpt = (name, desc, latlon, icon = "Flag") => `<wpt lat="${latlon[0]}" lon="${latlon[1]}"><ele>0</ele><name>${name}</name><cmt>-</cmt><desc>${desc}</desc><sym>${icon}</sym></wpt>`
const esc_str = (str) => (str || "Undefined").replace('"', "&quot;").replace("'", "&apos;").replace("<", "&lt;").replace(">", "&gt;").replace("&", "&amp;").replace("\n", "...")
data.main.forEach(a => {
file += gen_wpt(esc_str(a.hotel.name), esc_str(a.hotel.notes), a.hotel.latlon, "Hotel");
a.places.restaurants.forEach(b => {
file += gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, "Restaurant");
});
a.places.activities.forEach(b => {
file += gen_wpt(esc_str(b.name), esc_str(b.notes), b.latlon, "Tree");
});
});
file += "</gpx>";
reply.header('Content-Type', 'application/gpx+xml');
reply.header('Content-Disposition', `attachment; filename=${req.params.id}.gpx`);
reply.send(file);
}
});
return reply;
});
server.get("/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
return server.level.db.get(req.params.id)
.then(r=> reply.send(JSON.parse(r)))
.catch(err=>{
console.warn(err);
return reply.code(500).send({error: "Error with DB"});
})
});
server.post("/:id", async (req, reply) => {
if (req.params.id == undefined)
return reply.code(400).send({ error: "No ID query parameter" });
return server.level.db.get(req.params.id).then(r=>JSON.parse(r)).then(r=>r.version||-1).catch(_=>-1).then(db_version=>{
if(db_version+1 == req.body.version || db_version == -1)
return server.level.db.put(req.params.id, req.body)
.then(_=>reply.send({ content: "ok" }))
.catch(_=>reply.code(500).send({ error: "Error with DB" }))
return reply.code(409).send({error:"Too old version, please refresh."});
})
})
done();
};

86
src/server/api_flight.ts Normal file
View File

@ -0,0 +1,86 @@
import { JSDOM } from 'jsdom';
interface FlightData {
id: string;
date: string;
from: string;
to: string;
std: string;
atd: string | string[] | null;
sta: string;
ata: string | string[] | null;
}
function clean_times(s: string): string | null {
if (s == "—" || s == "Scheduled") return null
if (s.indexOf("Estimated departure") == 0) return null
if (s.indexOf("Landed") == 0) return s.replace("Landed ", "")
return s
}
export function flight_get_data(flightId: string): Promise<FlightData[]> {
const url = new URL(`https://www.flightradar24.com/data/flights/${flightId}`);
return fetch(url).then(res => res.text()).then(res => new JSDOM(res)).then(dom => {
const rows = dom.window.document.querySelectorAll('table tbody tr');
const flightData: FlightData[] = [];
rows.forEach(row => {
const columns = row.querySelectorAll('td');
/*
* 2/6 = Date/Duration
* 3/4 = From/To
* 7/8 = STD/ATD
* 9/11 = STA/ATA
*/
const flight: FlightData = {
id: flightId,
date: columns[2].textContent.trim(),
from: columns[3].textContent.trim(),
to: columns[4].textContent.trim(),
std: columns[7].textContent.trim(),
atd: clean_times(columns[8].textContent.trim()),
sta: columns[9].textContent.trim(),
ata: clean_times(columns[11].textContent.trim())
};
flightData.push(flight);
});
return groupByPair(flightData).map(v => groupByFrequency(v));
})
}
function groupByPair(flightData: FlightData[]): FlightData[][] {
const flightMap: { [key: string]: FlightData[] } = {};
flightData.forEach(flight => {
const key = `${flight.from}-${flight.to}-${flight.std}`;
if (!flightMap[key])
flightMap[key] = [];
flightMap[key].push(flight);
});
return Object.values(flightMap);
}
function groupByFrequency(flightData: FlightData[]): FlightData {
let data = 'OOOOOOO'; // Initialize with no flights
const atdArray: string[] = [];
const ataArray: string[] = [];
flightData.forEach(flight => {
const dayOfWeek = (new Date(flight.date).getDay() + 6) % 7;
data = data.substring(0, dayOfWeek) + 'X' + data.substring(dayOfWeek + 1);
if (flight.atd)
atdArray.push(flight.atd as string);
if (flight.ata)
ataArray.push(flight.ata as string);
});
return {
id: flightData[0].id,
date: data,
from: flightData[0].from,
to: flightData[0].to,
std: flightData[0].std,
sta: flightData[0].sta,
atd: atdArray,
ata: ataArray
};
};

View File

@ -0,0 +1,27 @@
function drop_fields(results) {
results.forEach(e => {
delete e.licence;
delete e.place_rank;
delete e.importance;
delete e.boundingbox;
});
return results
}
export function nominatim_get_data(id: string, bb: string[][] | null = null): Promise<any> {
if (!id) return Promise.resolve([])
const url = new URL("https://nominatim.openstreetmap.org/search");
url.searchParams.append('format', 'jsonv2')
url.searchParams.append('q', id)
url.searchParams.append('limit', '20')
if (bb) {
url.searchParams.append('viewbox', `${bb[0][0]},${bb[0][1]},${bb[1][0]},${bb[1][1]}`)
url.searchParams.append('bounded', `1`)
}
return fetch(url).then((res) => {
if (!res.ok) throw new Error("Nominatim Error")
return res.json().then(r => drop_fields(r))
})
}

36
src/server/main.ts Normal file
View File

@ -0,0 +1,36 @@
import fastify from 'fastify'
import fastify_static from '@fastify/static'
import fastify_db from '@fastify/leveldb'
import fastify_view from '@fastify/view';
import pug from 'pug'
import { join as pathJoin } from "path";
import api from "./api"
const server = fastify(); //{ logger: true });
server.register(fastify_static, {
root: pathJoin(__dirname, "../public"),
prefix: "/public/",
});
server.register(
fastify_db as any,
{ name: "db" }
);
server.register(fastify_view, {
engine: { pug: pug },
});
server.register(api, { prefix: "/api" });
server.get("/", (req, reply) => reply.view("/src/template/home.pug"));
server.get("/:id", (req, reply) => reply.view("/src/template/journey.pug"));
server.get("/view/:id", (req, reply) => reply.view("/src/template/view.pug"));
server.get("/short/:id", (req, reply) => reply.view("/src/template/short.pug"));
server.listen({ port: 8080, host: "0.0.0.0" }, (err, address) => {
if (err) throw err;
console.log("Listening on", address);
});

366
src/style/custom.css Normal file
View File

@ -0,0 +1,366 @@
body {
background: var(--darkdark);
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.leaflet-control-attribution,
.leaflet-popup-close-button {
visibility: hidden;
}
.p-abs {
position: absolute;
}
.display-none {
display: none;
}
.col-0 {
flex: 0 0 0%;
max-width: 0%;
overflow: hidden;
}
.map-container,
.drawer-container {
transition: flex 0.5s ease-in-out, max-width 0.5s ease-in-out;
}
.map-container {
min-width: 52px;
height: 100%;
}
.drawer-container {
height: 100%;
right: 0;
top: 0;
margin-left: 0;
}
.travel-path-icon {
margin-left: -12px;
margin-top: -32px;
}
.input {
max-height: 100%;
}
.ml-auto {
margin-left: auto;
}
.mr-auto {
margin-right: auto;
}
.input .mx-input {
height: 40px;
}
.h-100 {
height: 100%;
}
.h-0 {
height: 0%;
overflow: hidden;
}
.w-100 {
width: 100%;
}
.w-0 {
width: 0%;
overflow: hidden;
}
.map-menu {
position: absolute;
display: flex;
z-index: 1100;
right: 0;
flex-direction: column;
gap: 10px;
margin: 5px;
}
.map-menu-top {
top: 0;
}
.map-menu-bottom {
bottom: 0;
}
.map-menu-center {
top: 50%;
}
.padding-1 {
padding: 5px;
}
.map-menu-item {
background-color: var(--darkdark);
padding: 5px;
border-radius: 50%;
cursor: pointer;
}
.map-menu-item:hover {
background-color: var(--lightdark);
}
.map-menu-sub {
display: flex;
flex-direction: row-reverse;
gap: 5px;
}
.map-menu-item,
.map-menu-sub-item {
background-color: var(--darkdark);
padding: 5px;
border-radius: 50%;
cursor: pointer;
float: right;
width: 42px;
height: 42px;
align-content: center;
text-align: center;
}
.vue2leaflet-map {
border-radius: var(--border-radius);
}
.leaflet-popup-content {
margin: 10px 20px !important;
}
.leaflet-popup>.leaflet-popup-content-wrapper {
border-radius: var(--border-radius);
width: 350px;
}
.leaflet-popup-button-group {
position: absolute;
top: 0;
right: 0;
display: flex;
flex-direction: row;
gap: 5px;
margin: 2px 6px;
}
.leaflet-popup-button-group>a {
cursor: pointer;
}
.leaflet-popup-button-group>a:hover {
cursor: pointer;
filter: brightness(150%);
}
.leaflet-popup-button-group>a>i {
font-size: 1em;
}
.query-result {
display: flex;
align-items: center;
border-radius: var(--border-radius);
cursor: pointer;
}
.query-result:hover {
filter: brightness(85%);
}
.scroll-content.list-group>.list-group-item {
display: none;
}
.list-group-item.placeholder-left.show.bg-dark,
.list-group-item.placeholder-right.show.bg-dark {
background: var(--darkdark);
}
.list-group-item.placeholder-left.show {
margin-left: auto;
color: transparent;
cursor: default;
background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, var(--white) 50%);
}
.list-group-item.placeholder-right.show {
margin-right: auto;
color: transparent;
cursor: default;
background: linear-gradient(to left, rgba(0, 0, 0, 0) 0%, var(--white) 50%);
}
.scroll-content>.list-group-item.active,
.scroll-content>.list-group-item.show {
display: inline-block;
}
.scroll-content>div:nth-child(2) {
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}
.scroll-content>div:nth-last-child(3) {
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
.scroll-content>div:last-child {
border-radius: var(--border-radius);
}
.list-group-item.add {
position: absolute;
right: 0;
}
@media (min-width: 768px) {
.scroll-content.list-group>.list-group-item {
display: inline-block;
}
.scroll-content>.list-group-item.placeholder-left,
.scroll-content>.list-group-item.placeholder-right {
display: none;
}
.list-group-item.add {
position: relative;
right: 0;
margin-left: 1em;
}
}
.list-group {
display: flex;
overflow: auto;
white-space: nowrap;
scrollbar-width: none;
padding: 1rem 0rem;
}
.list-group-item>.text {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.list-group-item.show {
min-width: 24px;
}
.list-group-item {
border: 1px solid var(--darkdark);
display: inline-block;
position: relative;
cursor: pointer;
text-align: center;
padding: .5rem .5rem;
max-width: calc(100% - 48px);
display: inline-block;
}
.list-group-item.bg-white:hover {
background: var(--blue);
/* filter: brightness(85%); */
}
.mx-datepicker {
width: 100% !important;
}
.overlay {
position: fixed;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 5000;
}
.popup {
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
z-index: 5001;
}
.button {
background-color: var(--lightlight);
border: 0;
border-radius: var(--border-radius);
color: var(--darkdark);
display: inline-block;
text-align: center;
text-decoration: none;
text-transform: uppercase;
font-weight: 600;
white-space: nowrap;
line-height: 1.2em;
padding: 8px 16px;
margin: 8px 16px;
font-weight: 600;
cursor: pointer;
}
.tooltip {
position: relative;
display: inline-block;
cursor: pointer;
}
.tooltip .tooltip-text {
visibility: hidden;
background-color: var(--white);
color: var(--darkdark);
text-align: center;
border-radius: var(--border-radius);
padding: 2px;
position: absolute;
z-index: 20;
bottom: 0%;
left: 0%;
margin-left: 0px;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
.smallbar {
z-index: 10;
position: absolute;
width: 100%;
top: 0;
display: flex;
flex-flow: row;
padding: 8px;
margin: 0 -12px;
}

19
src/style/define.css Normal file
View File

@ -0,0 +1,19 @@
:root {
--black: #030B12;
--darkdark: #0C1D2E;
--dark: #203A53;
--lightdark: #425F7C;
--light: #93A9BE;
--lightlight: #B6C5D5;
--white: #F0F3F7;
--orange: ##F5B97D;
--yellow: #F5F57D;
--green: #B9F57D;
--turquoise: #7DF5B9;
--blue: #7DB9F5;
--purple: #B97DF5;
--pink: #F57DB9;
--red: #F57D7D;
--border-radius: 3px;
}

13
src/style/index.css Normal file
View File

@ -0,0 +1,13 @@
@import './define.css';
@import './module/input.css';
@import './module/load_n_spin.css';
@import './module/typography.css';
@import './module/layout.css';
@import './module/general.css';
@import './custom.css';
[v-cloak] {
display: none;
}

View File

@ -0,0 +1,206 @@
* {
box-sizing: border-box;
}
html,
body,
body,
div,
span,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
abbr,
address,
cite,
code,
del,
dfn,
em,
ins,
kbd,
q,
samp,
small,
strong,
sub,
sup,
var,
b,
i,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
figure,
footer,
header,
hgroup,
menu,
nav,
section,
time,
mark,
audio,
video {
background: transparent;
border: 0;
font-size: 100%;
margin: 0;
outline: 0;
padding: 0;
vertical-align: baseline;
}
article,
aside,
figure,
footer,
header,
main,
nav,
section {
display: block;
}
*,
*:before,
*:after {
box-sizing: border-box;
}
*,
*::after,
*::before {
box-sizing: border-box;
outline: none;
}
body {
background-color: #fff;
min-height: 100%;
overflow-x: hidden;
position: relative;
}
p {
font-weight: normal;
margin-bottom: 1.5em;
}
img {
max-width: 100%;
}
strong {
font-weight: 600;
}
ul {
margin-bottom: 1em;
}
li {
list-style: none;
margin-bottom: 0.5em;
}
/**
* BACKGROUND
*/
.bg-primary {
background-color: var(--blue);
}
.bg-dark {
background-color: var(--darkdark);
}
.bg-secondary {
background-color: var(--pink);
}
.bg-white {
background-color: var(--white);
}
.bg-success {
background-color: var(--green);
}
.bg-info {
background-color: var(--yellow);
}
.bg-warning {
background-color: var(--orange);
}
.bg-error {
background-color: var(--red);
}
.bg-gray {
background-color: var(--lightdark);
}
.bg-gray-light {
background-color: var(--lightlight);
}
.align {
align-items: center;
justify-content: center;
}
.fleft {
float: left;
}
.fright {
float: right;
}
.clearfix ::after {
clear: both;
content: "";
display: table;
}
.no-wrap {
white-space: nowrap;
}
.overflow-hidden {
overflow: hidden;
}
.rounded {
border-radius: var(--border-radius);
}

424
src/style/module/input.css Normal file
View File

@ -0,0 +1,424 @@
switch,
input,
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
resize: none;
}
label {
display: block;
font-weight: normal;
}
input:-webkit-autofill {
box-shadow: 0 0 0 1000px #eceff1 inset;
}
.switch,
.textarea,
.input,
.select {
border: 1px solid var(--white);
border-radius: var(--border-radius);
box-shadow: none;
display: inline-block;
font-weight: normal;
overflow: hidden;
}
.textarea :focus,
.input :focus,
.select :focus {
outline: none;
}
.textarea.has-error,
.input.has-error,
.select.has-error {
background: #eceff1;
border: 1px solid #e74c3c;
margin-bottom: 0;
}
.select {
background-color: #eceff1;
display: inline-block;
margin-right: 16px;
position: relative;
}
.select:last-child {
margin-right: 0;
}
.select-fullWidth {
display: block;
margin-left: 0;
margin-right: 0;
width: 100%;
}
.select select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: transparent;
border: 0;
border-radius: 0;
color: #272727;
display: block;
font-size: 16px;
line-height: 1.5em;
margin: 0;
padding: 8px 16px;
padding-right: 30px;
transition: background-color 0.2s ease-in-out;
width: 100%;
}
.select select:active,
.select select:focus {
background-color: #fbfbfc;
border: 0;
outline: none;
}
.select select::-ms-expand {
display: none;
}
.select::after,
.select::before {
background: #03a9f4;
content: "";
display: block;
height: 2px;
margin-top: 2px;
position: absolute;
right: 5px;
top: 50%;
-webkit-transform-origin: 1px;
transform-origin: 1px;
width: 10px;
}
.select::after {
-webkit-transform: rotate(-135deg);
transform: rotate(-135deg);
}
.select::before {
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.textarea {
background-color: #eceff1;
padding: 0;
}
.textarea-fullWidth {
display: block;
margin-left: 0;
margin-right: 0;
width: 100%;
}
.textarea textarea {
background: transparent;
border: 0;
color: #272727;
display: block;
font-family: "Lato", sans-serif;
font-size: 16px;
line-height: 1.5em;
margin: 0;
min-height: 120px;
padding: 8px 16px;
transition: background-color 0.2s ease-in-out;
width: 100%;
}
.textarea textarea::-webkit-input-placeholder {
color: #969da6;
}
.textarea textarea::-ms-input-placeholder {
color: #969da6;
}
.textarea textarea::placeholder {
color: #969da6;
}
.textarea textarea:focus,
.textarea textarea:active {
background-color: #fbfbfc;
border: 0;
outline: none;
}
.checkbox {
margin-bottom: 8px;
position: relative;
}
.checkbox input[type="checkbox"] {
display: none;
}
.checkbox input[type="checkbox"]:checked::after {
-webkit-animation: checkboxAndRadioAnimation 0.25s;
animation: checkboxAndRadioAnimation 0.25s;
content: "";
-webkit-transform: scale(1) rotate(45deg);
transform: scale(1) rotate(45deg);
}
.checkbox input[type="checkbox"] {
display: block;
overflow: hidden;
padding-left: 30px;
text-overflow: ellipsis;
white-space: nowrap;
}
.checkbox input[type="checkbox"]::before {
background-color: #eceff1;
border: 1px solid var(--white);
border-radius: var(--border-radius);
content: "";
display: inline-block;
height: 20px;
left: 0;
margin-top: -10px;
position: absolute;
top: 50%;
width: 20px;
}
.checkbox input[type="checkbox"]::after {
border-bottom: 3px solid #03a9f4;
border-right: 3px solid #03a9f4;
display: block;
height: 12px;
left: 11px;
margin-left: -4px;
margin-top: -7px;
position: absolute;
top: 50%;
width: 7px;
z-index: 1;
}
.radio {
margin-bottom: 8px;
position: relative;
}
.radio input[type="radio"] {
display: none;
}
.radio input[type="radio"]:checked::after {
-webkit-animation: checkboxAndRadioAnimation 0.25s;
animation: checkboxAndRadioAnimation 0.25s;
content: "";
-webkit-transform: scale(1) rotate(45deg);
transform: scale(1) rotate(45deg);
}
.radio input[type="radio"] {
display: block;
overflow: hidden;
padding-left: 30px;
text-overflow: ellipsis;
white-space: nowrap;
}
.radio input[type="radio"]::before {
background-color: #eceff1;
border: 1px solid var(--white);
border-radius: 20px;
content: "";
display: inline-block;
height: 20px;
left: 0;
margin-top: -10px;
position: absolute;
top: 50%;
width: 20px;
}
.radio input[type="radio"]::after {
background-color: #03a9f4;
border-radius: 20px;
display: block;
height: 10px;
left: 11px;
margin-left: -6px;
margin-top: -6px;
position: absolute;
top: 13px;
width: 10px;
z-index: 1;
}
@-webkit-keyframes checkboxAndRadioAnimation {
0% {
-webkit-transform: scale(0) rotate(45deg);
transform: scale(0) rotate(45deg);
}
50% {
-webkit-transform: scale(1.5) rotate(45deg);
transform: scale(1.5) rotate(45deg);
}
100% {
-webkit-transform: scale(1) rotate(45deg);
transform: scale(1) rotate(45deg);
}
}
@keyframes checkboxAndRadioAnimation {
0% {
-webkit-transform: scale(0) rotate(45deg);
transform: scale(0) rotate(45deg);
}
50% {
-webkit-transform: scale(1.5) rotate(45deg);
transform: scale(1.5) rotate(45deg);
}
100% {
-webkit-transform: scale(1) rotate(45deg);
transform: scale(1) rotate(45deg);
}
}
.input-invis {
background-color: transparent !important;
margin: auto !important;
border: 0 !important;
}
.switch,
.input {
background-color: var(--white);
padding: 0;
position: relative;
}
.input :focus,
.input :active {
background-color: var(--white);
border-radius: var(--border-radius);
}
.switch .rocker,
.input input,
.input textarea {
background: transparent;
border: 0;
box-shadow: none;
color: #272727;
font-size: 16px;
line-height: 1.5em;
margin: 0;
outline: none;
padding: 8px 16px;
width: 100%;
}
.input input::-webkit-input-placeholder {
color: #969da6;
}
.input input::-ms-input-placeholder {
color: #969da6;
}
.input input::placeholder {
color: #969da6;
}
.input input.small {
line-height: 1em;
padding: 0;
}
.input-withIcon input {
padding-right: 32px;
}
.input-icon {
fill: #969da6;
height: 16px;
margin-top: -8px;
position: absolute;
right: 16px;
top: 50%;
width: 16px;
}
.switch {
overflow: hidden;
display: block;
}
.switch input[type="checkbox"] {
position: absolute;
height: 100%;
width: 100%;
margin: 0;
padding: 0;
z-index: 3;
}
.switch .rocker {
padding: 0;
display: flex;
}
.switch .rocker-left,
.switch .rocker-right {
max-width: 50%;
font-weight: bold;
text-align: center;
font-size: 10px;
line-height: 1;
width: 50%;
color: var(--darkdark);
padding: 8px 16px;
-webkit-transition: background-position .25s;
-moz-transition: background-position .25s;
transition: background-position .25s;
background-size: 200% 100%;
background-image: linear-gradient(to right, transparent 50%, var(--blue) 50%);
}
.switch .rocker-left {
background-position-x: 100%;
}
.switch .rocker-bg {
position: relative;
border-radius: 50%;
transition: 0.4s cubic-bezier(0.18, 0.89, 0.35, 1.15) all;
left: 0;
width: 50%;
background-color: var(--blue);
z-index: 1;
}
.switch input:checked+.rocker .rocker-left {
background-position-x: 0%;
}
.switch input:checked+.rocker .rocker-right {
background-position-x: -100%;
}

359
src/style/module/layout.css Normal file
View File

@ -0,0 +1,359 @@
/**
* LAYOUT
*/
.section {
padding-bottom: 36px;
padding-top: 36px;
}
@media (min-width: 768px) {
.section {
padding-bottom: 72px;
padding-top: 72px;
}
}
.section+.section {
padding-top: 0;
}
.container {
margin: 0 auto;
max-width: 1380px;
padding-left: 12px;
padding-right: 12px;
width: 100%;
}
@media (min-width: 576px) {
.container {
max-width: 540px;
}
}
@media (min-width: 768px) {
.container {
max-width: 720px;
}
}
@media (min-width: 992px) {
.container {
max-width: 960px;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1140px;
}
}
@media (min-width: 768px) {
.container {
padding-left: 24px;
padding-right: 24px;
}
}
.container-medium {
margin: 0 auto;
max-width: 944px;
padding-left: 12px;
padding-right: 12px;
}
@media (min-width: 768px) {
.container-medium {
padding-left: 24px;
padding-right: 24px;
}
}
.container {
width: 100%;
padding-right: 12px;
padding-left: 12px;
margin-right: auto;
margin-left: auto;
}
.container-fluid {
width: 100%;
padding-right: 12px;
padding-left: 12px;
margin-right: auto;
margin-left: auto;
}
.row {
display: flex;
flex-wrap: wrap;
margin-right: -12px;
margin-left: -12px;
}
.col-1,
.col-2,
.col-3,
.col-4,
.col-5,
.col-6,
.col-7,
.col-8,
.col-9,
.col-10,
.col-11,
.col-12,
.col,
.col-auto,
.col-sm-1,
.col-sm-2,
.col-sm-3,
.col-sm-4,
.col-sm-5,
.col-sm-6,
.col-sm-7,
.col-sm-8,
.col-sm-9,
.col-sm-10,
.col-sm-11,
.col-sm-12,
.col-md-1,
.col-md-2,
.col-md-3,
.col-md-4,
.col-md-5,
.col-md-6,
.col-md-7,
.col-md-8,
.col-md-9,
.col-md-10,
.col-md-11,
.col-md-12 {
position: relative;
width: 100%;
min-height: 1px;
padding-right: 12px;
padding-left: 12px;
}
.col {
flex-basis: 0;
flex-grow: 1;
max-width: 100%;
}
.col-auto {
flex: 0 0 auto;
width: auto;
max-width: none;
}
.col-1 {
flex: 0 0 8.33333%;
max-width: 8.33333%;
}
.col-2 {
flex: 0 0 16.66667%;
max-width: 16.66667%;
}
.col-3 {
flex: 0 0 25%;
max-width: 25%;
}
.col-4 {
flex: 0 0 33.33333%;
max-width: 33.33333%;
}
.col-5 {
flex: 0 0 41.66667%;
max-width: 41.66667%;
}
.col-6 {
flex: 0 0 50%;
max-width: 50%;
}
.col-7 {
flex: 0 0 58.33333%;
max-width: 58.33333%;
}
.col-8 {
flex: 0 0 66.66667%;
max-width: 66.66667%;
}
.col-9 {
flex: 0 0 75%;
max-width: 75%;
}
.col-10 {
flex: 0 0 83.33333%;
max-width: 83.33333%;
}
.col-11 {
flex: 0 0 91.66667%;
max-width: 91.66667%;
}
.col-12 {
flex: 0 0 100%;
max-width: 100%;
}
@media (min-width: 576px) {
.col-sm {
flex-basis: 0;
flex-grow: 1;
max-width: 100%;
}
.col-sm-auto {
flex: 0 0 auto;
width: auto;
max-width: none;
}
.col-sm-1 {
flex: 0 0 8.33333%;
max-width: 8.33333%;
}
.col-sm-2 {
flex: 0 0 16.66667%;
max-width: 16.66667%;
}
.col-sm-3 {
flex: 0 0 25%;
max-width: 25%;
}
.col-sm-4 {
flex: 0 0 33.33333%;
max-width: 33.33333%;
}
.col-sm-5 {
flex: 0 0 41.66667%;
max-width: 41.66667%;
}
.col-sm-6 {
flex: 0 0 50%;
max-width: 50%;
}
.col-sm-7 {
flex: 0 0 58.33333%;
max-width: 58.33333%;
}
.col-sm-8 {
flex: 0 0 66.66667%;
max-width: 66.66667%;
}
.col-sm-9 {
flex: 0 0 75%;
max-width: 75%;
}
.col-sm-10 {
flex: 0 0 83.33333%;
max-width: 83.33333%;
}
.col-sm-11 {
flex: 0 0 91.66667%;
max-width: 91.66667%;
}
.col-sm-12 {
flex: 0 0 100%;
max-width: 100%;
}
}
@media (min-width: 768px) {
.col-md {
flex-basis: 0;
flex-grow: 1;
max-width: 100%;
}
.col-md-auto {
flex: 0 0 auto;
width: auto;
max-width: none;
}
.col-md-1 {
flex: 0 0 8.33333%;
max-width: 8.33333%;
}
.col-md-2 {
flex: 0 0 16.66667%;
max-width: 16.66667%;
}
.col-md-3 {
flex: 0 0 25%;
max-width: 25%;
}
.col-md-4 {
flex: 0 0 33.33333%;
max-width: 33.33333%;
}
.col-md-5 {
flex: 0 0 41.66667%;
max-width: 41.66667%;
}
.col-md-6 {
flex: 0 0 50%;
max-width: 50%;
}
.col-md-7 {
flex: 0 0 58.33333%;
max-width: 58.33333%;
}
.col-md-8 {
flex: 0 0 66.66667%;
max-width: 66.66667%;
}
.col-md-9 {
flex: 0 0 75%;
max-width: 75%;
}
.col-md-10 {
flex: 0 0 83.33333%;
max-width: 83.33333%;
}
.col-md-11 {
flex: 0 0 91.66667%;
max-width: 91.66667%;
}
.col-md-12 {
flex: 0 0 100%;
max-width: 100%;
}
}

View File

@ -0,0 +1,166 @@
/**
* LOADING BAR
*
* Markup:
* ---------
* <div class="loadingBar"></div>
*
*/
.loadingBar {
height: 6px;
left: 0;
overflow: hidden;
position: fixed;
right: 0;
top: 0;
width: 100%;
z-index: 1000;
}
.loadingBar::before {
-webkit-animation: loading 2s linear infinite;
animation: loading 2s linear infinite;
background-color: #03a9f4;
content: "";
display: block;
height: 6px;
left: -300px;
position: absolute;
width: 300px;
}
@-webkit-keyframes loading {
from {
left: -300px;
width: 30%;
}
50% {
width: 30%;
}
70% {
width: 70%;
}
80% {
left: 50%;
}
95% {
left: 120%;
}
to {
left: 100%;
}
}
@keyframes loading {
from {
left: -300px;
width: 30%;
}
50% {
width: 30%;
}
70% {
width: 70%;
}
80% {
left: 50%;
}
95% {
left: 120%;
}
to {
left: 100%;
}
}
.spinner {
position: absolute;
right: 0;
top: 0;
width: 40px;
height: 40px;
display: block
}
.spinner:after,
.spinner:before {
position: absolute;
content: "";
top: 50%;
left: 50%;
margin: -12px 0 0 -12px;
width: 24px;
height: 24px;
border-radius: 100%;
border: 3px solid transparent;
border-top-color: var(--blue);
}
.spinner:before {
-webkit-animation: spinning 2.4s cubic-bezier(.41, .26, .2, .62);
animation: spinning 2.4s cubic-bezier(.41, .26, .2, .62);
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite
}
.spinner:after {
-webkit-animation: spinning 2.4s cubic-bezier(.51, .09, .21, .8);
animation: spinning 2.4s cubic-bezier(.51, .09, .21, .8);
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite
}
@-webkit-keyframes spinning {
0% {
-webkit-transform: rotate(0);
transform: rotate(0);
}
25% {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
50% {
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
75% {
-webkit-transform: rotate(270deg);
transform: rotate(270deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spinning {
0% {
-webkit-transform: rotate(0);
transform: rotate(0);
}
50% {
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,143 @@
/**
* TYPOGRAPHY
*/
body {
color: #272727;
font-family: "Lato", sans-serif;
font-size: 16px;
font-weight: 400;
line-height: 1.5em;
}
a {
color: var(--blue);
text-decoration: none;
}
a:hover {
color: color-mix(in srgb, var(--color-primary), #FFF 15%);
}
a:focus {
color: var(--blue);
}
.text-huge {
font-size: 36px;
line-height: 1.3em;
}
.text-big {
font-size: 24px;
line-height: 1.3em;
}
.text-medium {
font-size: 16px;
line-height: 1.5em;
}
.text-small {
font-size: 12px;
line-height: 1.3em;
}
.text-body {
font-size: 16px;
line-height: 1.5em;
}
.text-primary {
color: #03a9f4;
}
.text-dark {
color: #18232f;
}
.text-secondary {
color: #e91e63;
}
.text-white {
color: #fff;
}
.text-success {
color: #4caf50;
}
.text-info {
color: #5bc0de;
}
.text-warning {
color: #f0ad4e;
}
.text-error {
color: #e74c3c;
}
.text-gray {
color: #969da6;
}
.text-gray-light {
color: #eceff1;
}
.text-light {
font-weight: 300;
}
.text-normal {
font-weight: 400;
}
.text-lineThrough {
text-decoration: line-through;
}
.text-italic {
font-style: italic;
}
.text-underline {
text-decoration: underline;
}
.text-uppercase {
text-transform: uppercase;
}
.text-withSubtitle {
margin-bottom: 0 !important;
}
.text-withSubtitle+.text-huge,
.text-withSubtitle+.text-big,
.text-withSubtitle+.text-medium,
.text-withSubtitle+.text-small {
margin-top: 0.5em;
}
h1,
h2,
h3,
h4 {
font-weight: 300;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}

32
src/template/home.pug Normal file
View File

@ -0,0 +1,32 @@
doctype html
include module/head.pug
main#app
.bg-white.section.text-dark
.container.mb-big
.text-center
div
h1.text-huge.text-withSubtitle Open Tourism Map
h2.text-big.text-gray Collaborative Holiday Planner
.spacer
.section.bg-dark.text-white
.container-medium
.row
.col-8
h2.text-big Your journey
p.text-gray
| Browse hotels, restaurants and attractions,....
br
|
| Select and plan the varying elements of your journey
.col-4
.row.align
.input
input#journey.id(v-model="journey.id", placeholder="ID", type="text")
p
.row.align
button.button.button--primary.button--mobileFull(
v-on:click="start_journey"
) Start the journey
include module/foot.pug

8
src/template/journey.pug Normal file
View File

@ -0,0 +1,8 @@
doctype html
include module/head.pug
main#app(v-cloak)
include module/view/toast.pug
include module/journey/main.pug
include module/foot.pug

View File

@ -0,0 +1,13 @@
script(src="https://unpkg.com/leaflet")
script(src="https://unpkg.com/vue@2")
script(src="https://unpkg.com/vue2-datepicker")
script(src="https://unpkg.com/vue2-leaflet")
script(src="https://unpkg.com/sortablejs")
script(src="https://unpkg.com/vuedraggable")
script(src="/public/main.js", type="text/javascript", charset="utf-8")
footer.bg-dark.section
.container.text-center.text-small.text-white
| Built with &nbsp; &#x2764; &nbsp; by Helcel
br
span.text-small.text-gray v0.0.1

View File

@ -8,18 +8,10 @@ head
href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans:400,700,300",
type="text/css"
)
link(rel="stylesheet", href="/public/css/index.css")
link(rel="stylesheet", href="/public/index.css")
link(rel="stylesheet", href="https://unpkg.com/vue2-datepicker/index.css")
link(
rel="stylesheet",
href="https://unpkg.com/vue-multiselect/dist/vue-multiselect.min.css"
)
link(rel="stylesheet", href="https://unpkg.com/leaflet/dist/leaflet.css")
link(
rel="stylesheet",
href="https://unpkg.com/leaflet.awesome-markers/dist/leaflet.awesome-markers.css"
)
link(
rel="stylesheet",
href="https://unpkg.com/@fortawesome/fontawesome-free/css/all.min.css"

View File

@ -0,0 +1,9 @@
.w-100.input()
textarea.text-small#query_note(
v-model="journey.leg_get().notes"
rows="1"
@input="refreshTextAreaHeight"
@focus="refreshTextAreaHeight"
placeholder="...",
)

View File

@ -0,0 +1,50 @@
.input.w-100.text-dark
input#query_input(
type="search"
@input="search_active"
@focus="search_active"
placeholder="Search ... "
style="width:85%;"
:disabled="query.note"
v-model="query.query"
)
.spinner(v-if="query.load")
div(v-if="['hotel', 'restaurant', 'place','other', 'travel'].indexOf(query.type)>=0")
template(v-for="(item, idx) in query.res" )
.query-result.col-12.bg-white.text-dark(
:key="'q'+idx"
@mouseover="drawer_hover_item(item)"
@mouseleave="drawer_hover_item()"
@click="drawer_click_item(item)" )
div( v-html="generate_icon(item, 'var(--dark)')")
.col-10()
| {{ item.name }}
.bg-dark.divider(
:key="'qdiv'+idx" style="height:1px" )
.query-result.col-12.bg-white.text-dark(
v-if="query.load==false && query.res.length>=0 && query.query!=''"
@click="query.addmarker=true" )
div( v-html="generate_icon('star', 'var(--dark)')")
.col-10()
| Add custom
.col-12.text-white.text-center(
) {{query.load? `Loading ...` : `Found ${query.res.length} results`}}
div(v-else-if="['flight'].indexOf(query.type)>=0")
template(v-for="(item, idx) in query.res" )
.query-result.col-12.bg-white.text-dark(
:key="'q'+idx"
@mouseover="drawer_hover_item(item)"
@mouseleave="drawer_hover_item()"
@click="drawer_click_item(item)" )
div( v-html="generate_icon('plane', 'var(--dark)')")
.col-10()
| {{ item.from }} => {{item.to}}
.bg-dark.divider(
:key="'qdiv'+idx" style="height:1px" )
div(v-else)
template()
.query-result.col-12.bg-white.text-dark()
| Unsuppored Query type {{query.type}}

View File

@ -0,0 +1,54 @@
.row
.mr-auto.col-auto
.list-group.text-dark
.list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.day_prev(); focus_day(journey);")
i.fas.fa-angle-left
.col-8.col-sm-8.col-md-10.scroll-handler(
@pointerleave="nav_mouseleave"
@pointermove="nav_mousemove")
draggable.scroll-content.nav-day.list-group.bg-dark(
tag="div",
:list="journey.leg_get().day_title",
handle=".handle"
)
.list-group-item.bg-white.text-white.show.placeholder-left(
:class="journey.sel_day >0? '': 'bg-dark'"
slot="header"
)
.text {{'⋅'}}
.list-group-item.handle.text-dark(
v-for="(element, idx) in journey.leg_len()",
:key="idx",
@click="journey.day_sel(idx)",
:class="journey.sel_day == idx ? 'bg-primary active' : 'bg-white'"
)
.text {{ journey.leg_get().day_title[idx] || "Leg "+idx}}
.list-group-item.bg-white.text-white.show.placeholder-right(
:class="journey.sel_leg < journey.data.main.length-1? '': 'bg-dark'"
slot="footer"
)
.text {{'⋅'}}
.list-group-item.bg-dark.text-white(
style="display:none"
slot="footer"
)
.text {{'⋅'}}
.ml-auto.col-auto
.list-group.text-dark
.list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.day_next(); focus_day(journey);")
i.fas.fa-angle-right
.row
.col-8.col-sm-6.col-md-4
.input.w-100
input.col-8(
placeholder="Day"
v-model="journey.leg_get().day_title[journey.sel_day]"
)
.ml-auto.col-4.col-sm-4.col-md-3
.input.w-100
input.text-right(
disabled="",
:value="journey.date_sel() + ' (' + journey.sel_day + ')'"
)

View File

@ -0,0 +1,61 @@
.row
.mr-auto.col-auto
.list-group.text-dark
.list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.leg_prev(); focus_leg(journey);")
i.fas.fa-angle-left
.col-8.col-sm-8.col-md-10.scroll-handler(
@pointerleave="nav_mouseleave"
@pointermove="nav_mousemove")
draggable.scroll-content.nav-leg.list-group.bg-dark(
tag="div",
:list="journey.data.main",
handle=".handle"
)
.list-group-item.bg-white.text-white.show.placeholder-left(
:class="journey.sel_leg >0? '': 'bg-dark'"
slot="header"
)
.text {{'⋅'}}
.list-group-item.handle.text-dark(
v-for="(element, idx) in journey.data.main",
:key="idx",
@click="journey.leg_sel(idx)",
:class="journey.sel_leg == idx ? 'bg-primary active' : 'bg-white'"
)
.text {{ element.title || "Leg "+idx}}
i.fa.fa-times.text-small.fright(
style="top: 2px; right: 2px; position: absolute",
@click="journey.rm_leg(idx); focus_leg(journey);"
)
.list-group-item.bg-white.text-white.show.placeholder-right(
:class="journey.sel_leg < journey.data.main.length-1? '': 'bg-dark'"
slot="footer"
)
.text {{'⋅'}}
.list-group-item.bg-white.text-dark.add(@click="journey.add_leg(); focus_leg(journey);"
slot="footer"
:class="journey.sel_leg >= journey.data.main.length-1? 'show': ''")
div
i.fa.fa-plus()
.ml-auto.col-auto
.list-group.text-dark
.list-group-item.show.bg-white.rounded(v-on:click.prevent="journey.leg_next(); focus_leg(journey);")
i.fas.fa-angle-right
.row.text-center
.col-6.col-sm-6.col-md-5.mr-auto
.input.w-100
input(
placeholder="Leg"
v-model="journey.leg_get().title")
.col-6.col-sm-6.col-md-4
.input.w-100
date-picker(
:lang="lang",
v-model="journey.leg_get().date_range",
range="",
format="ddd D MMM",
placeholder="Date Range",
v-on:change="journey.date_update(journey.sel_leg)"
)

View File

@ -0,0 +1,25 @@
include smallbar.pug
.bg-dark.text-white(v-if="journey && journey.leg_get()")
.container
.row.align(style="padding-top:45px;position:relative")
.col-6.col-sm-4.col-md-3
.input.text-big
input.text-center(v-model="journey.data.name" placeholder="My Journey" type="text")
.col-6.col-sm-4.col-md-3( v-if="useDay" )
.input.text-small
input.text-center(v-model="journey.leg_get().title" placeholder="Leg" type="text")
//- input.small(type="text", :placeholder="journey.date_tot() + ' (' + journey.tot_len() + ')'" )
template(v-if="useDay")
include leg/top_day.pug
template(v-else)
include leg/top_leg.pug
.row(style="aspect-ratio:1.25;min-height:432px;max-height:calc(100vh - 125px);width:calc(100% + 24px);")
.map-container(:class=" { 'col-2 col-sm-5 col-md-8': query.type, 'col-2 col-sm-5 col-md-6': query.note , 'col-12': (!query.type && !query.note) }" )
include map.pug
.row.drawer-container(:class="{ 'col-10 col-sm-7 col-md-4': query.type, 'col-10 col-sm-7 col-md-6': query.note, 'col-0': (!query.type && !query.note) }")
.h-100(:class="{ 'w-100 ': query.type, 'w-0': !query.type }")
include leg/drawer.pug
.h-100(:class="{ 'w-100': query.note, 'w-0': !query.note }")
include leg/drawer-notes.pug

View File

@ -0,0 +1,23 @@
l-map(
:zoom.sync="journey.leg_get().map.zoom"
:center.sync="journey.leg_get().map.center"
@click="onMapClick"
style="height:100%"
no-blocking-animations=true
:style="query.addmarker?'cursor:crosshair':''"
ref="map"
)
l-tile-layer(
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution="© <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors"
)
l-control-scale(position="bottomleft" :imperial="false" :metric="true")
include map/override.pug
include map/hotel.pug
include map/activities.pug
include map/restaurants.pug
include map/travel.pug
template(v-if="edit_active")
include map/right_menu.pug

View File

@ -0,0 +1,6 @@
include mixin-marker.pug
div(
v-for="(place, index) in journey.leg_get().places.activities",
:key="'activities'+index",
)
+map_marker("activities", "var(--lightdark)", "var(--light)", "var(--lightlight)")

View File

@ -0,0 +1,7 @@
include mixin-marker.pug
div(
v-if="journey.leg_get().hotel",
v-for="(place, index) in [journey.leg_get().hotel]",
:key="'hotel'+index",
)
+map_marker("hotel", "var(--darkdark)", "var(--darkdark)", "var(--darkdark)")

View File

@ -0,0 +1,37 @@
mixin map_marker(place, color_sel_c, color_sel_o, color_else)
l-marker(
:lat-lng="place.latlon"
)
l-icon(
v-if="(place.step == journey.sel_day)"
v-html="generate_marker(place, \""+color_sel_c+"\")"
)
l-icon(
v-else-if="(place.step >=0)"
v-html="generate_marker(place, \""+color_sel_o+"\")"
)
l-icon(
v-else
v-html="generate_marker(place, \""+color_else+"\")"
)
l-popup(
:options="{maxWidth:400, minWidth:300}")
h1.row.text-medium.text-center {{ place.sname }}
span.row.text-small.text-gray {{ place.display_name }}
span(v-if="edit_active")
.row.input()
textarea.col-12.col-sm-12.text-small(
placeholder="",
v-model="place.notes",
)
.leaflet-popup-button-group(v-if="edit_active")
a.text-gray(
v-on:click.prevent="place.step = ((place.step==journey.sel_day)?-1:journey.sel_day)"
v-html="generate_icon(((place.step==journey.sel_day)?'calendar-xmark':'calendar-plus'), 'NA')"
)
a.text-gray(
v-on:click.prevent="place_delete(\""+place+"\",index)"
v-html="generate_icon('trash', 'NA')"
)
span.row.text-small.text-dark(v-else) {{ place.notes }}

View File

@ -0,0 +1,11 @@
l-marker(
v-if="map_override.active",
v-for="(el, idx) in map_override.elements"
:key="'ovr'+idx"
:lat-lng="el"
)
l-icon(v-html="generate_marker('plus', 'darkgreen')")
l-polyline(
v-if="map_override.active && map_override.elements.length>1"
:lat-lngs="map_override.elements" :color="'darkgreen'"
)

View File

@ -0,0 +1,6 @@
include mixin-marker.pug
div(
v-for="(place, index) in journey.leg_get().places.restaurants",
:key="'restaurants'+index",
)
+map_marker("restaurants", "var(--dark)", "var(--dark)", "var(--dark)")

View File

@ -0,0 +1,21 @@
.map-menu.map-menu-top
div(v-if="query.type" @click="drawer_click_item()" )
.map-menu-item(v-html="generate_icon('close')")
div(v-if="!query.type" @click="search_enable('hotel')")
.map-menu-item( v-html="generate_icon('bed')")
div(v-if="!query.type" @click="search_enable('restaurant')")
.map-menu-item( v-html="generate_icon('utensils')")
div(v-if="!query.type" @click="search_enable('place')")
.map-menu-item( v-html="generate_icon('star')")
.map-menu-sub(v-if="!query.type" @mouseenter="query.sub=true" @mouseleave="query.sub=false" )
.map-menu-item(v-html="generate_icon('route')")
.map-menu-item(v-if="query.sub" @click="search_enable('flight')" v-html="generate_icon('plane')")
.map-menu-item(v-if="query.sub" @click="search_enable('train')" v-html="generate_icon('train')")
.map-menu-item(v-if="query.sub" @click="search_enable('car')" v-html="generate_icon('car')")
.map-menu-item(v-if="query.sub" @click="search_enable('other')" v-html="generate_icon('person-biking')")
.map-menu.map-menu-center
div(v-if="query.note" @click="drawer_click_item()" )
.map-menu-item(v-html="generate_icon('close')")
div(v-if="!query.note" @click="search_enable('notes')")
.map-menu-item( v-html="generate_icon('pencil')")

View File

@ -0,0 +1,32 @@
mixin flight_popup()
l-popup(
:options="{maxWidth:400, minWidth:300}"
)
h1.row.text-medium.text-center.text-uppercase {{ travel.id }}
span.row.text-small.text-gray {{ travel.from }} - {{travel.to}}
span(v-if="edit_active")
.row.input(style="margin-bottom:0")
textarea.col-12.col-sm-12.text-small(
placeholder="",
v-model="travel.notes",
)
span.row.text-small.text-dark(v-else) {{ travel.notes }}
span(v-if="edit_active")
.leaflet-popup-button-group(v-if="edit_active")
a.text-gray(
v-on:click.prevent="place_delete('flight',idx)"
v-html="generate_icon('trash', 'NA')"
)
div(v-for= "(travel, idx) in journey.leg_get().travel")
l-polyline(:lat-lngs="travel.path" :color="travel.color || 'gray'")
+flight_popup()
l-marker(
v-for="(place, index) in travel.path"
:key="'plane'+index"
:lat-lng="place"
)
l-icon(v-html="generate_icon('plane', travel.color || 'gray', generate_rotation(index,travel.path), 'travel-path-icon')"
)
+flight_popup()

View File

@ -0,0 +1,30 @@
.smallbar
.col-auto.mr-auto(style="display:flex; flex-flow: row")
.col-1
a.text-white.tooltip(:href="'/short/' + journey.id")
i.fas.fa-file-contract
.tooltip-text Summary
.col-1
a.text-white.tooltip(:href="'/view/' + journey.id")
i.fas.fa-camera
.tooltip-text Daily
.col-1
a.text-white.tooltip(href="#", v-on:click.prevent="first_step")
i.fas.fa-tools
.tooltip-text Editor
.col-1
.col-1
a.text-white.tooltip(v-on:click.prevent="toast_impexp(true)")
i.fas.fa-file-import
.tooltip-text Import
.col-1
a.text-white.tooltip(v-on:click.prevent="toast_impexp(false)")
i.fas.fa-file-export
.tooltip-text Export
.col-auto.ml-auto
.switch.legday-switch
input(type="checkbox" v-model="useDay")
.rocker
.rocker-left LEG
.rocker-right DAY

View File

@ -0,0 +1,10 @@
.row.fleft(style="position:absolute;right:0;")
.col-1
a(:href="'/short/' + journey.id")
i.fas.fa-file-contract
.col-1
a(:href="'/view/' + journey.id")
i.fas.fa-camera
//- .col-1
//- a(:href="'/' + journey.id" v-on:click.prevent="first_step")
//- i.fas.fa-tools

View File

@ -0,0 +1,39 @@
.col-11.container.section
.row.text-center.align.padding-1
.input.col-5.col-sm-2
input(disabled="", placeholder="Unnamed" :value="item.title")
.col-sm-1
.input.col-6.col-sm-4
input(
disabled="",
placeholder="No Dates",
:value="item.date_range ? item.date_range[0].toLocal() + ' - ' + item.date_range[1].toLocal() : ''"
)
.col-1.col-sm-2
.input.col-5.col-sm-3.text-dark
.text(disabled="" placeholder="No Hotel" :value="item.hotel?item.hotel.sname:''") {{(item.hotel?item.hotel.sname:'') || 'No Hotel'}}
//- .row.text-center
.input.col-sm-3(v-if="item.travel")
div(v-for="(item, idx) in item.travel")
input(disabled="", placeholder="-" :value="item.map((v) => v.id).join(', ')")
.row.align.padding-1
.input.col-sm-10.text-dark
.text-small(
placeholder="No Restaurants",
:value="item.places.restaurants.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Restaurants'",
disabled=""
) {{item.places.restaurants.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Restaurants'}}
.row.align.padding-1
.input.col-sm-10.text-dark
.text-small(
placeholder="No Activities",
:value="item.places.activities.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Activites'",
disabled=""
) {{item.places.activities.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ') || 'No Activites'}}
.row.align.padding-1
.input.col-sm-10.text-dark
.text-small(
placeholder="No Notes",
:value="item.notes || 'No Notes'",
disabled=""
) {{item.notes || 'No Notes'}}

View File

@ -0,0 +1,14 @@
template
.overlay.text-dark(v-if="toast.show" @click="")
.popup.bg-white(@click.stop)
.row
.col-auto.text-huge {{ toast.title }}
.row(v-if="toast.desc")
.col-auto.text-medium {{ toast.desc }}
.row(v-if="toast.special")
.col-auto.text-medium
.input
input(v-model="toast.special")
.row.align
button.button.bg-white(@click="toast.func" v-if="toast.acceptText") {{ toast.acceptText }}
button.button.bg-white(@click="toast.show=false;" v-if="toast.cancelText") {{ toast.cancelText }}

View File

@ -0,0 +1,19 @@
div(v-for="(e, idx) in journey.data.main", :key="idx")
.bg-dark.text-white(v-if="journey.sel_leg == idx")
.container.section
.row
.col-3.fleft.text-center.text-white.text-huge
a(v-on:click.prevent="journey.day_prev()")
i.fas.fa-angle-left
.col-6.container.text-center.align
span.small {{ journey.data.main[idx].title }} {{ journey.sel_day }}
.text-big.text-gray {{ journey.data.main[idx].day_title[journey.sel_day] }}
.col-3.fright.text-center.text-white.text-huge
a(v-on:click.prevent="journey.day_next()")
i.fas.fa-angle-right
.row
.col-12.col-sm-12(style="aspect-ratio:1.25;")
include ../journey/map.pug
.row
.col-10
span.small.text-gray {{journey.data.main[idx].note || '...'}}

16
src/template/short.pug Normal file
View File

@ -0,0 +1,16 @@
doctype html
include module/head.pug
main#app(v-cloak)
include module/view/nav.pug
.bg-dark.text-white(v-if="journey && journey.leg_get()")
.container
.row.align(style="padding:45px;")
.col-7.col-sm-5.col-md-4.input.text-big
input.text-center(v-model="journey.data.name" placeholder="My Journey" type="text" disabled)
div(
v-for="(item, idx) in journey.data.main",
:class="idx % 2 === 0 ? 'bg-white text-dark' : 'bg-dark text-white'"
)
include module/view/short_leg.pug
include module/foot.pug

6
src/template/view.pug Normal file
View File

@ -0,0 +1,6 @@
doctype html
include module/head.pug
main#app(v-cloak)
div(v-if="journey.leg_get()")
include module/view/view_day.pug
include module/foot.pug

View File

@ -1,77 +0,0 @@
doctype html
include module/head.pug
main#app
.container
section.mb-big
.text-center
img.main-logo.mb-medium(
src="/public/img/helcel.png",
alt="Helcel logo"
)
div
h1.text-huge.text-withSubtitle Open Tourism Map
h2.text-big.text-gray Collaborative Holiday Planner
p#js-header-waypoint.m-none
a.button.button--primary.button--mobileFull(href="#go") Get started
.bg-dark
.container
.row.text-center
.col-12.col-sm-3
.section
img(
src="/public/img/lightweight.png",
alt="Lightweight",
width="118"
)
br
h2.text-withSubtitle.text-big.text-white
| Lightweight
br
span.text-medium.text-gray
| Powered By
br
| Fastify &amp; Sierra
.col-12.col-sm-4
.section
img(
src="/public/img/customizable.png",
alt="Customizable",
width="118"
)
br
h2.text-withSubtitle.text-big.text-white
| Customizable
br
span.text-medium.text-gray
| Many Templates
br
| to choose from
.col-12.col-sm-4
.section
h2.text-withSubtitle.text-big.text-white
img(
src="/public/img/opensource.png",
alt="Open Source",
width="118"
)
br
|
| FOSS
br
span.text-medium.text-gray :-)
#go.container-medium.section
h2.text-big Your journey
p
| Browse hotels, restaurants and attractions,....
br
|
| Select and plan the varying elements of your journey
.aligner.aligner--contentEnd
.input
input#journey_id(v-model="journey_id", placeholder="ID", type="text")
button.button.button--primary.button--mobileFull(
v-on:click="start_journey"
) Start the journey
include module/foot.pug

View File

@ -1,10 +0,0 @@
doctype html
include module/head.pug
main#app(v-cloak)
include module/nav.pug
include module/journey_sec.pug
include module/journey_step.pug
include module/importexport.pug
include module/foot.pug

View File

@ -1,31 +0,0 @@
script(src="https://unpkg.com/vue@2/dist/vue.min.js")
script(src="https://unpkg.com/vue2-datepicker/index.min.js")
script(src="https://unpkg.com/leaflet/dist/leaflet.js")
script(src="https://unpkg.com/vue2-leaflet")
script(
src="https://unpkg.com/leaflet.awesome-markers/dist/leaflet.awesome-markers.min.js"
)
script(src="https://unpkg.com/axios/dist/axios.min.js")
script(src="https://unpkg.com/lodash")
script(src="https://unpkg.com/vue-multiselect")
script(
src="https://unpkg.com/vue-textarea-autosize/dist/vue-textarea-autosize.umd.min.js"
)
script(src="https://unpkg.com/sortablejs/Sortable.min.js")
script(src="https://unpkg.com/vuedraggable/dist/vuedraggable.umd.min.js")
script(src="/public/js/main.js", type="text/javascript", charset="utf-8")
footer.bg-dark
.container
.section.text-center.text-small
p.text-white
img(src="/public/img/helcel.png", alt="helcel logo", width="100")
br
br
|
| Built with &nbsp; &#x2764; &nbsp; by Helcel
br
span.text-small.text-gray v0.0.1
p.text-gray
a(href="https://git.helcel.net") Helcel Git

View File

@ -1,13 +0,0 @@
div
.container-medium.section
.aligner
.input.col-sm-4
input(v-model="impexp", type="text")
.col-sm-2
button.button.button--primary.button--mobileFull(
v-on:click="import_data"
) Import
.col-sm-2
button.button.button--primary.button--mobileFull(
v-on:click="export_data"
) Export

View File

@ -1,20 +0,0 @@
draggable.list-group.bg-dark(
tag="div",
:list="journey_data.main",
handle=".handle"
)
.list-group-item.handle(
v-for="(element, idx) in journey_data.main",
:key="idx",
@click="sel_section(idx)",
:class="journey_step_data.section == idx ? 'bg-primary' : 'bg-white'"
)
.text {{ element.title }}
i.fa.fa-times.close.fright(
style="top: 2px; right: 2px; position: absolute",
@click="rm_section(idx)"
)
.list-group-item.bg-white(@click="add_section()")
.text Add Section
i.fa.fa-plus.add(style="top: 12px; right: 5px; position: absolute")

View File

@ -1,102 +0,0 @@
div(v-for="(e, idx) in journey_data.main", :key="idx")
.bg-dark.text-white(v-if="journey_step_data.section == idx")
.container.section
.row.text-center
.input.col-sm-2
input(v-model="journey_data.main[idx].title")
.input.col-sm-2
input(
placeholder="Day title",
v-model="journey_data.main[idx].step_title[journey_step_data.day]"
)
.col-sm-3
.right.input.col-sm-2
input(
disabled="",
:value="active_date() + ' (' + journey_step_data.day + ')'"
)
.row
.col-9.col-ssm-12
include map.pug
.col-3.col-ssm-12
.row.text-center
div
label Date Range ({{ step_len(idx) }})
.input.text-dark
date-picker(
:lang="lang",
v-model="journey_data.main[idx].dateRange",
range="",
format="ddd D MMM",
placeholder="Date Range",
v-on:change="update_date(idx)"
)
.row.text-center
div
label Hotel
multiselect#ajax(
v-model="journey_data.main[idx].hotel",
label="sname",
track-by="place_id",
placeholder="Type to search",
open-direction="bottom",
:options="query.nominatim",
:searchable="true",
:loading="querying.hotel",
:internal-search="false",
:clear-on-select="false",
:options-limit="50",
:limit="1",
:max-height="600",
@search-change="debounceSearch.hotel"
)
.row.text-center
div
label Restoration
multiselect#ajax(
v-model="journey_data.main[idx].places.restaurants",
label="sname",
track-by="place_id",
placeholder="Type to search",
open-direction="bottom",
:multiple="true",
:options="query.nominatim",
:searchable="true",
:loading="querying.food",
:internal-search="false",
:clear-on-select="false",
:options-limit="50",
:limit="10",
:max-height="600",
@search-change="debounceSearch.restaurants"
)
.row.text-center
div
label Activities
multiselect#ajax(
v-model="journey_data.main[idx].places.activities",
label="sname",
track-by="place_id",
placeholder="Type to search",
open-direction="bottom",
:multiple="true",
:options="query.nominatim",
:searchable="true",
:loading="querying.place",
:internal-search="false",
:clear-on-select="false",
:options-limit="50",
:limit="10",
:max-height="600",
@search-change="debounceSearch.places"
)
.row.text-center
div
label Notes
.input.text-dark(style="width: 100%")
textarea-autosize.text-small(
v-model="journey_data.main[idx].notes",
placeholder="Notes",
:min-height="30",
:max-height="350"
)

View File

@ -1,84 +0,0 @@
l-map(
:zoom.sync="journey_data.main[idx].map.zoom",
:center.sync="journey_data.main[idx].map.center",
style="padding-top: 100%"
)
l-tile-layer(
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution="© <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors"
)
l-control-scale(position="topright", :imperial="false", :metric="true")
l-marker(
v-if="journey_data.main[idx].hotel",
:lat-lng="journey_data.main[idx].hotel.latlon"
)
l-icon
div(v-html="generate_icon(journey_data.main[idx].hotel, 'darkblue')")
l-popup
h1.row.text-medium.text-center {{ journey_data.main[idx].hotel.sname }}
span.row.text-small.text-gray {{ journey_data.main[idx].hotel.display_name }}
span(v-if="journey_edit")
.row.input
textarea-autosize.col-12.col-sm-12.text-small(
placeholder="Notes",
v-model="journey_data.main[idx].hotel.notes",
:min-height="30",
:max-height="350"
)
span.row.text-small.text-white(v-else) {{ journey_data.main[idx].hotel.notes }}
l-marker(
v-for="place in journey_data.main[idx].places.activities",
:lat-lng="place.latlon"
)
l-icon
div(
v-if="place.step == journey_step_data.day",
v-html="generate_icon(place)"
)
div(
v-else-if="place.step == -1 || place.step == undefined",
v-html="generate_icon(place, 'gray')"
)
div(v-else-if="journey_edit", v-html="generate_icon(place, 'lightgray')")
div(v-else)
l-popup
h1.row.text-medium.text-center {{ place.sname }}
span.row.text-small.text-gray {{ place.display_name }}
span(v-if="journey_edit")
.row.input
textarea-autosize.col-12.col-sm-12.text-small(
placeholder="Notes",
v-model="place.notes",
:min-height="30",
:max-height="350"
)
a.leaflet-popup-close-button.text-gray(
style="right: 0px; visibility: visible",
href="#rm",
v-on:click.prevent="place.step = -1"
) -
a.leaflet-popup-close-button.text-gray(
style="right: 16px; visibility: visible",
href="#ad",
v-on:click.prevent="place.step = journey_step_data.day"
) +
span.row.text-small.text-dark(v-else) {{ place.notes }}
l-marker(
v-for="place in journey_data.main[idx].places.restaurants",
:lat-lng.sync="place.latlon"
)
l-icon
div(v-html="generate_icon(place, 'cadetblue')")
l-popup
h1.row.text-medium.text-center {{ place.sname }}
span.row.text-small.text-gray {{ place.display_name }}
span(v-if="journey_edit")
.row.input
textarea-autosize.col-12.col-sm-12.text-small(
placeholder="Notes",
v-model="place.notes",
:min-height="30",
:max-height="350"
)
span.row.text-small.text-dark(v-else) {{ place.notes }}

View File

@ -1,38 +0,0 @@
header.header
.header-inner.container
a.header-logo.text-dark(href="/")
img.header-logoImage(
src="/public/img/helcel.png",
alt="Helcel logo",
width="40"
)
span.hide-small OTM
.input.input-invis.row
input.col-6.small(v-model="journey_data.name", type="text")
input.col-6.small(
disabled,
type="text",
:placeholder="total_date() + ' (' + total_days() + ')'"
)
.row.header-nav.text-big(style="margin-bottom: 0")
.col-sm-2
a(:href="'/short/' + journey_id")
i.fas.fa-file-contract
.col-sm-2
a(:href="'/view/' + journey_id")
i.fas.fa-camera
.col-sm-2
a(href="#main", v-on:click.prevent="first_step")
i.fas.fa-tools
.col-sm-1.text-small
a(href="#prevprev", v-on:click.prevent="prevprev_step")
i.fas.fa-angle-double-left
.col-sm-1
a(href="#prev", v-on:click.prevent="prev_step")
i.fas.fa-angle-left
.col-sm-1
a(href="#next", v-on:click.prevent="next_step")
i.fas.fa-angle-right
.col-sm-1.text-small
a(href="#nextnext", v-on:click.prevent="nextnext_step")
i.fas.fa-angle-double-right

View File

@ -1,18 +0,0 @@
header.header
.header-inner.container
a.header-logo.text-dark(href="/")
img.header-logoImage(
src="/public/img/helcel.png",
alt="Helcel logo",
width="40"
)
span.hide-small HOTM
.input.input-invis
input.small(:value="journey_data.name", type="text", disabled="")
.row.header-nav.text-big(style="margin-bottom: 0")
.col-sm-3
a(:href="'/short/' + journey_id")
i.fas.fa-file-contract
.col-sm-3
a(:href="'/view/' + journey_id")
i.fas.fa-camera

View File

@ -1,43 +0,0 @@
.container.section
.row.text-center
.input.col-sm-2
input(disabled="", :value="item.title")
.input.col-sm-4
input(
disabled="",
placeholder="No Dates",
:value="item.dateRange ? format_date(item.dateRange[0]) + ' - ' + format_date(item.dateRange[1]) : ''"
)
.input.col-sm-2
input(disabled="", placeholder="No Hotel", :value="item.hotel.sname")
.row.text-center
.input.col-sm-3(v-if="item.transit")
div(v-for="(item, idx) in item.transit")
input(disabled="", :value="item.map((v) => v.id).join(', ')")
.row.text-center
.input.col-sm-8(v-if="item.places && item.places.restaurants")
textarea-autosize.text-small(
placeholder="No Restaurants",
:value="item.places.restaurants.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ')",
:min-height="30",
:max-height="350",
disabled=""
)
.row.text-center
.input.col-sm-8(v-if="item.places && item.places.activities")
textarea-autosize.text-small(
placeholder="No Activities",
:value="item.places.activities.map((v) => v.sname + (v.notes ? '(' + v.notes + ')' : '')).join(', ')",
:min-height="30",
:max-height="350",
disabled=""
)
.row.text-center
.input.col-sm-8(v-if="item.notes")
textarea-autosize.text-small(
placeholder="No Notes",
:value="item.notes",
:min-height="30",
:max-height="350",
disabled=""
)

View File

@ -1,19 +0,0 @@
div(v-for="(e, idx) in journey_data.main", :key="idx")
.bg-dark.text-white(v-if="journey_step_data.section == idx")
.container.section
.aligner.text-center.text-white.text-huge(style="margin-bottom: 5px")
.aligner--itemTop.fleft
a(href="#prev", v-on:click.prevent="prev_step")
i.fas.fa-angle-left
span.container
span.small {{ journey_data.main[idx].title }} {{ journey_step_data.day }}
.text-big.text-gray {{ journey_data.main[idx].step_title[journey_step_data.day] }}
.aligner--itemEnd.fright
a(href="#next", v-on:click.prevent="next_step")
i.fas.fa-angle-right
.row
.col-12.col-sm-12
include map.pug
.row
.col-12.col-sm-12
.container

View File

@ -1,10 +0,0 @@
doctype html
include module/head.pug
main#app(v-cloak)
include module/nav_pub.pug
div(
v-for="(item, idx) in journey_data.main",
:class="idx % 2 === 0 ? 'bg-dark text-white' : ''"
)
include module/short_sec.pug
include module/foot.pug

View File

@ -1,6 +0,0 @@
doctype html
include module/head.pug
main#app(v-cloak)
div(v-if="journey_data.main[journey_step_data.section] != undefined")
include module/view_step.pug
include module/foot.pug

17
tsconfig-client.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "esnext",
"typeRoots": [
"./node_modules/@types",
"./types/ext"
],
"lib": [
"esnext",
"DOM"
],
"noEmit": true, // Disable emitting output (use esbuild to handle this)
"skipLibCheck": true, // Skip type checking of all declaration files (*.d.ts)
"strict": false, // Disable strict type checks if needed
"moduleResolution": "node",
}
}

1291
yarn.lock

File diff suppressed because it is too large Load Diff