1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-08-04 20:55:19 +02:00

Merge branch 'development' into main

This commit is contained in:
Sean Morley 2025-02-08 10:14:11 -05:00 committed by GitHub
commit 57db1e088f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 2113 additions and 1152 deletions

View file

@ -16,7 +16,6 @@
"@event-calendar/day-grid": "^3.7.1",
"@event-calendar/time-grid": "^3.7.1",
"@iconify-json/mdi": "^1.1.67",
"@sveltejs/adapter-auto": "^3.2.2",
"@sveltejs/adapter-node": "^5.2.0",
"@sveltejs/adapter-vercel": "^5.4.1",
"@sveltejs/kit": "^2.8.3",
@ -47,7 +46,6 @@
"psl": "^1.15.0",
"qrcode": "^1.5.4",
"svelte-i18n": "^4.0.1",
"svelte-maplibre": "^0.9.8",
"tsparticles": "^3.7.1"
"svelte-maplibre": "^0.9.8"
}
}

398
frontend/pnpm-lock.yaml generated
View file

@ -35,9 +35,6 @@ importers:
svelte-maplibre:
specifier: ^0.9.8
version: 0.9.8(svelte@4.2.19)
tsparticles:
specifier: ^3.7.1
version: 3.7.1
devDependencies:
'@event-calendar/core':
specifier: ^3.7.1
@ -51,9 +48,6 @@ importers:
'@iconify-json/mdi':
specifier: ^1.1.67
version: 1.1.67
'@sveltejs/adapter-auto':
specifier: ^3.2.2
version: 3.2.2(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))
'@sveltejs/adapter-node':
specifier: ^5.2.0
version: 5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))
@ -741,11 +735,6 @@ packages:
cpu: [x64]
os: [win32]
'@sveltejs/adapter-auto@3.2.2':
resolution: {integrity: sha512-Mso5xPCA8zgcKrv+QioVlqMZkyUQ5MjDJiEPuG/Z7cV/5tmwV7LmcVWk5tZ+H0NCOV1x12AsoSpt/CwFwuVXMA==}
peerDependencies:
'@sveltejs/kit': ^2.0.0
'@sveltejs/adapter-node@5.2.0':
resolution: {integrity: sha512-HVZoei2078XSyPmvdTHE03VXDUD0ytTvMuMHMQP0j6zX4nPDpCcKrgvU7baEblMeCCMdM/shQvstFxOJPQKlUQ==}
peerDependencies:
@ -785,147 +774,6 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || insiders'
'@tsparticles/basic@3.7.1':
resolution: {integrity: sha512-oJMJ3qzYUROYaOEsaFXkVynxT2OTWBXbQ9MNc1bJi/bVc1VOU44VN7X/KmiZjD+w1U+Qalk6BeVvDRwpFshblw==}
'@tsparticles/engine@3.7.1':
resolution: {integrity: sha512-GYzBgq/oOE9YJdOL1++MoawWmYg4AvVct6CIrJGx84ZRb3U2owYmLsRGabYl0qX1CWWOvUG569043RJmyp/vQA==}
'@tsparticles/interaction-external-attract@3.7.1':
resolution: {integrity: sha512-cpnMsFJ7ZJNKccpQvskKvSs1ofknByHE6FGqbEb17ij7HqvbECQOCOVKHPFnYipHe14cXor/Cd+nVisRcTASoQ==}
'@tsparticles/interaction-external-bounce@3.7.1':
resolution: {integrity: sha512-npvU9Qt6WDonjezHqi+hWM44ga2Oh5yXdr8eSoJpvuHZrCP7rIdRSz5XseHouO1bMS9DbXk86sx4qwrhB5w58w==}
'@tsparticles/interaction-external-bubble@3.7.1':
resolution: {integrity: sha512-WdbYL46lMfuf2g5kfVB1hhhxRBtEXDvnwz8PJwLKurSThL/27bqsqysyXsMzXtLByXUneGhtJTj4D5I5RYdgjA==}
'@tsparticles/interaction-external-connect@3.7.1':
resolution: {integrity: sha512-hqx0ANIbjLIz/nxmk0LvqANBiNLLmVybbCA7N+xDHtEORvpKjNlKEvMz6Razocl6vRjoHZ/olSwcxIG84dh/cg==}
'@tsparticles/interaction-external-grab@3.7.1':
resolution: {integrity: sha512-JMYpFW+7YvkpK5MYlt4Ec3Gwb5ZxS7RLVL8IRUSd5yJOw25husPTYg+FQywxrt5WhKe+tPsCAYo+uGIbTTHi9w==}
'@tsparticles/interaction-external-pause@3.7.1':
resolution: {integrity: sha512-Kkp+7sCe24hawH0XvS1V6UCCuHfMvpLK7oseqSam9Gt4SyGrFvaqIXxkjXhRhn9MysJyKFPBV4/dtBM1HR9p6A==}
'@tsparticles/interaction-external-push@3.7.1':
resolution: {integrity: sha512-4VoaR5jvXgQdB7irtq4uSZYr5c+D6TBTVEnLVpBfJhUs6jhw6mgN5g7yp5izIYkK0AlcO431MHn8dvJacvRLDw==}
'@tsparticles/interaction-external-remove@3.7.1':
resolution: {integrity: sha512-FRBW7U7zD5MkO6/b7e8iSMk/UTtRLY2XiIVFZNsKri3Re3yPpvZzzd5tl2YlYGQlg1Xc+K8SJYMQQA3PtgQ/Tg==}
'@tsparticles/interaction-external-repulse@3.7.1':
resolution: {integrity: sha512-mwM06dVmg2FEvHMQsPOfRBQWACbjf3qnelODkqI9DSVxQ0B8DESP4BYNXyraFGYv00YiPzRv5Xy/uejHdbsQUA==}
'@tsparticles/interaction-external-slow@3.7.1':
resolution: {integrity: sha512-CfCAs3kUQC3pLOj0dbzn5AolQyBHgjxORLdfnYBhepvFV1BXB+4ytChRfXBzjykBPI6U+rCnw5Fk/vVjAroSFA==}
'@tsparticles/interaction-external-trail@3.7.1':
resolution: {integrity: sha512-M7lNQUWP15m8YIDP/JZcZAXaVJLqdwpBs0Uv9F6dU6jsnNXwwHFVFZ+1icrnlbgl9k/Ehhodbdo5weE7GHhQhQ==}
'@tsparticles/interaction-particles-attract@3.7.1':
resolution: {integrity: sha512-UABbBORKaiItAT8vR0t4ye2H3VE6/Ah4zcylBlnq0Jd5yDkyP4rnkwhicaY6y4Zlfwyx+0PWdAC4f/ziFAyObg==}
'@tsparticles/interaction-particles-collisions@3.7.1':
resolution: {integrity: sha512-0GY9++Gn2KXViyeifXWkH7a2UO5+uRwyS1rDeTN8eleyiq2j9zQf4xztZEIft8T0hTetq2rkWxQ92j2kev6NVA==}
'@tsparticles/interaction-particles-links@3.7.1':
resolution: {integrity: sha512-BxCXAAOBNmEvlyOQzwprryW8YdtMIop2v4kgSCff5MCtDwYWoQIfzaQlWbBAkD9ey6BoF8iMjhBUaY1MnDecTA==}
'@tsparticles/move-base@3.7.1':
resolution: {integrity: sha512-LPtMHwJHhzwfRIcSAk814fY9NcRiICwaEbapaJSYyP1DwscSXqOWoyAEWwzV9hMgAcPdsED6nGeg8RCXGm58lw==}
'@tsparticles/move-parallax@3.7.1':
resolution: {integrity: sha512-B40azo6EJyMdI+kmIxpqWDaObPwODTYLDCikzkZ73n5tS6OhFUlkz81Scfo+g1iGTdryKFygUKhVGcG1EFuA5g==}
'@tsparticles/plugin-absorbers@3.7.1':
resolution: {integrity: sha512-3s+fILLV1tdKOq/bXwfoxFVbzkWwXpdWTC2C0QIP6BFwDSQqV5txluiLEf7SCf8C5etQ6dstEnOgVbdzK7+eWA==}
'@tsparticles/plugin-easing-quad@3.7.1':
resolution: {integrity: sha512-nSwKCRe6C/noCi3dyZlm1GiQGask0aXdWDuS36b82iwzwQ01cBTXeXR25mLr4fsfMLFfYAZXyBxEMMpw3rkSiw==}
'@tsparticles/plugin-emitters-shape-circle@3.7.1':
resolution: {integrity: sha512-eBwktnGROkiyCvtrSwdPpoRbIjQgV/Odq//0dw8D+qUdnox6dNzzhJjz8L2LAA2kQZBqtdBqV2kcx3w5ZdqoEQ==}
'@tsparticles/plugin-emitters-shape-square@3.7.1':
resolution: {integrity: sha512-nvGBsRLrkiz6Q38TJRl8Y/eu9i1ChQ9oorQydLBok+iZ6MefuOj39iYsAOkD1w9yRVrFWKHG6CR1mmJUniz/HA==}
'@tsparticles/plugin-emitters@3.7.1':
resolution: {integrity: sha512-WV5Uwxp/Ckqv5kZynTj6mj13jYbQCArNLFv8ks+zjdlteoyT5EhQl4rg+TalaySCb1zCd6Fu2Scp35l3JJgbnw==}
'@tsparticles/plugin-hex-color@3.7.1':
resolution: {integrity: sha512-7xu3MV8EdNNejjYyEmrq5fCDdYAcqz/9VatLpnwtwR5Q5t2qI0tD4CrcGaFfC/rTAVJacfiJe02UV/hlj03KKA==}
'@tsparticles/plugin-hsl-color@3.7.1':
resolution: {integrity: sha512-zzAI1CuoCMBJhgeYZ5Rq42nGbPg35ZzIs11eQegjsWG5Msm5QKSj60qPzERnoUcCc4HCKtIWP7rYMz6h3xpoEg==}
'@tsparticles/plugin-rgb-color@3.7.1':
resolution: {integrity: sha512-taEraTpCYR6jpjflqBL95tN0zFU8JrAChXTt8mxVn7gddxoNMHI/LGymEPRCweLukwV6GQyAGOkeGEdWDPtYTA==}
'@tsparticles/shape-circle@3.7.1':
resolution: {integrity: sha512-kmOWaUuFwuTtcCFYjuyJbdA5qDqWdGsharLalYnIczkLu2c1I8jJo/OmGePKhWn62ocu7mqKMomfElkIcw2AsA==}
'@tsparticles/shape-emoji@3.7.1':
resolution: {integrity: sha512-mX18c/xhYVljS/r5Xbowzclw+1YwhtWoQFOOfkmjjZppA+RjgcwSKLvH6E20PaH1yVTjBOfSF+3POKpwsULzTg==}
'@tsparticles/shape-image@3.7.1':
resolution: {integrity: sha512-eDzfkQhqLY6fb9QH2Vo9TGfdJBFFpYnojhxQxc7IdzIwOFMD3JK4B52RVl9oowR+rNE8dNp6P2L+eMAF4yld0g==}
'@tsparticles/shape-line@3.7.1':
resolution: {integrity: sha512-lMPYApUpg7avxmYPfHHr4dQepZSNn/g0Q1/g2+lnTi8ZtUBiCZ2WMVy9R3GOzyozbnzigLQ6AJRnOpsUZV7H4g==}
'@tsparticles/shape-polygon@3.7.1':
resolution: {integrity: sha512-5FrRfpYC3qnvV2nXBLE4Q0v+SMNWJO8xgzh6MBFwfptvqH4EOrqc/58eS5x0jlf+evwf9LjPgeGkOTcwaHHcYQ==}
'@tsparticles/shape-square@3.7.1':
resolution: {integrity: sha512-7VCqbRwinjBZ+Ryme27rOtl+jKrET8qDthqZLrAoj3WONBqyt+R9q6SXAJ9WodqEX68IBvcluqbFY5qDZm8iAQ==}
'@tsparticles/shape-star@3.7.1':
resolution: {integrity: sha512-3G4oipioyWKLEQYT11Sx3k6AObu3dbv/A5LRqGGTQm5IR6UACa+INwykZYI0a+MdJJMb83E0e4Fn3hlZbi0/8w==}
'@tsparticles/shape-text@3.7.1':
resolution: {integrity: sha512-aU1V9O8uQQBlL0jGFh9Q0b5vQ1Ji6Oo5ptyyj5yJ5uP/ZU00L0Vhk4DNyLXpaU0+H6OBoPpCqnvEsZBB9/HmCQ==}
'@tsparticles/slim@3.7.1':
resolution: {integrity: sha512-OtJEhud2KleX7OxiG2r/VYriHNIwTpFm3sPFy4EOJzAD0EW7KZoKXGpGn5gwGI1NWeB0jso92yNTrTC2ZTW0qw==}
'@tsparticles/updater-color@3.7.1':
resolution: {integrity: sha512-QimV3yn17dcdJx7PpTwLtw9BhkQ0q8qFF035OdcZpnynBPAO/hg0zvSMpMGoeuDVFH02wWBy4h2/BYCv6wh6Sw==}
'@tsparticles/updater-destroy@3.7.1':
resolution: {integrity: sha512-krXNoMDKyeyE/ZjQh3LVjrLYivFefQOQ9i+B7RpMe7x4h+iRgpB6npTCqidGQ82+hZ8G6xfQ9ToduebWwK4JGg==}
'@tsparticles/updater-life@3.7.1':
resolution: {integrity: sha512-NY5gUrgO5AsARNC0usP9PKahXf7JCxbP/H1vzTfA0SJw4veANfWTldOvhIlcm2CHVP5P1b827p0hWsBHECwz7A==}
'@tsparticles/updater-opacity@3.7.1':
resolution: {integrity: sha512-YcyviCooTv7SAKw7sxd84CfJqZ7dYPSdYZzCpedV6TKIObRiwLqXlyLXQGJ3YltghKQSCSolmVy8woWBCDm1qA==}
'@tsparticles/updater-out-modes@3.7.1':
resolution: {integrity: sha512-Cb5sWquRtUYLSiFpmBjjYKRdpNV52diCo9+qMtK1oVlldDBhUwqO+1TQjdlaA2yl5DURlY9ZfOHXvY+IT7CHCw==}
'@tsparticles/updater-roll@3.7.1':
resolution: {integrity: sha512-gHLRqpTGVGPJBEAIPUiYVembIn5bcaTXXxsUJEM/IN+GIOvj2uZZGZ4r2aFTA6WugqEbJsJdblDSvMfouyz7Ug==}
'@tsparticles/updater-rotate@3.7.1':
resolution: {integrity: sha512-toVHwl+h6SvtA8dyxSA2kMH2QdDA71vehuAa+HoRqf1y06h5kxyYiMKZFHCqDJ6lFfRPs47MjrC9dD2bDz14MQ==}
'@tsparticles/updater-size@3.7.1':
resolution: {integrity: sha512-+Y0H0PnDJVIsJ+zHTyubYu1jtRFmVnY1dAv3VCjScIDw6bcpL/ol+HrtHTGIX0WbMyUfjCyALfAoaXi/Wm8VcQ==}
'@tsparticles/updater-stroke-color@3.7.1':
resolution: {integrity: sha512-VHhQkCNuxjx/Hy7A+g0Yijb24T0+wQ3jNsF/yfrR9dEdZWSBiimZLvV1bilPdAeEtieAJTAZo2VNhcD1snF0iQ==}
'@tsparticles/updater-tilt@3.7.1':
resolution: {integrity: sha512-pSOXoXPre1VPKC5nC5GW0L9jw63w1dVdsDdggEau7MP9xO7trko9L/KyayBX12Y4Ief1ca12Incxxr67hw7GGA==}
'@tsparticles/updater-twinkle@3.7.1':
resolution: {integrity: sha512-maRTqPbeZcxBK6s1ry+ih71qSVaitfP1KTrAKR38v26GMwyO6z+zYV2bu9WTRt21FRFAoxlMLWxNu21GtQoXDA==}
'@tsparticles/updater-wobble@3.7.1':
resolution: {integrity: sha512-YIlNg4L0w4egQJhPLpgcvcfv9+X621+cQsrdN9sSmajxhhwtEQvQUvFUzGTcvpjVi+GcBNp0t4sCKEzoP8iaYw==}
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
@ -2234,9 +2082,6 @@ packages:
tslib@2.6.3:
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
tsparticles@3.7.1:
resolution: {integrity: sha512-NNkOYIo01eHpDuaJxDCGgcLEMZKEJTCN/XPVCLg7VxgEWN19rjXpDnDguISxadS8GSFPws7hpGgbeDDAm3MX+Q==}
type-detect@4.0.8:
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
engines: {node: '>=4'}
@ -2875,11 +2720,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.31.0':
optional: true
'@sveltejs/adapter-auto@3.2.2(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))':
dependencies:
'@sveltejs/kit': 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4))
import-meta-resolve: 4.1.0
'@sveltejs/adapter-node@5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.4.12(@types/node@22.5.4)))':
dependencies:
'@rollup/plugin-commonjs': 26.0.1(rollup@4.24.0)
@ -2946,228 +2786,6 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.4
'@tsparticles/basic@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/move-base': 3.7.1
'@tsparticles/plugin-hex-color': 3.7.1
'@tsparticles/plugin-hsl-color': 3.7.1
'@tsparticles/plugin-rgb-color': 3.7.1
'@tsparticles/shape-circle': 3.7.1
'@tsparticles/updater-color': 3.7.1
'@tsparticles/updater-opacity': 3.7.1
'@tsparticles/updater-out-modes': 3.7.1
'@tsparticles/updater-size': 3.7.1
'@tsparticles/engine@3.7.1': {}
'@tsparticles/interaction-external-attract@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-bounce@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-bubble@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-connect@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-grab@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-pause@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-push@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-remove@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-repulse@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-slow@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-trail@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-particles-attract@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-particles-collisions@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-particles-links@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/move-base@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/move-parallax@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/plugin-absorbers@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/plugin-easing-quad@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/plugin-emitters-shape-circle@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/plugin-emitters': 3.7.1
'@tsparticles/plugin-emitters-shape-square@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/plugin-emitters': 3.7.1
'@tsparticles/plugin-emitters@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/plugin-hex-color@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/plugin-hsl-color@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/plugin-rgb-color@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/shape-circle@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/shape-emoji@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/shape-image@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/shape-line@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/shape-polygon@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/shape-square@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/shape-star@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/shape-text@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/slim@3.7.1':
dependencies:
'@tsparticles/basic': 3.7.1
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-attract': 3.7.1
'@tsparticles/interaction-external-bounce': 3.7.1
'@tsparticles/interaction-external-bubble': 3.7.1
'@tsparticles/interaction-external-connect': 3.7.1
'@tsparticles/interaction-external-grab': 3.7.1
'@tsparticles/interaction-external-pause': 3.7.1
'@tsparticles/interaction-external-push': 3.7.1
'@tsparticles/interaction-external-remove': 3.7.1
'@tsparticles/interaction-external-repulse': 3.7.1
'@tsparticles/interaction-external-slow': 3.7.1
'@tsparticles/interaction-particles-attract': 3.7.1
'@tsparticles/interaction-particles-collisions': 3.7.1
'@tsparticles/interaction-particles-links': 3.7.1
'@tsparticles/move-parallax': 3.7.1
'@tsparticles/plugin-easing-quad': 3.7.1
'@tsparticles/shape-emoji': 3.7.1
'@tsparticles/shape-image': 3.7.1
'@tsparticles/shape-line': 3.7.1
'@tsparticles/shape-polygon': 3.7.1
'@tsparticles/shape-square': 3.7.1
'@tsparticles/shape-star': 3.7.1
'@tsparticles/updater-life': 3.7.1
'@tsparticles/updater-rotate': 3.7.1
'@tsparticles/updater-stroke-color': 3.7.1
'@tsparticles/updater-color@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/updater-destroy@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/updater-life@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/updater-opacity@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/updater-out-modes@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/updater-roll@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/updater-rotate@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/updater-size@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/updater-stroke-color@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/updater-tilt@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/updater-twinkle@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/updater-wobble@3.7.1':
dependencies:
'@tsparticles/engine': 3.7.1
'@types/cookie@0.6.0': {}
'@types/estree@1.0.6': {}
@ -4539,22 +4157,6 @@ snapshots:
tslib@2.6.3: {}
tsparticles@3.7.1:
dependencies:
'@tsparticles/engine': 3.7.1
'@tsparticles/interaction-external-trail': 3.7.1
'@tsparticles/plugin-absorbers': 3.7.1
'@tsparticles/plugin-emitters': 3.7.1
'@tsparticles/plugin-emitters-shape-circle': 3.7.1
'@tsparticles/plugin-emitters-shape-square': 3.7.1
'@tsparticles/shape-text': 3.7.1
'@tsparticles/slim': 3.7.1
'@tsparticles/updater-destroy': 3.7.1
'@tsparticles/updater-roll': 3.7.1
'@tsparticles/updater-tilt': 3.7.1
'@tsparticles/updater-twinkle': 3.7.1
'@tsparticles/updater-wobble': 3.7.1
type-detect@4.0.8: {}
type@2.7.3: {}

