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:
commit
57db1e088f
75 changed files with 2113 additions and 1152 deletions
|
@ -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
398
frontend/pnpm-lock.yaml
generated
|
@ -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: {}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
315
frontend/src/lib/components/HotelModal.svelte
Normal file
315
frontend/src/lib/components/HotelModal.svelte
Normal 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>
|
|
@ -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>
|
||||
|
|
336
frontend/src/lib/components/LocationDropdown.svelte
Normal file
336
frontend/src/lib/components/LocationDropdown.svelte
Normal 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>
|
|
@ -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();
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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": "全部",
|
||||
|
|
17
frontend/src/routes/admin/+page.server.ts
Normal file
17
frontend/src/routes/admin/+page.server.ts
Normal 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/');
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
|
@ -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>
|
39
frontend/src/routes/profile/[uuid]/+page.server.ts
Normal file
39
frontend/src/routes/profile/[uuid]/+page.server.ts
Normal 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
|
||||
};
|
||||
};
|
184
frontend/src/routes/profile/[uuid]/+page.svelte
Normal file
184
frontend/src/routes/profile/[uuid]/+page.svelte
Normal 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>
|
BIN
frontend/static/backgrounds/adventurelog_showcase_6.webp
Normal file
BIN
frontend/static/backgrounds/adventurelog_showcase_6.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 760 KiB |
Loading…
Add table
Add a link
Reference in a new issue