View file

@ -1,52 +1,35 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type {
Adventure,
Attachment,
Category,
Collection,
OpenStreetMapPlace,
Point,
ReverseGeocode
} from '$lib/types';
import { onMount } from 'svelte';
import { enhance } from '$app/forms';
import { createEventDispatcher, onMount } from 'svelte';
import type { Adventure, Attachment, Category, Collection } from '$lib/types';
import { addToast } from '$lib/toasts';
import { deserialize } from '$app/forms';
import { t } from 'svelte-i18n';
export let longitude: number | null = null;
export let latitude: number | null = null;
export let collection: Collection | null = null;
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
const dispatch = createEventDispatcher();
let query: string = '';
let places: OpenStreetMapPlace[] = [];
let images: { id: string; image: string; is_primary: boolean }[] = [];
let warningMessage: string = '';
let constrainDates: boolean = false;
let categories: Category[] = [];
let fileInput: HTMLInputElement;
let immichIntegration: boolean = false;
import ActivityComplete from './ActivityComplete.svelte';
import { appVersion } from '$lib/config';
import CategoryDropdown from './CategoryDropdown.svelte';
import { findFirstValue } from '$lib';
import MarkdownEditor from './MarkdownEditor.svelte';
import ImmichSelect from './ImmichSelect.svelte';
import Star from '~icons/mdi/star';
import Crown from '~icons/mdi/crown';
import AttachmentCard from './AttachmentCard.svelte';
import LocationDropdown from './LocationDropdown.svelte';
let modal: HTMLDialogElement;
let wikiError: string = '';
let noPlaces: boolean = false;
let is_custom_location: boolean = false;
let reverseGeocodePlace: ReverseGeocode | null = null;
let adventure: Adventure = {
id: '',
name: '',
@ -101,38 +84,18 @@
attachments: adventureToEdit?.attachments || []
};
let markers: Point[] = [];
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
modal.showModal();
console.log('open');
});
let url: string = '';
let imageError: string = '';
let wikiImageError: string = '';
let old_display_name: string = '';
let triggerMarkVisted: boolean = false;
images = adventure.images || [];
if (longitude && latitude) {
adventure.latitude = latitude;
adventure.longitude = longitude;
reverseGeocode(true);
}
$: {
is_custom_location = adventure.location != reverseGeocodePlace?.display_name;
}
if (adventure.longitude && adventure.latitude) {
markers = [];
markers = [
{
lngLat: { lng: adventure.longitude, lat: adventure.latitude },
location: adventure.location || '',
name: adventure.name,
activity_type: ''
}
];
}
$: {
if (!adventure.rating) {
adventure.rating = NaN;
@ -230,11 +193,6 @@
}
}
function clearMap() {
console.log('CLEAR');
markers = [];
}
let imageSearch: string = adventure.name || '';
async function removeImage(id: string) {
@ -258,51 +216,6 @@
close();
}
let willBeMarkedVisited: boolean = false;
$: {
willBeMarkedVisited = false; // Reset before evaluating
const today = new Date(); // Cache today's date to avoid redundant calculations
for (const visit of adventure.visits) {
const startDate = new Date(visit.start_date);
const endDate = visit.end_date ? new Date(visit.end_date) : null;
// If the visit has both a start date and an end date, check if it started by today
if (startDate && endDate && startDate <= today) {
willBeMarkedVisited = true;
break; // Exit the loop since we've determined the result
}
// If the visit has a start date but no end date, check if it started by today
if (startDate && !endDate && startDate <= today) {
willBeMarkedVisited = true;
break; // Exit the loop since we've determined the result
}
}
console.log('WMBV:', willBeMarkedVisited);
}
let previousCoords: { lat: number; lng: number } | null = null;
$: if (markers.length > 0) {
const newLat = Math.round(markers[0].lngLat.lat * 1e6) / 1e6;
const newLng = Math.round(markers[0].lngLat.lng * 1e6) / 1e6;
if (!previousCoords || previousCoords.lat !== newLat || previousCoords.lng !== newLng) {
adventure.latitude = newLat;
adventure.longitude = newLng;
previousCoords = { lat: newLat, lng: newLng };
reverseGeocode();
}
if (!adventure.name) {
adventure.name = markers[0].name;
}
}
async function makePrimaryImage(image_id: string) {
let res = await fetch(`/api/images/${image_id}/toggle_primary`, {
method: 'POST'
@ -407,28 +320,6 @@
}
}
}
async function geocode(e: Event | null) {
if (e) {
e.preventDefault();
}
if (!query) {
alert($t('adventures.no_location'));
return;
}
let res = await fetch(`https://nominatim.openstreetmap.org/search?q=${query}&format=jsonv2`, {
headers: {
'User-Agent': `AdventureLog / ${appVersion} `
}
});
console.log(res);
let data = (await res.json()) as OpenStreetMapPlace[];
places = data;
if (data.length === 0) {
noPlaces = true;
} else {
noPlaces = false;
}
}
let new_start_date: string = '';
let new_end_date: string = '';
@ -459,93 +350,6 @@
new_notes = '';
}
async function markVisited() {
console.log(reverseGeocodePlace);
if (reverseGeocodePlace) {
if (!reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) {
let region_res = await fetch(`/api/visitedregion`, {
headers: { 'Content-Type': 'application/json' },
method: 'POST',
body: JSON.stringify({ region: reverseGeocodePlace.region_id })
});
if (region_res.ok) {
reverseGeocodePlace.region_visited = true;
addToast('success', `Visit to ${reverseGeocodePlace.region} marked`);
} else {
addToast('error', `Failed to mark visit to ${reverseGeocodePlace.region}`);
}
}
if (!reverseGeocodePlace.city_visited && reverseGeocodePlace.city_id != null) {
let city_res = await fetch(`/api/visitedcity`, {
headers: { 'Content-Type': 'application/json' },
method: 'POST',
body: JSON.stringify({ city: reverseGeocodePlace.city_id })
});
if (city_res.ok) {
reverseGeocodePlace.city_visited = true;
addToast('success', `Visit to ${reverseGeocodePlace.city} marked`);
} else {
addToast('error', `Failed to mark visit to ${reverseGeocodePlace.city}`);
}
}
}
}
async function reverseGeocode(force_update: boolean = false) {
let res = await fetch(
`/api/reverse-geocode/reverse_geocode/?lat=${adventure.latitude}&lon=${adventure.longitude}`
);
let data = await res.json();
if (data.error) {
console.log(data.error);
reverseGeocodePlace = null;
return;
}
reverseGeocodePlace = data;
console.log(reverseGeocodePlace);
console.log(is_custom_location);
if (
reverseGeocodePlace &&
reverseGeocodePlace.display_name &&
(!is_custom_location || force_update)
) {
old_display_name = reverseGeocodePlace.display_name;
adventure.location = reverseGeocodePlace.display_name;
}
console.log(data);
}
let fileInput: HTMLInputElement;
const dispatch = createEventDispatcher();
let modal: HTMLDialogElement;
let immichIntegration: boolean = false;
onMount(async () => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
modal.showModal();
console.log('open');
let categoryFetch = await fetch('/api/categories/categories');
if (categoryFetch.ok) {
categories = await categoryFetch.json();
} else {
addToast('error', $t('adventures.category_fetch_error'));
}
// Check for Immich Integration
let res = await fetch('/api/integrations');
if (!res.ok) {
addToast('error', $t('immich.integration_fetch_error'));
} else {
let data = await res.json();
if (data.immich) {
immichIntegration = true;
}
}
});
function close() {
dispatch('close');
}
@ -567,20 +371,6 @@
}
}
async function addMarker(e: CustomEvent<any>) {
markers = [];
markers = [
...markers,
{
lngLat: e.detail.lngLat,
name: '',
location: '',
activity_type: ''
}
];
console.log(markers);
}
function imageSubmit() {
return async ({ result }: any) => {
if (result.type === 'success') {
@ -600,6 +390,8 @@
async function handleSubmit(event: Event) {
event.preventDefault();
triggerMarkVisted = true;
console.log(adventure);
if (adventure.id === '') {
console.log(categories);
@ -655,12 +447,6 @@
addToast('error', $t('adventures.adventure_update_error'));
}
}
if (
adventure.is_visited &&
(!reverseGeocodePlace?.region_visited || !reverseGeocodePlace?.city_visited)
) {
markVisited();
}
imageSearch = adventure.name;
}
</script>
@ -811,150 +597,7 @@
</div>
</div>
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">
{$t('adventures.location_information')}
</div>
<div class="collapse-content">
<!-- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> -->
<div>
<label for="latitude">{$t('adventures.location')}</label><br />
<div class="flex items-center">
<input
type="text"
id="location"
name="location"
bind:value={adventure.location}
class="input input-bordered w-full"
/>
{#if is_custom_location}
<button
class="btn btn-primary ml-2"
type="button"
on:click={() => (adventure.location = reverseGeocodePlace?.display_name)}
>{$t('adventures.set_to_pin')}</button
>
{/if}
</div>
</div>
<div>
<form on:submit={geocode} class="mt-2">
<input
type="text"
placeholder={$t('adventures.search_for_location')}
class="input input-bordered w-full max-w-xs mb-2"
id="search"
name="search"
bind:value={query}
/>
<button class="btn btn-neutral -mt-1" type="submit">{$t('navbar.search')}</button>
<button class="btn btn-neutral -mt-1" type="button" on:click={clearMap}
>{$t('adventures.clear_map')}</button
>
</form>
</div>
{#if places.length > 0}
<div class="mt-4 max-w-full">
<h3 class="font-bold text-lg mb-4">{$t('adventures.search_results')}</h3>
<div class="flex flex-wrap">
{#each places as place}
<button
type="button"
class="btn btn-neutral mb-2 mr-2 max-w-full break-words whitespace-normal text-left"
on:click={() => {
markers = [
{
lngLat: { lng: Number(place.lon), lat: Number(place.lat) },
location: place.display_name,
name: place.name,
activity_type: place.type
}
];
}}
>
{place.display_name}
</button>
{/each}
</div>
</div>
{:else if noPlaces}
<p class="text-error text-lg">{$t('adventures.no_results')}</p>
{/if}
<!-- </div> -->
<div>
<MapLibre
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg"
standardControls
>
<!-- MapEvents gives you access to map events even from other components inside the map,
where you might not have access to the top-level `MapLibre` component. In this case
it would also work to just use on:click on the MapLibre component itself. -->
<MapEvents on:click={addMarker} />
{#each markers as marker}
<DefaultMarker lngLat={marker.lngLat} />
{/each}
</MapLibre>
{#if reverseGeocodePlace}
<div class="mt-2">
<p>
{reverseGeocodePlace.city
? reverseGeocodePlace.city + ', '
: ''}{reverseGeocodePlace.region},
{reverseGeocodePlace.country}
</p>
<p>
{reverseGeocodePlace.region}:
{reverseGeocodePlace.region_visited
? $t('adventures.visited')
: $t('adventures.not_visited')}
</p>
{#if reverseGeocodePlace.city}
<p>
{reverseGeocodePlace.city}:
{reverseGeocodePlace.city_visited
? $t('adventures.visited')
: $t('adventures.not_visited')}
</p>
{/if}
</div>
{#if !reverseGeocodePlace.region_visited || (!reverseGeocodePlace.city_visited && !willBeMarkedVisited)}
<button type="button" class="btn btn-neutral" on:click={markVisited}>
{$t('adventures.mark_visited')}
</button>
{/if}
{#if (willBeMarkedVisited && !reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) || (!reverseGeocodePlace.city_visited && willBeMarkedVisited && reverseGeocodePlace.city_id)}
<div role="alert" class="alert alert-info mt-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span
>{reverseGeocodePlace.city
? reverseGeocodePlace.city + ', '
: ''}{reverseGeocodePlace.region},
{reverseGeocodePlace.country}
{$t('adventures.will_be_marked')}</span
>
</div>
{/if}
{/if}
</div>
</div>
</div>
<LocationDropdown bind:item={adventure} bind:triggerMarkVisted />
<div class="collapse collapse-plus bg-base-200 mb-4 overflow-visible">
<input type="checkbox" />
@ -1257,6 +900,7 @@ it would also work to just use on:click on the MapLibre component itself. -->
{#if immichIntegration}
<ImmichSelect
{adventure}
on:fetchImage={(e) => {
url = e.detail;
fetchImage();

View file

@ -34,7 +34,9 @@
? `${user.first_name} ${user.last_name}`
: user.username}
</p>
<li><button on:click={() => goto('/profile')}>{$t('navbar.profile')}</button></li>
<li>
<button on:click={() => goto(`/profile/${user.username}`)}>{$t('navbar.profile')}</button>
</li>
<li><button on:click={() => goto('/adventures')}>{$t('navbar.my_adventures')}</button></li>
<li><button on:click={() => goto('/shared')}>{$t('navbar.shared_with_me')}</button></li>
<li><button on:click={() => goto('/settings')}>{$t('navbar.settings')}</button></li>

View file

@ -0,0 +1,315 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { addToast } from '$lib/toasts';
import { t } from 'svelte-i18n';
import MarkdownEditor from './MarkdownEditor.svelte';
import { appVersion } from '$lib/config';
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
import type { Collection, Hotel, ReverseGeocode, OpenStreetMapPlace, Point } from '$lib/types';
import LocationDropdown from './LocationDropdown.svelte';
const dispatch = createEventDispatcher();
export let collection: Collection;
export let hotelToEdit: Hotel | null = null;
let modal: HTMLDialogElement;
let constrainDates: boolean = false;
let hotel: Hotel = { ...initializeHotel(hotelToEdit) };
let fullStartDate: string = '';
let fullEndDate: string = '';
let reverseGeocodePlace: any | null = null;
let query: string = '';
let places: OpenStreetMapPlace[] = [];
let noPlaces: boolean = false;
let is_custom_location: boolean = false;
let markers: Point[] = [];
// Format date as local datetime
function toLocalDatetime(value: string | null): string {
if (!value) return '';
const date = new Date(value);
return date.toISOString().slice(0, 16); // Format: YYYY-MM-DDTHH:mm
}
// Initialize hotel with values from hotelToEdit or default values
function initializeHotel(hotelToEdit: Hotel | null): Hotel {
return {
id: hotelToEdit?.id || '',
user_id: hotelToEdit?.user_id || '',
name: hotelToEdit?.name || '',
description: hotelToEdit?.description || '',
rating: hotelToEdit?.rating || NaN,
link: hotelToEdit?.link || '',
check_in: hotelToEdit?.check_in || null,
check_out: hotelToEdit?.check_out || null,
reservation_number: hotelToEdit?.reservation_number || '',
price: hotelToEdit?.price || null,
latitude: hotelToEdit?.latitude || null,
longitude: hotelToEdit?.longitude || null,
location: hotelToEdit?.location || '',
is_public: hotelToEdit?.is_public || false,
collection: hotelToEdit?.collection || '',
created_at: hotelToEdit?.created_at || '',
updated_at: hotelToEdit?.updated_at || ''
};
}
// Set full start and end dates from collection
if (collection.start_date && collection.end_date) {
fullStartDate = `${collection.start_date}T00:00`;
fullEndDate = `${collection.end_date}T23:59`;
}
// Handle rating change
$: {
if (!hotel.rating) {
hotel.rating = NaN;
}
}
// Show modal on mount
onMount(() => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) modal.showModal();
});
// Close modal
function close() {
dispatch('close');
}
// Close modal on escape key press
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') close();
}
// Handle form submission (save hotel)
async function handleSubmit(event: Event) {
event.preventDefault();
if (hotel.check_in && !hotel.check_out) {
const checkInDate = new Date(hotel.check_in);
checkInDate.setDate(checkInDate.getDate() + 1);
hotel.check_out = checkInDate.toISOString();
}
if (hotel.check_in && hotel.check_out && hotel.check_in > hotel.check_out) {
addToast('error', $t('adventures.start_before_end_error'));
return;
}
// Create or update hotel
const url = hotel.id === '' ? '/api/hotels' : `/api/hotels/${hotel.id}`;
const method = hotel.id === '' ? 'POST' : 'PATCH';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(hotel)
});
const data = await res.json();
if (data.id) {
hotel = data as Hotel;
const toastMessage =
hotel.id === '' ? 'adventures.adventure_created' : 'adventures.adventure_updated';
addToast('success', $t(toastMessage));
dispatch('save', hotel);
} else {
const errorMessage =
hotel.id === '' ? 'adventures.adventure_create_error' : 'adventures.adventure_update_error';
addToast('error', $t(errorMessage));
}
}
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div class="modal-box w-11/12 max-w-3xl" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-2xl">
{hotelToEdit
? $t('transportation.edit_transportation')
: $t('transportation.new_transportation')}
</h3>
<div class="modal-action items-center">
<form method="post" style="width: 100%;" on:submit={handleSubmit}>
<!-- Basic Information Section -->
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{$t('adventures.basic_information')}
</div>
<div class="collapse-content">
<!-- Name -->
<div>
<label for="name">
{$t('adventures.name')}<span class="text-red-500">*</span>
</label>
<input
type="text"
id="name"
name="name"
bind:value={hotel.name}
class="input input-bordered w-full"
required
/>
</div>
<!-- Description -->
<div>
<label for="description">{$t('adventures.description')}</label><br />
<MarkdownEditor bind:text={hotel.description} editor_height={'h-32'} />
</div>
<!-- Rating -->
<div>
<label for="rating">{$t('adventures.rating')}</label><br />
<input
type="number"
min="0"
max="5"
hidden
bind:value={hotel.rating}
id="rating"
name="rating"
class="input input-bordered w-full max-w-xs mt-1"
/>
<div class="rating -ml-3 mt-1">
<input
type="radio"
name="rating-2"
class="rating-hidden"
checked={Number.isNaN(hotel.rating)}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (hotel.rating = 1)}
checked={hotel.rating === 1}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (hotel.rating = 2)}
checked={hotel.rating === 2}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (hotel.rating = 3)}
checked={hotel.rating === 3}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (hotel.rating = 4)}
checked={hotel.rating === 4}
/>
<input
type="radio"
name="rating-2"
class="mask mask-star-2 bg-orange-400"
on:click={() => (hotel.rating = 5)}
checked={hotel.rating === 5}
/>
{#if hotel.rating}
<button
type="button"
class="btn btn-sm btn-error ml-2"
on:click={() => (hotel.rating = NaN)}
>
{$t('adventures.remove')}
</button>
{/if}
</div>
</div>
<!-- Link -->
<div>
<label for="link">{$t('adventures.link')}</label>
<input
type="url"
id="link"
name="link"
bind:value={hotel.link}
class="input input-bordered w-full"
/>
</div>
</div>
</div>
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
{$t('adventures.date_information')}
</div>
<div class="collapse-content">
<!-- Start Date -->
<div>
<label for="date">
{$t('adventures.start_date')}
</label>
{#if collection && collection.start_date && collection.end_date}<label
class="label cursor-pointer flex items-start space-x-2"
>
<span class="label-text">{$t('adventures.date_constrain')}</span>
<input
type="checkbox"
class="toggle toggle-primary"
id="constrain_dates"
name="constrain_dates"
on:change={() => (constrainDates = !constrainDates)}
/></label
>
{/if}
<div>
<input
type="datetime-local"
id="date"
name="date"
bind:value={hotel.check_in}
min={constrainDates ? fullStartDate : ''}
max={constrainDates ? fullEndDate : ''}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
<!-- End Date -->
{#if hotel.check_in}
<div>
<label for="end_date">
{$t('adventures.end_date')}
</label>
<div>
<input
type="datetime-local"
id="end_date"
name="end_date"
min={constrainDates ? hotel.check_in : ''}
max={constrainDates ? fullEndDate : ''}
bind:value={hotel.check_out}
class="input input-bordered w-full max-w-xs mt-1"
/>
</div>
</div>
{/if}
</div>
</div>
<!-- Location Information -->
<LocationDropdown bind:item={hotel} />
<!-- Form Actions -->
<div class="mt-4">
<button type="submit" class="btn btn-primary">
{$t('notes.save')}
</button>
<button type="button" class="btn" on:click={close}>
{$t('about.close')}
</button>
</div>
</form>
</div>
</div>
</dialog>

View file

@ -1,32 +1,81 @@
<script lang="ts">
let immichSearchValue: string = '';
let searchOrSelect: string = 'search';
let immichError: string = '';
let immichNext: string = '';
let immichPage: number = 1;
import { createEventDispatcher, onMount } from 'svelte';
import { t } from 'svelte-i18n';
import ImmichLogo from '$lib/assets/immich.svg';
import type { Adventure, ImmichAlbum } from '$lib/types';
import { debounce } from '$lib';
let immichImages: any[] = [];
let immichSearchValue: string = '';
let searchCategory: 'search' | 'date' | 'album' = 'date';
let immichError: string = '';
let immichNextURL: string = '';
let loading = false;
export let adventure: Adventure | null = null;
const dispatch = createEventDispatcher();
let albums: ImmichAlbum[] = [];
let currentAlbum: string = '';
let selectedDate: string =
(adventure as Adventure | null)?.visits
.map((v) => new Date(v.end_date || v.start_date))
.sort((a, b) => +b - +a)[0]
?.toISOString()
?.split('T')[0] || '';
if (!selectedDate) {
selectedDate = new Date().toISOString().split('T')[0];
}
$: {
if (currentAlbum) {
immichImages = [];
fetchAlbumAssets(currentAlbum);
} else {
immichImages = [];
} else if (searchCategory === 'date' && selectedDate) {
searchImmich();
}
}
async function loadMoreImmich() {
// The next URL returned by our API is a absolute url to API, but we need to use the relative path, to use the frontend api proxy.
const url = new URL(immichNextURL);
immichNextURL = url.pathname + url.search;
return fetchAssets(immichNextURL, true);
}
async function fetchAssets(url: string, usingNext = false) {
loading = true;
try {
let res = await fetch(url);
immichError = '';
if (!res.ok) {
let data = await res.json();
let errorMessage = data.message;
console.error('Error in handling fetchAsstes', errorMessage);
immichError = $t(data.code);
} else {
let data = await res.json();
if (data.results && data.results.length > 0) {
if (usingNext) {
immichImages = [...immichImages, ...data.results];
} else {
immichImages = data.results;
}
} else {
immichError = $t('immich.no_items_found');
}
immichNextURL = data.next || '';
}
} finally {
loading = false;
}
}
async function fetchAlbumAssets(album_id: string) {
let res = await fetch(`/api/integrations/immich/albums/${album_id}`);
if (res.ok) {
let data = await res.json();
immichNext = '';
immichImages = data;
}
return fetchAssets(`/api/integrations/immich/albums/${album_id}`);
}
onMount(async () => {
@ -37,66 +86,23 @@
}
});
let immichImages: any[] = [];
import { t } from 'svelte-i18n';
import ImmichLogo from '$lib/assets/immich.svg';
import type { ImmichAlbum } from '$lib/types';
async function searchImmich() {
let res = await fetch(`/api/integrations/immich/search/?query=${immichSearchValue}`);
if (!res.ok) {
let data = await res.json();
let errorMessage = data.message;
console.log(errorMessage);
immichError = $t(data.code);
} else {
let data = await res.json();
console.log(data);
immichError = '';
if (data.results && data.results.length > 0) {
immichImages = data.results;
} else {
immichError = $t('immich.no_items_found');
}
if (data.next) {
immichNext =
'/api/integrations/immich/search?query=' +
immichSearchValue +
'&page=' +
(immichPage + 1);
} else {
immichNext = '';
}
function buildQueryParams() {
let params = new URLSearchParams();
if (immichSearchValue && searchCategory === 'search') {
params.append('query', immichSearchValue);
} else if (selectedDate && searchCategory === 'date') {
params.append('date', selectedDate);
}
return params.toString();
}
async function loadMoreImmich() {
let res = await fetch(immichNext);
if (!res.ok) {
let data = await res.json();
let errorMessage = data.message;
console.log(errorMessage);
immichError = $t(data.code);
} else {
let data = await res.json();
console.log(data);
immichError = '';
if (data.results && data.results.length > 0) {
immichImages = [...immichImages, ...data.results];
} else {
immichError = $t('immich.no_items_found');
}
if (data.next) {
immichNext =
'/api/integrations/immich/search?query=' +
immichSearchValue +
'&page=' +
(immichPage + 1);
immichPage++;
} else {
immichNext = '';
}
}
const searchImmich = debounce(() => {
_searchImmich();
}, 500); // Debounce the search function to avoid multiple requests on every key press
async function _searchImmich() {
immichImages = [];
return fetchAssets(`/api/integrations/immich/search/?${buildQueryParams()}`);
}
</script>
@ -111,20 +117,27 @@
on:click={() => (currentAlbum = '')}
type="radio"
class="join-item btn"
bind:group={searchOrSelect}
bind:group={searchCategory}
value="search"
aria-label="Search"
/>
<input
type="radio"
class="join-item btn"
bind:group={searchOrSelect}
value="select"
bind:group={searchCategory}
value="date"
aria-label="Show by date"
/>
<input
type="radio"
class="join-item btn"
bind:group={searchCategory}
value="album"
aria-label="Select Album"
/>
</div>
<div>
{#if searchOrSelect === 'search'}
{#if searchCategory === 'search'}
<form on:submit|preventDefault={searchImmich}>
<input
type="text"
@ -134,7 +147,13 @@
/>
<button type="submit" class="btn btn-neutral mt-2">Search</button>
</form>
{:else}
{:else if searchCategory === 'date'}
<input
type="date"
bind:value={selectedDate}
class="input input-bordered w-full max-w-xs mt-2"
/>
{:else if searchCategory === 'album'}
<select class="select select-bordered w-full max-w-xs mt-2" bind:value={currentAlbum}>
<option value="" disabled selected>Select an Album</option>
{#each albums as album}
@ -147,14 +166,25 @@
<p class="text-red-500">{immichError}</p>
<div class="flex flex-wrap gap-4 mr-4 mt-2">
{#if loading}
<div
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[100] w-24 h-24"
>
<span class="loading loading-spinner w-24 h-24"></span>
</div>
{/if}
{#each immichImages as image}
<div class="flex flex-col items-center gap-2">
<div class="flex flex-col items-center gap-2" class:blur-sm={loading}>
<!-- svelte-ignore a11y-img-redundant-alt -->
<img
src={`/immich/${image.id}`}
alt="Image from Immich"
class="h-24 w-24 object-cover rounded-md"
/>
<h4>
{image.fileCreatedAt?.split('T')[0] || 'Unknown'}
</h4>
<button
type="button"
class="btn btn-sm btn-primary"
@ -168,7 +198,7 @@
</button>
</div>
{/each}
{#if immichNext}
{#if immichNextURL}
<button class="btn btn-neutral" on:click={loadMoreImmich}>{$t('immich.load_more')}</button>
{/if}
</div>

View file

@ -0,0 +1,336 @@
<script lang="ts">
import { appVersion } from '$lib/config';
import { addToast } from '$lib/toasts';
import type { Adventure, Hotel, OpenStreetMapPlace, Point, ReverseGeocode } from '$lib/types';
import { t } from 'svelte-i18n';
import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre';
export let item: Adventure | Hotel;
export let triggerMarkVisted: boolean = false;
let reverseGeocodePlace: ReverseGeocode | null = null;
let markers: Point[] = [];
let query: string = '';
let is_custom_location: boolean = false;
let willBeMarkedVisited: boolean = false;
let previousCoords: { lat: number; lng: number } | null = null;
let old_display_name: string = '';
let places: OpenStreetMapPlace[] = [];
let noPlaces: boolean = false;
$: if (markers.length > 0) {
const newLat = Math.round(markers[0].lngLat.lat * 1e6) / 1e6;
const newLng = Math.round(markers[0].lngLat.lng * 1e6) / 1e6;
if (!previousCoords || previousCoords.lat !== newLat || previousCoords.lng !== newLng) {
item.latitude = newLat;
item.longitude = newLng;
previousCoords = { lat: newLat, lng: newLng };
reverseGeocode();
}
if (!item.name) {
item.name = markers[0].name;
}
}
$: {
console.log(triggerMarkVisted);
}
$: if (triggerMarkVisted && willBeMarkedVisited) {
markVisited();
triggerMarkVisted = false;
}
$: {
is_custom_location = Boolean(
item.location != reverseGeocodePlace?.display_name && item.location
);
}
if (item.longitude && item.latitude) {
markers = [];
markers = [
{
lngLat: { lng: item.longitude, lat: item.latitude },
location: item.location || '',
name: item.name,
activity_type: ''
}
];
}
$: {
if ('visits' in item) {
willBeMarkedVisited = false; // Reset before evaluating
const today = new Date(); // Cache today's date to avoid redundant calculations
for (const visit of item.visits) {
const startDate = new Date(visit.start_date);
const endDate = visit.end_date ? new Date(visit.end_date) : null;
// If the visit has both a start date and an end date, check if it started by today
if (startDate && endDate && startDate <= today) {
willBeMarkedVisited = true;
break; // Exit the loop since we've determined the result
}
// If the visit has a start date but no end date, check if it started by today
if (startDate && !endDate && startDate <= today) {
willBeMarkedVisited = true;
break; // Exit the loop since we've determined the result
}
}
console.log('WMBV:', willBeMarkedVisited);
}
}
async function markVisited() {
console.log(reverseGeocodePlace);
if (reverseGeocodePlace) {
if (!reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) {
let region_res = await fetch(`/api/visitedregion`, {
headers: { 'Content-Type': 'application/json' },
method: 'POST',
body: JSON.stringify({ region: reverseGeocodePlace.region_id })
});
if (region_res.ok) {
reverseGeocodePlace.region_visited = true;
addToast('success', `Visit to ${reverseGeocodePlace.region} marked`);
} else {
addToast('error', `Failed to mark visit to ${reverseGeocodePlace.region}`);
}
}
if (!reverseGeocodePlace.city_visited && reverseGeocodePlace.city_id != null) {
let city_res = await fetch(`/api/visitedcity`, {
headers: { 'Content-Type': 'application/json' },
method: 'POST',
body: JSON.stringify({ city: reverseGeocodePlace.city_id })
});
if (city_res.ok) {
reverseGeocodePlace.city_visited = true;
addToast('success', `Visit to ${reverseGeocodePlace.city} marked`);
} else {
addToast('error', `Failed to mark visit to ${reverseGeocodePlace.city}`);
}
}
}
}
async function addMarker(e: CustomEvent<any>) {
markers = [];
markers = [
...markers,
{
lngLat: e.detail.lngLat,
name: '',
location: '',
activity_type: ''
}
];
console.log(markers);
}
async function geocode(e: Event | null) {
if (e) {
e.preventDefault();
}
if (!query) {
alert($t('adventures.no_location'));
return;
}
let res = await fetch(`https://nominatim.openstreetmap.org/search?q=${query}&format=jsonv2`, {
headers: {
'User-Agent': `AdventureLog / ${appVersion} `
}
});
console.log(res);
let data = (await res.json()) as OpenStreetMapPlace[];
places = data;
if (data.length === 0) {
noPlaces = true;
} else {
noPlaces = false;
}
}
async function reverseGeocode(force_update: boolean = false) {
let res = await fetch(
`/api/reverse-geocode/reverse_geocode/?lat=${item.latitude}&lon=${item.longitude}`
);
let data = await res.json();
if (data.error) {
console.log(data.error);
reverseGeocodePlace = null;
return;
}
reverseGeocodePlace = data;
console.log(reverseGeocodePlace);
console.log(is_custom_location);
if (
reverseGeocodePlace &&
reverseGeocodePlace.display_name &&
(!is_custom_location || force_update)
) {
old_display_name = reverseGeocodePlace.display_name;
item.location = reverseGeocodePlace.display_name;
}
console.log(data);
}
function clearMap() {
console.log('CLEAR');
markers = [];
}
</script>
<div class="collapse collapse-plus bg-base-200 mb-4">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">
{$t('adventures.location_information')}
</div>
<div class="collapse-content">
<!-- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> -->
<div>
<label for="latitude">{$t('adventures.location')}</label><br />
<div class="flex items-center">
<input
type="text"
id="location"
name="location"
bind:value={item.location}
class="input input-bordered w-full"
/>
{#if is_custom_location}
<button
class="btn btn-primary ml-2"
type="button"
on:click={() => (item.location = reverseGeocodePlace?.display_name)}
>{$t('adventures.set_to_pin')}</button
>
{/if}
</div>
</div>
<div>
<form on:submit={geocode} class="mt-2">
<input
type="text"
placeholder={$t('adventures.search_for_location')}
class="input input-bordered w-full max-w-xs mb-2"
id="search"
name="search"
bind:value={query}
/>
<button class="btn btn-neutral -mt-1" type="submit">{$t('navbar.search')}</button>
<button class="btn btn-neutral -mt-1" type="button" on:click={clearMap}
>{$t('adventures.clear_map')}</button
>
</form>
</div>
{#if places.length > 0}
<div class="mt-4 max-w-full">
<h3 class="font-bold text-lg mb-4">{$t('adventures.search_results')}</h3>
<div class="flex flex-wrap">
{#each places as place}
<button
type="button"
class="btn btn-neutral mb-2 mr-2 max-w-full break-words whitespace-normal text-left"
on:click={() => {
markers = [
{
lngLat: { lng: Number(place.lon), lat: Number(place.lat) },
location: place.display_name,
name: place.name,
activity_type: place.type
}
];
}}
>
{place.display_name}
</button>
{/each}
</div>
</div>
{:else if noPlaces}
<p class="text-error text-lg">{$t('adventures.no_results')}</p>
{/if}
<!-- </div> -->
<div>
<MapLibre
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full rounded-lg"
standardControls
>
<!-- MapEvents gives you access to map events even from other components inside the map,
where you might not have access to the top-level `MapLibre` component. In this case
it would also work to just use on:click on the MapLibre component itself. -->
<MapEvents on:click={addMarker} />
{#each markers as marker}
<DefaultMarker lngLat={marker.lngLat} />
{/each}
</MapLibre>
{#if reverseGeocodePlace}
<div class="mt-2">
<p>
{reverseGeocodePlace.city
? reverseGeocodePlace.city + ', '
: ''}{reverseGeocodePlace.region},
{reverseGeocodePlace.country}
</p>
<p>
{reverseGeocodePlace.region}:
{reverseGeocodePlace.region_visited
? $t('adventures.visited')
: $t('adventures.not_visited')}
</p>
{#if reverseGeocodePlace.city}
<p>
{reverseGeocodePlace.city}:
{reverseGeocodePlace.city_visited
? $t('adventures.visited')
: $t('adventures.not_visited')}
</p>
{/if}
</div>
{#if !reverseGeocodePlace.region_visited || (!reverseGeocodePlace.city_visited && !willBeMarkedVisited)}
<button type="button" class="btn btn-neutral" on:click={markVisited}>
{$t('adventures.mark_visited')}
</button>
{/if}
{#if (willBeMarkedVisited && !reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) || (!reverseGeocodePlace.city_visited && willBeMarkedVisited && reverseGeocodePlace.city_id)}
<div role="alert" class="alert alert-info mt-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span
>{reverseGeocodePlace.city
? reverseGeocodePlace.city + ', '
: ''}{reverseGeocodePlace.region},
{reverseGeocodePlace.country}
{$t('adventures.will_be_marked')}</span
>
</div>
{/if}
{/if}
</div>
</div>
</div>

View file

@ -17,7 +17,8 @@
// Event listener for focusing input
function handleKeydown(event: KeyboardEvent) {
if (event.key === '/' && document.activeElement !== inputElement) {
// Ignore any keypresses in an input/textarea field, so we don't interfere with typing.
if (event.key === '/' && !["INPUT", "TEXTAREA"].includes((event.target as HTMLElement)?.tagName)) {
event.preventDefault(); // Prevent browser's search shortcut
if (inputElement) {
inputElement.focus();

View file

@ -50,7 +50,7 @@
<!-- Card Actions -->
<div class="card-actions justify-center mt-6">
{#if !sharing}
<button class="btn btn-primary" on:click={() => goto(`/user/${user.uuid}`)}>
<button class="btn btn-primary" on:click={() => goto(`/profile/${user.username}`)}>
View Profile
</button>
{:else if shared_with && !shared_with.includes(user.uuid)}

View file

@ -464,3 +464,13 @@ export function osmTagToEmoji(tag: string) {
return '📍'; // Default placeholder emoji for unknown tags
}
}
export function debounce(func: Function, timeout: number) {
let timer: number | NodeJS.Timeout;
return (...args: any) => {
clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
}, timeout);
};
}

View file

@ -24,6 +24,11 @@
"url": "backgrounds/adventurelog_showcase_5.webp",
"author": "Sean Morley",
"location": "Hoboken, New Jersey, USA"
},
{
"url": "backgrounds/adventurelog_showcase_6.webp",
"author": "Sean Morley",
"location": "Smugglers' Notch Resort, Vermont, USA"
}
]
}

View file

@ -1,7 +1,6 @@
export type User = {
pk: number;
username: string;
email: string | null;
first_name: string | null;
last_name: string | null;
date_joined: string | null;
@ -41,6 +40,7 @@ export type Adventure = {
is_visited?: boolean;
category: Category | null;
attachments: Attachment[];
user?: User | null;
};
export type Country = {
@ -113,6 +113,7 @@ export type Collection = {
end_date: string | null;
transportations?: Transportation[];
notes?: Note[];
hotels?: Hotel[];
checklists?: Checklist[];
is_archived?: boolean;
shared_with: string[] | undefined;
@ -262,3 +263,23 @@ export type Attachment = {
user_id: string;
name: string;
};
export type Hotel = {
id: string;
user_id: string;
name: string;
description: string | null;
rating: number | null;
link: string | null;
check_in: string | null; // ISO 8601 date string
check_out: string | null; // ISO 8601 date string
reservation_number: string | null;
price: number | null;
latitude: number | null;
longitude: number | null;
location: string | null;
is_public: boolean;
collection: string | null;
created_at: string; // ISO 8601 date string
updated_at: string; // ISO 8601 date string
};

View file

@ -234,7 +234,8 @@
"images": "Bilder",
"primary": "Primär",
"upload": "Hochladen",
"view_attachment": "Anhang anzeigen"
"view_attachment": "Anhang anzeigen",
"of": "von"
},
"home": {
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",
@ -302,7 +303,11 @@
"both_passwords_required": "Beide Passwörter sind erforderlich",
"new_password": "Neues Passwort",
"reset_failed": "Passwort konnte nicht zurückgesetzt werden",
"or_3rd_party": "Oder melden Sie sich bei einem Drittanbieter an"
"or_3rd_party": "Oder melden Sie sich bei einem Drittanbieter an",
"no_public_adventures": "Keine öffentlichen Abenteuer gefunden",
"no_public_collections": "Keine öffentlichen Sammlungen gefunden",
"user_adventures": "Benutzerabenteuer",
"user_collections": "Benutzersammlungen"
},
"users": {
"no_users_found": "Keine Benutzer mit öffentlichen Profilen gefunden."

View file

@ -165,6 +165,7 @@
"delete_collection_success": "Collection deleted successfully!",
"delete_collection_warning": "Are you sure you want to delete this collection? This will also delete all of the linked adventures. This action cannot be undone.",
"cancel": "Cancel",
"of": "of",
"delete_collection": "Delete Collection",
"delete_adventure": "Delete Adventure",
"adventure_delete_success": "Adventure deleted successfully!",
@ -190,6 +191,7 @@
"no_description_found": "No description found",
"adventure_created": "Adventure created",
"adventure_create_error": "Failed to create adventure",
"hotel": "Hotel",
"create_adventure": "Create Adventure",
"adventure_updated": "Adventure updated",
"adventure_update_error": "Failed to update adventure",
@ -326,7 +328,11 @@
"new_password": "New Password (6+ characters)",
"both_passwords_required": "Both passwords are required",
"reset_failed": "Failed to reset password",
"or_3rd_party": "Or login with a third-party service"
"or_3rd_party": "Or login with a third-party service",
"no_public_adventures": "No public adventures found",
"no_public_collections": "No public collections found",
"user_adventures": "User Adventures",
"user_collections": "User Collections"
},
"users": {
"no_users_found": "No users found with public profiles."

View file

@ -281,7 +281,8 @@
"primary": "Primario",
"upload": "Subir",
"view_attachment": "Ver archivo adjunto",
"attachment_name": "Nombre del archivo adjunto"
"attachment_name": "Nombre del archivo adjunto",
"of": "de"
},
"worldtravel": {
"all": "Todo",
@ -326,7 +327,11 @@
"both_passwords_required": "Se requieren ambas contraseñas",
"new_password": "Nueva contraseña",
"reset_failed": "No se pudo restablecer la contraseña",
"or_3rd_party": "O inicie sesión con un servicio de terceros"
"or_3rd_party": "O inicie sesión con un servicio de terceros",
"no_public_adventures": "No se encontraron aventuras públicas",
"no_public_collections": "No se encontraron colecciones públicas",
"user_adventures": "Aventuras de usuario",
"user_collections": "Colecciones de usuarios"
},
"users": {
"no_users_found": "No se encontraron usuarios con perfiles públicos."

View file

@ -234,7 +234,8 @@
"images": "Images",
"primary": "Primaire",
"upload": "Télécharger",
"view_attachment": "Voir la pièce jointe"
"view_attachment": "Voir la pièce jointe",
"of": "de"
},
"home": {
"desc_1": "Découvrez, planifiez et explorez en toute simplicité",
@ -302,7 +303,11 @@
"both_passwords_required": "Les deux mots de passe sont requis",
"new_password": "Nouveau mot de passe",
"reset_failed": "Échec de la réinitialisation du mot de passe",
"or_3rd_party": "Ou connectez-vous avec un service tiers"
"or_3rd_party": "Ou connectez-vous avec un service tiers",
"no_public_adventures": "Aucune aventure publique trouvée",
"no_public_collections": "Aucune collection publique trouvée",
"user_adventures": "Aventures utilisateur",
"user_collections": "Collections d'utilisateurs"
},
"users": {
"no_users_found": "Aucun utilisateur trouvé avec des profils publics."

View file

@ -234,7 +234,8 @@
"images": "Immagini",
"primary": "Primario",
"upload": "Caricamento",
"view_attachment": "Visualizza allegato"
"view_attachment": "Visualizza allegato",
"of": "Di"
},
"home": {
"desc_1": "Scopri, pianifica ed esplora con facilità",
@ -302,7 +303,11 @@
"both_passwords_required": "Sono necessarie entrambe le password",
"new_password": "Nuova parola d'ordine",
"reset_failed": "Impossibile reimpostare la password",
"or_3rd_party": "Oppure accedi con un servizio di terze parti"
"or_3rd_party": "Oppure accedi con un servizio di terze parti",
"no_public_adventures": "Nessuna avventura pubblica trovata",
"no_public_collections": "Nessuna collezione pubblica trovata",
"user_adventures": "Avventure utente",
"user_collections": "Collezioni utente"
},
"users": {
"no_users_found": "Nessun utente trovato con profili pubblici."

View file

@ -234,7 +234,8 @@
"images": "Afbeeldingen",
"primary": "Primair",
"upload": "Uploaden",
"view_attachment": "Bijlage bekijken"
"view_attachment": "Bijlage bekijken",
"of": "van"
},
"home": {
"desc_1": "Ontdek, plan en verken met gemak",
@ -301,8 +302,12 @@
"email_required": "E-mail is vereist",
"both_passwords_required": "Beide wachtwoorden zijn vereist",
"new_password": "Nieuw wachtwoord",
"reset_failed": "Kan het wachtwoord niet opnieuw instellen.",
"or_3rd_party": "Of log in met een service van derden"
"reset_failed": "Kan het wachtwoord niet opnieuw instellen",
"or_3rd_party": "Of log in met een service van derden",
"no_public_adventures": "Geen openbare avonturen gevonden",
"no_public_collections": "Geen openbare collecties gevonden",
"user_adventures": "Gebruikersavonturen",
"user_collections": "Gebruikerscollecties"
},
"users": {
"no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen."

View file

@ -281,7 +281,8 @@
"images": "Obrazy",
"primary": "Podstawowy",
"upload": "Wgrywać",
"view_attachment": "Zobacz załącznik"
"view_attachment": "Zobacz załącznik",
"of": "z"
},
"worldtravel": {
"country_list": "Lista krajów",
@ -326,7 +327,11 @@
"both_passwords_required": "Obydwa hasła są wymagane",
"new_password": "Nowe hasło",
"reset_failed": "Nie udało się zresetować hasła",
"or_3rd_party": "Lub zaloguj się za pomocą usługi strony trzeciej"
"or_3rd_party": "Lub zaloguj się za pomocą usługi strony trzeciej",
"no_public_adventures": "Nie znaleziono publicznych przygód",
"no_public_collections": "Nie znaleziono publicznych kolekcji",
"user_adventures": "Przygody użytkowników",
"user_collections": "Kolekcje użytkowników"
},
"users": {
"no_users_found": "Nie znaleziono użytkowników z publicznymi profilami."

View file

@ -234,7 +234,8 @@
"images": "Bilder",
"primary": "Primär",
"upload": "Ladda upp",
"view_attachment": "Visa bilaga"
"view_attachment": "Visa bilaga",
"of": "av"
},
"home": {
"desc_1": "Upptäck, planera och utforska med lätthet",
@ -326,7 +327,11 @@
"both_passwords_required": "Båda lösenorden krävs",
"new_password": "Nytt lösenord",
"reset_failed": "Det gick inte att återställa lösenordet",
"or_3rd_party": "Eller logga in med en tredjepartstjänst"
"or_3rd_party": "Eller logga in med en tredjepartstjänst",
"no_public_adventures": "Inga offentliga äventyr hittades",
"no_public_collections": "Inga offentliga samlingar hittades",
"user_adventures": "Användaräventyr",
"user_collections": "Användarsamlingar"
},
"users": {
"no_users_found": "Inga användare hittades med offentliga profiler."

View file

@ -234,7 +234,8 @@
"images": "图片",
"primary": "基本的",
"upload": "上传",
"view_attachment": "查看附件"
"view_attachment": "查看附件",
"of": "的"
},
"home": {
"desc_1": "轻松发现、规划和探索",
@ -302,7 +303,11 @@
"both_passwords_required": "两个密码都需要",
"new_password": "新密码",
"reset_failed": "重置密码失败",
"or_3rd_party": "或者使用第三方服务登录"
"or_3rd_party": "或者使用第三方服务登录",
"no_public_adventures": "找不到公共冒险",
"no_public_collections": "找不到公共收藏",
"user_adventures": "用户冒险",
"user_collections": "用户收集"
},
"worldtravel": {
"all": "全部",

View file

@ -0,0 +1,17 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from '../$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load: PageServerLoad = async (event) => {
let publicUrlFetch = await fetch(`${endpoint}/public-url/`);
let publicUrl = '';
if (!publicUrlFetch.ok) {
return redirect(302, '/');
} else {
let publicUrlJson = await publicUrlFetch.json();
publicUrl = publicUrlJson.PUBLIC_URL;
}
return redirect(302, publicUrl + '/admin/');
};

View file

@ -12,6 +12,7 @@
import toGeoJSON from '@mapbox/togeojson';
import LightbulbOn from '~icons/mdi/lightbulb-on';
import Account from '~icons/mdi/account';
let geojson: any;
@ -221,6 +222,40 @@
</div>
</div>
<div class="grid gap-2">
<div class="flex items-center gap-2">
{#if adventure.user.profile_pic}
<div class="avatar">
<div class="w-8 rounded-full">
<img src={adventure.user.profile_pic} />
</div>
</div>
{:else}
<div class="avatar placeholder">
<div class="bg-neutral text-neutral-content w-8 rounded-full">
<span class="text-lg"
>{adventure.user.first_name
? adventure.user.first_name.charAt(0)
: adventure.user.username.charAt(0)}{adventure.user.last_name
? adventure.user.last_name.charAt(0)
: ''}</span
>
</div>
</div>
{/if}
<div>
{#if adventure.user.public_profile}
<a href={`/profile/${adventure.user.username}`} class="text-base font-medium">
{adventure.user.first_name || adventure.user.username}{' '}
{adventure.user.last_name}
</a>
{:else}
<span class="text-base font-medium">
{adventure.user.first_name || adventure.user.username}{' '}
{adventure.user.last_name}
</span>
{/if}
</div>
</div>
<div class="flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
@ -241,6 +276,7 @@
>{adventure.is_public ? 'Public' : 'Private'}</span
>
</div>
{#if adventure.location}
<div class="flex items-center gap-2">
<svg

View file

@ -1,5 +1,5 @@
<script lang="ts">
import type { Adventure, Checklist, Collection, Note, Transportation } from '$lib/types';
import type { Adventure, Checklist, Collection, Hotel, Note, Transportation } from '$lib/types';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import { marked } from 'marked'; // Import the markdown parser
@ -35,6 +35,7 @@
import TransportationModal from '$lib/components/TransportationModal.svelte';
import CardCarousel from '$lib/components/CardCarousel.svelte';
import { goto } from '$app/navigation';
import HotelModal from '$lib/components/HotelModal.svelte';
export let data: PageData;
console.log(data);
@ -115,6 +116,7 @@
let numAdventures: number = 0;
let transportations: Transportation[] = [];
let hotels: Hotel[] = [];
let notes: Note[] = [];
let checklists: Checklist[] = [];
@ -174,6 +176,9 @@
if (collection.transportations) {
transportations = collection.transportations;
}
if (collection.hotels) {
hotels = collection.hotels;
}
if (collection.notes) {
notes = collection.notes;
}
@ -243,6 +248,8 @@
let adventureToEdit: Adventure | null = null;
let transportationToEdit: Transportation | null = null;
let isShowingHotelModal: boolean = false;
let hotelToEdit: Hotel | null = null;
let isAdventureModalOpen: boolean = false;
let isNoteModalOpen: boolean = false;
let noteToEdit: Note | null;
@ -260,6 +267,11 @@
isShowingTransportationModal = true;
}
function editHotel(event: CustomEvent<Hotel>) {
hotelToEdit = event.detail;
isShowingHotelModal = true;
}
function saveOrCreateAdventure(event: CustomEvent<Adventure>) {
if (adventures.find((adventure) => adventure.id === event.detail.id)) {
adventures = adventures.map((adventure) => {
@ -355,6 +367,22 @@
}
isShowingTransportationModal = false;
}
function saveOrCreateHotel(event: CustomEvent<Hotel>) {
if (hotels.find((hotel) => hotel.id === event.detail.id)) {
// Update existing hotel
hotels = hotels.map((hotel) => {
if (hotel.id === event.detail.id) {
return event.detail;
}
return hotel;
});
} else {
// Create new hotel
hotels = [event.detail, ...hotels];
}
isShowingHotelModal = false;
}
</script>
{#if isShowingLinkModal}
@ -376,6 +404,15 @@
/>
{/if}
{#if isShowingHotelModal}
<HotelModal
{hotelToEdit}
on:close={() => (isShowingHotelModal = false)}
on:save={saveOrCreateHotel}
{collection}
/>
{/if}
{#if isAdventureModalOpen}
<AdventureModal
{adventureToEdit}
@ -501,6 +538,16 @@
>
{$t('adventures.checklist')}</button
>
<button
class="btn btn-primary"
on:click={() => {
isShowingHotelModal = true;
newType = '';
hotelToEdit = null;
}}
>
{$t('adventures.hotel')}</button
>
<!-- <button
class="btn btn-primary"

View file

@ -20,11 +20,14 @@ export const load = (async (event) => {
let stats = null;
let res = await event.fetch(`${serverEndpoint}/api/stats/counts/`, {
headers: {
Cookie: `sessionid=${event.cookies.get('sessionid')}`
let res = await event.fetch(
`${serverEndpoint}/api/stats/counts/${event.locals.user.username}`,
{
headers: {
Cookie: `sessionid=${event.cookies.get('sessionid')}`
}
}
});
);
if (!res.ok) {
console.error('Failed to fetch user stats');
} else {

View file

@ -1,29 +0,0 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad, RequestEvent } from '../$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
export const load: PageServerLoad = async (event: RequestEvent) => {
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
if (!event.locals.user || !event.cookies.get('sessionid')) {
return redirect(302, '/login');
}
let sessionId = event.cookies.get('sessionid');
let stats = null;
let res = await event.fetch(`${endpoint}/api/stats/counts/`, {
headers: {
Cookie: `sessionid=${sessionId}`
}
});
if (!res.ok) {
console.error('Failed to fetch user stats');
} else {
stats = await res.json();
}
return {
user: event.locals.user,
stats
};
};

View file

@ -1,112 +0,0 @@
<script lang="ts">
export let data;
import { t } from 'svelte-i18n';
let stats: {
visited_country_count: number;
total_regions: number;
trips_count: number;
adventure_count: number;
visited_region_count: number;
total_countries: number;
visited_city_count: number;
total_cities: number;
} | null;
stats = data.stats || null;
</script>
<section class="min-h-screen bg-base-100 py-8 px-4">
<div class="flex flex-col items-center">
<!-- Profile Picture -->
{#if data.user.profile_pic}
<div class="avatar">
<div
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
>
<img src={data.user.profile_pic} alt="Profile" />
</div>
</div>
{/if}
<!-- User Name -->
{#if data.user && data.user.first_name && data.user.last_name}
<h1 class="text-4xl font-bold text-primary mt-4">
{data.user.first_name}
{data.user.last_name}
</h1>
{/if}
<p class="text-lg text-base-content mt-2">{data.user.username}</p>
<!-- Member Since -->
{#if data.user && data.user.date_joined}
<div class="mt-4 flex items-center text-center text-base-content">
<p class="text-lg font-medium">{$t('profile.member_since')}</p>
<div class="flex items-center ml-2">
<iconify-icon icon="mdi:calendar" class="text-2xl text-primary"></iconify-icon>
<p class="ml-2 text-lg">
{new Date(data.user.date_joined).toLocaleDateString(undefined, { timeZone: 'UTC' })}
</p>
</div>
</div>
{/if}
</div>
<!-- Stats Section -->
{#if stats}
<div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('profile.user_stats')}
</h2>
<div class="flex justify-center">
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-200">
<div class="stat">
<div class="stat-title">{$t('navbar.adventures')}</div>
<div class="stat-value text-center">{stats.adventure_count}</div>
</div>
<div class="stat">
<div class="stat-title">{$t('navbar.collections')}</div>
<div class="stat-value text-center">{stats.trips_count}</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_countries')}</div>
<div class="stat-value text-center">
{Math.round((stats.visited_country_count / stats.total_countries) * 100)}%
</div>
<div class="stat-desc text-center">
{stats.visited_country_count}/{stats.total_countries}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_regions')}</div>
<div class="stat-value text-center">
{Math.round((stats.visited_region_count / stats.total_regions) * 100)}%
</div>
<div class="stat-desc text-center">
{stats.visited_region_count}/{stats.total_regions}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_cities')}</div>
<div class="stat-value text-center">
{Math.round((stats.visited_city_count / stats.total_cities) * 100)}%
</div>
<div class="stat-desc text-center">
{stats.visited_city_count}/{stats.total_cities}
</div>
</div>
</div>
</div>
{/if}
</section>
<svelte:head>
<title>Profile | AdventureLog</title>
<meta name="description" content="{data.user.first_name}'s profile on AdventureLog." />
</svelte:head>

View file

@ -0,0 +1,39 @@
import { redirect, error } from '@sveltejs/kit';
import type { PageServerLoad, RequestEvent } from '../../$types';
import { t } from 'svelte-i18n';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
export const load: PageServerLoad = async (event: RequestEvent) => {
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
// @ts-ignore
let username = event.params.uuid as string;
if (!username) {
return error(404, 'Not found');
}
// let sessionId = event.cookies.get('sessionid');
let stats = null;
let res = await event.fetch(`${endpoint}/api/stats/counts/${username}`, {});
if (!res.ok) {
console.error('Failed to fetch user stats');
} else {
stats = await res.json();
}
let userData = await event.fetch(`${endpoint}/auth/user/${username}/`);
if (!userData.ok) {
return error(404, 'Not found');
}
let data = await userData.json();
return {
user: data.user,
adventures: data.adventures,
collections: data.collections,
stats: stats
};
};

View file

@ -0,0 +1,184 @@
<script lang="ts">
export let data;
import AdventureCard from '$lib/components/AdventureCard.svelte';
import CollectionCard from '$lib/components/CollectionCard.svelte';
import type { Adventure, Collection, User } from '$lib/types.js';
import { t } from 'svelte-i18n';
let stats: {
visited_country_count: number;
total_regions: number;
trips_count: number;
adventure_count: number;
visited_region_count: number;
total_countries: number;
visited_city_count: number;
total_cities: number;
} | null;
const user: User = data.user;
const adventures: Adventure[] = data.adventures;
const collections: Collection[] = data.collections;
stats = data.stats || null;
</script>
<section class="min-h-screen bg-base-100 py-8 px-4">
<div class="flex flex-col items-center">
<!-- Profile Picture -->
{#if user.profile_pic}
<div class="avatar">
<div
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
>
<img src={user.profile_pic} alt="Profile" />
</div>
</div>
{:else}
<!-- show first last initial -->
<div class="avatar">
<div
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
>
{#if user.first_name && user.last_name}
<img
src={`https://eu.ui-avatars.com/api/?name=${user.first_name}+${user.last_name}&size=250`}
alt="Profile"
/>
{:else}
<img
src={`https://eu.ui-avatars.com/api/?name=${user.username}&size=250`}
alt="Profile"
/>
{/if}
</div>
</div>
{/if}
<!-- User Name -->
{#if user && user.first_name && user.last_name}
<h1 class="text-4xl font-bold text-primary mt-4">
{user.first_name}
{user.last_name}
</h1>
{/if}
<p class="text-lg text-base-content mt-2">{user.username}</p>
<!-- Member Since -->
{#if user && user.date_joined}
<div class="mt-4 flex items-center text-center text-base-content">
<p class="text-lg font-medium">{$t('profile.member_since')}</p>
<div class="flex items-center ml-2">
<iconify-icon icon="mdi:calendar" class="text-2xl text-primary"></iconify-icon>
<p class="ml-2 text-lg">
{new Date(user.date_joined).toLocaleDateString(undefined, { timeZone: 'UTC' })}
</p>
</div>
</div>
{/if}
</div>
<!-- Stats Section -->
{#if stats}
<div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('profile.user_stats')}
</h2>
<div class="flex justify-center">
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-200">
<div class="stat">
<div class="stat-title">{$t('navbar.adventures')}</div>
<div class="stat-value text-center">{stats.adventure_count}</div>
</div>
<div class="stat">
<div class="stat-title">{$t('navbar.collections')}</div>
<div class="stat-value text-center">{stats.trips_count}</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_countries')}</div>
<div class="stat-value text-center">
{stats.visited_country_count}
</div>
<div class="stat-desc text-center">
{Math.round((stats.visited_country_count / stats.total_countries) * 100)}% {$t(
'adventures.of'
)}
{stats.total_countries}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_regions')}</div>
<div class="stat-value text-center">
{stats.visited_region_count}
</div>
<div class="stat-desc text-center">
{Math.round((stats.visited_region_count / stats.total_regions) * 100)}% {$t(
'adventures.of'
)}
{stats.total_regions}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_cities')}</div>
<div class="stat-value text-center">
{stats.visited_city_count}
</div>
<div class="stat-desc text-center">
{Math.round((stats.visited_city_count / stats.total_cities) * 100)}% {$t(
'adventures.of'
)}
{stats.total_cities}
</div>
</div>
</div>
</div>
{/if}
<!-- Adventures Section -->
<div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('auth.user_adventures')}
</h2>
{#if adventures && adventures.length === 0}
<p class="text-lg text-center text-base-content">
{$t('auth.no_public_adventures')}
</p>
{:else}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each adventures as adventure}
<AdventureCard {adventure} user={null} />
{/each}
</div>
{/if}
<!-- Collections Section -->
<div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('auth.user_collections')}
</h2>
{#if collections && collections.length === 0}
<p class="text-lg text-center text-base-content">
{$t('auth.no_public_collections')}
</p>
{:else}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each collections as collection}
<CollectionCard {collection} type={''} />
{/each}
</div>
{/if}
</section>
<svelte:head>
<title>{user.first_name || user.username}'s Profile | AdventureLog</title>
<meta name="description" content="User Profile" />
</svelte:head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB