diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..8fdd954 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts index 95b303c..6367408 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -14,10 +14,10 @@ export default defineConfig({ dialect: 'postgresql', casing: 'snake_case', dbCredentials: { - url: process.env.DATABASE_URL! + url: process.env.DATABASE_URL ?? '', }, migrations: { table: 'migrations', - schema: 'public' - } + schema: 'public', + }, }); diff --git a/package.json b/package.json index 5be675a..d5ca415 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@chromatic-com/storybook": "^3.2.3", + "@dicebear/collection": "^9.2.2", + "@dicebear/converter": "^9.2.2", + "@dicebear/core": "^9.2.2", "@faker-js/faker": "^9.3.0", "@playwright/test": "^1.45.3", "@storybook/addon-essentials": "^8.4.7", @@ -35,6 +38,8 @@ "@sveltejs/enhanced-img": "^0.4.4", "@sveltejs/kit": "^2.9.0", "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tanstack/svelte-query": "^5.62.9", + "@tanstack/svelte-query-devtools": "^5.62.9", "@types/cookie": "^1.0.0", "@types/node": "^22.10.2", "@types/pg": "^8.11.10", @@ -48,7 +53,6 @@ "storybook": "^8.4.7", "svelte": "^5.16.0", "svelte-check": "^4.0.0", - "svelte-headless-table": "^0.18.3", "svelte-meta-tags": "^4.0.4", "svelte-preprocess": "^6.0.3", "svelte-sequential-preprocessor": "^2.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4f2e27..13227e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,6 +123,15 @@ importers: '@chromatic-com/storybook': specifier: ^3.2.3 version: 3.2.3(react@18.3.1)(storybook@8.4.7) + '@dicebear/collection': + specifier: ^9.2.2 + version: 9.2.2(@dicebear/core@9.2.2) + '@dicebear/converter': + specifier: ^9.2.2 + version: 9.2.2 + '@dicebear/core': + specifier: ^9.2.2 + version: 9.2.2 '@faker-js/faker': specifier: ^9.3.0 version: 9.3.0 @@ -162,6 +171,12 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^5.0.3 version: 5.0.3(svelte@5.16.0)(vite@6.0.6(@types/node@22.10.2)(jiti@1.21.7)(yaml@2.6.1)) + '@tanstack/svelte-query': + specifier: ^5.62.9 + version: 5.62.9(svelte@5.16.0) + '@tanstack/svelte-query-devtools': + specifier: ^5.62.9 + version: 5.62.9(@tanstack/svelte-query@5.62.9(svelte@5.16.0))(svelte@5.16.0) '@types/cookie': specifier: ^1.0.0 version: 1.0.0 @@ -201,9 +216,6 @@ importers: svelte-check: specifier: ^4.0.0 version: 4.1.1(picomatch@4.0.2)(svelte@5.16.0)(typescript@5.7.2) - svelte-headless-table: - specifier: ^0.18.3 - version: 0.18.3(svelte@5.16.0) svelte-meta-tags: specifier: ^4.0.4 version: 4.0.4(svelte@5.16.0)(typescript@5.7.2) @@ -342,6 +354,200 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + '@dicebear/adventurer-neutral@9.2.2': + resolution: {integrity: sha512-XVAjhUWjav6luTZ7txz8zVJU/H0DiUy4uU1Z7IO5MDO6kWvum+If1+0OUgEWYZwM+RDI7rt2CgVP910DyZGd1w==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/adventurer@9.2.2': + resolution: {integrity: sha512-WjBXCP9EXbUul2zC3BS2/R3/4diw1uh/lU4jTEnujK1mhqwIwanFboIMzQsasNNL/xf+m3OHN7MUNJfHZ1fLZA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/avataaars-neutral@9.2.2': + resolution: {integrity: sha512-pRj16P27dFDBI3LtdiHUDwIXIGndHAbZf5AxaMkn6/+0X93mVQ/btVJDXyW0G96WCsyC88wKAWr6/KJotPxU6Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/avataaars@9.2.2': + resolution: {integrity: sha512-WqJPQEt0OhBybTpI0TqU1uD1pSk9M2+VPIwvBye/dXo46b+0jHGpftmxjQwk6tX8z0+mRko8pwV5n+cWht1/+w==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/big-ears-neutral@9.2.2': + resolution: {integrity: sha512-IPHt8fi3dv9cyfBJBZ4s8T+PhFCrQvOCf91iRHBT3iOLNPdyZpI5GNLmGiV0XMAvIDP5NvA5+f6wdoBLhYhbDA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/big-ears@9.2.2': + resolution: {integrity: sha512-hz4UXdPq4qqZpu0YVvlqM4RDFhk5i0WgPcuwj/MOLlgTjuj63uHUhCQSk6ZiW1DQOs12qpwUBMGWVHxBRBas9g==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/big-smile@9.2.2': + resolution: {integrity: sha512-D4td0GL8or1nTNnXvZqkEXlzyqzGPWs3znOnm1HIohtFTeIwXm72Ob2lNDsaQJSJvXmVlwaQQ0CCTvyCl8Stjw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/bottts-neutral@9.2.2': + resolution: {integrity: sha512-lSgpqmSJtlnyxVuUgNdBwyzuA0O9xa5zRJtz7x2KyWbicXir5iYdX0MVMCkp1EDvlcxm9rGJsclktugOyakTlw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/bottts@9.2.2': + resolution: {integrity: sha512-wugFkzw8JNWV1nftq/Wp/vmQsLAXDxrMtRK3AoMODuUpSVoP3EHRUfKS043xggOsQFvoj0HZ7kadmhn0AMLf5A==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/collection@9.2.2': + resolution: {integrity: sha512-vZAmXhPWCK3sf8Fj9/QflFC6XOLroJOT5K1HdnzHaPboEvffUQideGCrrEamnJtlH0iF0ZDXh8gqmwy2fu+yHA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/converter@9.2.2': + resolution: {integrity: sha512-704MOowKUOGI88tOJIYFSkd2HXbkNtPdjMrPTx7j+7gAqImCBZ298KVUk0TCo36VCUfN0EtgngUuBZUxyPTadQ==} + engines: {node: '>=18.0.0'} + + '@dicebear/core@9.2.2': + resolution: {integrity: sha512-ROhgHG249dPtcXgBHcqPEsDeAPRPRD/9d+tZCjLYyueO+cXDlIA8dUlxpwIVcOuZFvCyW6RJtqo8BhNAi16pIQ==} + engines: {node: '>=18.0.0'} + + '@dicebear/croodles-neutral@9.2.2': + resolution: {integrity: sha512-/4mNirxoQ+z1kHXnpDRbJ1JV1ZgXogeTeNp0MaFYxocCgHfJ7ckNM23EE1I7akoo9pqPxrKlaeNzGAjKHdS9vA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/croodles@9.2.2': + resolution: {integrity: sha512-OzvAXQWsOgMwL3Sl+lBxCubqSOWoBJpC78c4TKnNTS21rR63TtXUyVdLLzgKVN4YHRnvMgtPf8F/W9YAgIDK4w==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/dylan@9.2.2': + resolution: {integrity: sha512-s7e3XliC1YXP+Wykj+j5kwdOWFRXFzYHYk/PB4oZ1F3sJandXiG0HS4chaNu4EoP0yZgKyFMUVTGZx+o6tMaYg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/fun-emoji@9.2.2': + resolution: {integrity: sha512-M+rYTpB3lfwz18f+/i+ggNwNWUoEj58SJqXJ1wr7Jh/4E5uL+NmJg9JGwYNaVtGbCFrKAjSaILNUWGQSFgMfog==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/glass@9.2.2': + resolution: {integrity: sha512-imCMxcg+XScHYtQq2MUv1lCzhQSCUglMlPSezKEpXhTxgbgUpmGlSGVkOfmX5EEc7SQowKkF1W/1gNk6CXvBaQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/icons@9.2.2': + resolution: {integrity: sha512-Tqq2OVCdS7J02DNw58xwlgLGl40sWEckbqXT3qRvIF63FfVq+wQZBGuhuiyAURcSgvsc3h2oQeYFi9iXh7HTOA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/identicon@9.2.2': + resolution: {integrity: sha512-POVKFulIrcuZf3rdAgxYaSm2XUg/TJg3tg9zq9150reEGPpzWR7ijyJ03dzAADPzS3DExfdYVT9+z3JKwwJnTQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/initials@9.2.2': + resolution: {integrity: sha512-/xNnsEmsstWjmF77htAOuwOMhFlP6eBVXgcgFlTl/CCH/Oc6H7t0vwX1he8KLQBBzjGpvJcvIAn4Wh9rE4D5/A==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/lorelei-neutral@9.2.2': + resolution: {integrity: sha512-Eys9Os6nt2Xll7Mvu66CfRR2YggTopWcmFcRZ9pPdohS96kT0MsLI2iTcfZXQ51K8hvT3IbwoGc86W8n0cDxAQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/lorelei@9.2.2': + resolution: {integrity: sha512-koXqVr/vcWUPo00VP5H6Czsit+uF1tmwd2NK7Q/e34/9Sd1f4QLLxHjjBNm/iNjCI1+UNTOvZ2Qqu0N5eo7Flw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/micah@9.2.2': + resolution: {integrity: sha512-NCajcJV5yw8uMKiACp694w1T/UyYme2CUEzyTzWHgWnQ+drAuCcH8gpAoLWd67viNdQB/MTpNlaelUgTjmI4AQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/miniavs@9.2.2': + resolution: {integrity: sha512-vvkWXttdw+KHF3j+9qcUFzK+P0nbNnImGjvN48wwkPIh2h08WWFq0MnoOls4IHwUJC4GXBjWtiyVoCxz6hhtOA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/notionists-neutral@9.2.2': + resolution: {integrity: sha512-AhOzk+lz6kB4uxGun8AJhV+W1nttnMlxmxd+5KbQ/txCIziYIaeD3il44wsAGegEpGFvAZyMYtR/jjfHcem3TA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/notionists@9.2.2': + resolution: {integrity: sha512-Z9orRaHoj7Y9Ap4wEu8XOrFACsG1KbbBQUPV1R50uh6AHwsyNrm4cS84ICoGLvxgLNHHOae3YCjd8aMu2z19zg==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/open-peeps@9.2.2': + resolution: {integrity: sha512-6PeQDHYyjvKrGSl/gP+RE5dSYAQGKpcGnM65HorgyTIugZK7STo0W4hvEycedupZ3MCCEH8x/XyiChKM2sHXog==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/personas@9.2.2': + resolution: {integrity: sha512-705+ObNLC0w1fcgE/Utav+8bqO+Esu53TXegpX5j7trGEoIMf2bThqJGHuhknZ3+T2az3Wr89cGyOGlI0KLzLA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/pixel-art-neutral@9.2.2': + resolution: {integrity: sha512-CdUY77H6Aj7dKLW3hdkv7tu0XQJArUjaWoXihQxlhl3oVYplWaoyu9omYy5pl8HTqs8YgVTGljjMXYoFuK0JUw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/pixel-art@9.2.2': + resolution: {integrity: sha512-BvbFdrpzQl04+Y9UsWP63YGug+ENGC7GMG88qbEFWxb/IqRavGa4H3D0T4Zl2PSLiw7f2Ctv98bsCQZ1PtCznQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/rings@9.2.2': + resolution: {integrity: sha512-eD1J1k364Arny+UlvGrk12HP/XGG6WxPSm4BarFqdJGSV45XOZlwqoi7FlcMr9r9yvE/nGL8OizbwMYusEEdjw==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/shapes@9.2.2': + resolution: {integrity: sha512-e741NNWBa7fg0BjomxXa0fFPME2XCIR0FA+VHdq9AD2taTGHEPsg5x1QJhCRdK6ww85yeu3V3ucpZXdSrHVw5Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + + '@dicebear/thumbs@9.2.2': + resolution: {integrity: sha512-FkPLDNu7n5kThLSk7lR/0cz/NkUqgGdZGfLZv6fLkGNGtv6W+e2vZaO7HCXVwIgJ+II+kImN41zVIZ6Jlll7pQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@dicebear/core': ^9.0.0 + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -1349,6 +1555,9 @@ packages: resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} engines: {node: '>=10'} + '@photostructure/tz-lookup@11.0.0': + resolution: {integrity: sha512-QMV5/dWtY/MdVPXZs/EApqzyhnqDq1keYEqpS+Xj2uidyaqw2Nk/fWcsszdruIXjdqp1VoWNzsgrO6bUHU1mFw==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1365,6 +1574,82 @@ packages: resolution: {integrity: sha512-B4iV6QxW//Fn17+qF1EMZRmoThIUJlCtcO85yoRDJnMyHeAthjz4ig9OTkfGGXKtQhcdPX0me75gU5K9J897+w==} engines: {node: '>=18.16.0'} + '@resvg/resvg-js-android-arm-eabi@2.6.2': + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@resvg/resvg-js-android-arm64@2.6.2': + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@resvg/resvg-js-darwin-arm64@2.6.2': + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@resvg/resvg-js-darwin-x64@2.6.2': + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@resvg/resvg-js@2.6.2': + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} + engines: {node: '>= 10'} + '@rollup/plugin-commonjs@28.0.2': resolution: {integrity: sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==} engines: {node: '>=16.0.0 || 14 >= 14.17'} @@ -1761,6 +2046,23 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' + '@tanstack/query-core@5.62.9': + resolution: {integrity: sha512-lwePd8hNYhyQ4nM/iRQ+Wz2cDtspGeZZHFZmCzHJ7mfKXt+9S301fULiY2IR2byJYY6Z03T427E5PoVfMexHjw==} + + '@tanstack/query-devtools@5.62.9': + resolution: {integrity: sha512-b1NZzDLVf6laJsB1Cfm3ieuYzM+WqoO8qpm9v+3Etwd+Ph4zkhUMiT+wcWj5AhEPsXiRodKYiiW048VDNdBxNg==} + + '@tanstack/svelte-query-devtools@5.62.9': + resolution: {integrity: sha512-baESJCUDBIJIRiwfodW/j0BU8c5uADk37A0UfadOU8cWFt9M67zawKltJmut7AoEXbwLWYqTO+3HU2XKCyJEWw==} + peerDependencies: + '@tanstack/svelte-query': ^5.62.9 + svelte: ^3.54.0 || ^4.0.0 || ^5.0.0-next.0 + + '@tanstack/svelte-query@5.62.9': + resolution: {integrity: sha512-2M/CpePioU4IRw1OOdA+/KwA+0swJAb3c0uipFOoWkuT11uC4tTe8UD/lYRQpJYFFn5hML7KYRxVOW0HH60XiA==} + peerDependencies: + svelte: ^3.54.0 || ^4.0.0 || ^5.0.0-next.0 + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -1800,6 +2102,9 @@ packages: '@types/jsonwebtoken@9.0.7': resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -2027,6 +2332,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + batch-cluster@13.0.0: + resolution: {integrity: sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og==} + engines: {node: '>=14'} + before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} @@ -2695,6 +3004,17 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + exiftool-vendored.exe@13.0.0: + resolution: {integrity: sha512-4zAMuFGgxZkOoyQIzZMHv1HlvgyJK3AkNqjAgm8A8V0UmOZO7yv3pH49cDV1OduzFJqgs6yQ6eG4OGydhKtxlg==} + os: [win32] + + exiftool-vendored.pl@13.0.1: + resolution: {integrity: sha512-+BRRzjselpWudKR0ltAW5SUt9T82D+gzQN8DdOQUgnSVWWp7oLCeTGBRptbQz+436Ihn/mPzmo/xnf0cv/Qw1A==} + os: ['!win32'] + + exiftool-vendored@28.8.0: + resolution: {integrity: sha512-R7tirJLr9fWuH9JS/KFFLB+O7jNGKuPXGxREc6YybYangEudGb+X8ERsYXk9AifMiAWh/2agNfbgkbcQcF+MxA==} + expect-type@1.1.0: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} @@ -2898,6 +3218,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} @@ -3188,6 +3512,10 @@ packages: peerDependencies: svelte: ^3 || ^4 || ^5.0.0-next.42 + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -4018,16 +4346,6 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte-headless-table@0.18.3: - resolution: {integrity: sha512-1zVnqXW0dvn6ZceYa94k+ziK+w5Dj9nlWYTQGXBv2JhM0resj9w7CWpclZK1TJwAALfEeH4InPBPO87L5fr+nQ==} - peerDependencies: - svelte: ^4.0.0 - - svelte-keyed@2.0.0: - resolution: {integrity: sha512-7TeEn+QbJC2OJrHiuM0T8vMBkms3DNpTE+Ir+NtnVBnBMA78aL4f1ft9t0Hn/pBbD/TnIXi4YfjFRAgtN+DZ5g==} - peerDependencies: - svelte: ^4.0.0 - svelte-meta-tags@4.0.4: resolution: {integrity: sha512-i0vgxGreM3lXTTxLSuPQLE1n56KAFACHWIXRj7fJCTpd/5D16O97Ha/OXDZS4Lsk+D347VEK4LeMoacsftbeKw==} peerDependencies: @@ -4112,11 +4430,6 @@ packages: typescript: optional: true - svelte-render@2.0.1: - resolution: {integrity: sha512-RpB0SurwXm4xhjvHHtjeqMmvd645FURb79GFOotScOSqnKK5vpqBgoBPGC0pp+E/eZgDSQ9rRAdn/+N4ys1mXQ==} - peerDependencies: - svelte: ^4.0.0 - svelte-sequential-preprocessor@2.0.2: resolution: {integrity: sha512-DIFm0kSNscVxtBmKkBiygAHB5otoqN1aVmJ3t57jZhJfCB7Np/lUSoTtSrvPFjmlBbMeOsb1VQ06cut1+rBYOg==} engines: {node: '>=16'} @@ -4126,11 +4439,6 @@ packages: peerDependencies: svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.1 - svelte-subscribe@2.0.1: - resolution: {integrity: sha512-eKXIjLxB4C7eQWPqKEdxcGfNXm2g/qJ67zmEZK/GigCZMfrTR3m7DPY93R6MX+5uoqM1FRYxl8LZ1oy4URWi2A==} - peerDependencies: - svelte: ^4.0.0 - svelte-toolbelt@0.4.6: resolution: {integrity: sha512-k8OUvXBUifHZcAlWeY/HLg/4J0v5m2iOfOhn8fDmjt4AP8ZluaDh9eBFus9lFiLX6O5l6vKqI1dKL5wy7090NQ==} engines: {node: '>=18', pnpm: '>=8.7.0'} @@ -4239,6 +4547,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4656,6 +4971,171 @@ snapshots: - '@chromatic-com/playwright' - react + '@dicebear/adventurer-neutral@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/adventurer@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/avataaars-neutral@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/avataaars@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/big-ears-neutral@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/big-ears@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/big-smile@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/bottts-neutral@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/bottts@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/collection@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/adventurer': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/adventurer-neutral': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/avataaars': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/avataaars-neutral': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/big-ears': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/big-ears-neutral': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/big-smile': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/bottts': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/bottts-neutral': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/core': 9.2.2 + '@dicebear/croodles': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/croodles-neutral': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/dylan': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/fun-emoji': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/glass': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/icons': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/identicon': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/initials': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/lorelei': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/lorelei-neutral': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/micah': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/miniavs': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/notionists': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/notionists-neutral': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/open-peeps': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/personas': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/pixel-art': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/pixel-art-neutral': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/rings': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/shapes': 9.2.2(@dicebear/core@9.2.2) + '@dicebear/thumbs': 9.2.2(@dicebear/core@9.2.2) + + '@dicebear/converter@9.2.2': + dependencies: + '@resvg/resvg-js': 2.6.2 + exiftool-vendored: 28.8.0 + sharp: 0.33.5 + tmp-promise: 3.0.3 + + '@dicebear/core@9.2.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@dicebear/croodles-neutral@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/croodles@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/dylan@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/fun-emoji@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/glass@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/icons@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/identicon@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/initials@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/lorelei-neutral@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/lorelei@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/micah@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/miniavs@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/notionists-neutral@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/notionists@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/open-peeps@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/personas@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/pixel-art-neutral@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/pixel-art@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/rings@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/shapes@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + + '@dicebear/thumbs@9.2.2(@dicebear/core@9.2.2)': + dependencies: + '@dicebear/core': 9.2.2 + '@drizzle-team/brocli@0.10.2': {} '@emnapi/runtime@1.3.1': @@ -5505,6 +5985,8 @@ snapshots: '@phc/format@1.0.0': {} + '@photostructure/tz-lookup@11.0.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -5517,6 +5999,57 @@ snapshots: '@poppinss/macroable@1.0.3': optional: true + '@resvg/resvg-js-android-arm-eabi@2.6.2': + optional: true + + '@resvg/resvg-js-android-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-x64@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js@2.6.2': + optionalDependencies: + '@resvg/resvg-js-android-arm-eabi': 2.6.2 + '@resvg/resvg-js-android-arm64': 2.6.2 + '@resvg/resvg-js-darwin-arm64': 2.6.2 + '@resvg/resvg-js-darwin-x64': 2.6.2 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 + '@rollup/plugin-commonjs@28.0.2(rollup@4.29.1)': dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.29.1) @@ -5993,6 +6526,22 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.17 + '@tanstack/query-core@5.62.9': {} + + '@tanstack/query-devtools@5.62.9': {} + + '@tanstack/svelte-query-devtools@5.62.9(@tanstack/svelte-query@5.62.9(svelte@5.16.0))(svelte@5.16.0)': + dependencies: + '@tanstack/query-devtools': 5.62.9 + '@tanstack/svelte-query': 5.62.9(svelte@5.16.0) + esm-env: 1.2.1 + svelte: 5.16.0 + + '@tanstack/svelte-query@5.62.9(svelte@5.16.0)': + dependencies: + '@tanstack/query-core': 5.62.9 + svelte: 5.16.0 + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.26.2 @@ -6032,13 +6581,14 @@ snapshots: '@types/estree@1.0.6': {} - '@types/json-schema@7.0.15': - optional: true + '@types/json-schema@7.0.15': {} '@types/jsonwebtoken@9.0.7': dependencies: '@types/node': 22.10.2 + '@types/luxon@3.4.2': {} + '@types/mdx@2.0.13': {} '@types/node@22.10.2': @@ -6287,6 +6837,8 @@ snapshots: balanced-match@1.0.2: {} + batch-cluster@13.0.0: {} + before-after-hook@2.2.3: {} better-opn@3.0.2: @@ -6922,6 +7474,23 @@ snapshots: eventemitter3@5.0.1: {} + exiftool-vendored.exe@13.0.0: + optional: true + + exiftool-vendored.pl@13.0.1: + optional: true + + exiftool-vendored@28.8.0: + dependencies: + '@photostructure/tz-lookup': 11.0.0 + '@types/luxon': 3.4.2 + batch-cluster: 13.0.0 + he: 1.2.0 + luxon: 3.5.0 + optionalDependencies: + exiftool-vendored.exe: 13.0.0 + exiftool-vendored.pl: 13.0.1 + expect-type@1.1.0: {} express-rate-limit@7.5.0(express@4.21.2): @@ -7147,6 +7716,8 @@ snapshots: dependencies: function-bind: 1.1.2 + he@1.2.0: {} + help-me@5.0.0: {} hono-pino@0.7.0(hono@4.6.14)(pino@9.6.0): @@ -7419,6 +7990,8 @@ snapshots: dependencies: svelte: 5.16.0 + luxon@3.5.0: {} + lz-string@1.5.0: {} magic-string@0.30.17: @@ -8270,17 +8843,6 @@ snapshots: transitivePeerDependencies: - picomatch - svelte-headless-table@0.18.3(svelte@5.16.0): - dependencies: - svelte: 5.16.0 - svelte-keyed: 2.0.0(svelte@5.16.0) - svelte-render: 2.0.1(svelte@5.16.0) - svelte-subscribe: 2.0.1(svelte@5.16.0) - - svelte-keyed@2.0.0(svelte@5.16.0): - dependencies: - svelte: 5.16.0 - svelte-meta-tags@4.0.4(svelte@5.16.0)(typescript@5.7.2): dependencies: schema-dts: 1.1.2(typescript@5.7.2) @@ -8313,11 +8875,6 @@ snapshots: postcss-load-config: 4.0.2(postcss@8.4.49) typescript: 5.7.2 - svelte-render@2.0.1(svelte@5.16.0): - dependencies: - svelte: 5.16.0 - svelte-subscribe: 2.0.1(svelte@5.16.0) - svelte-sequential-preprocessor@2.0.2: dependencies: svelte: 4.2.19 @@ -8327,10 +8884,6 @@ snapshots: dependencies: svelte: 5.16.0 - svelte-subscribe@2.0.1(svelte@5.16.0): - dependencies: - svelte: 5.16.0 - svelte-toolbelt@0.4.6(svelte@5.16.0): dependencies: clsx: 2.1.1 @@ -8502,6 +9055,12 @@ snapshots: tinyspy@3.0.2: {} + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.3 + + tmp@0.2.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 diff --git a/project.inlang/project_id b/project.inlang/project_id new file mode 100644 index 0000000..0027e25 --- /dev/null +++ b/project.inlang/project_id @@ -0,0 +1 @@ +eb7d81211e6faa5e843476a33150228f5db02e3c62748d6eff4e45b709691d18 \ No newline at end of file diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 1d99618..a5e522c 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -4,5 +4,7 @@ import { sequence } from '@sveltejs/kit/hooks'; import { startServer } from '$lib/server/api'; const handleParaglide: Handle = i18n.handle(); + startServer(); + export const handle: Handle = sequence(handleParaglide); diff --git a/src/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..1f52856 --- /dev/null +++ b/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,16 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-description.svelte b/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..da02664 --- /dev/null +++ b/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,16 @@ + + +

+ {@render children?.()} +

diff --git a/src/lib/components/ui/card/card-footer.svelte b/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..6894149 --- /dev/null +++ b/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,16 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-header.svelte b/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..1baa92c --- /dev/null +++ b/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,16 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-title.svelte b/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..f7d59c1 --- /dev/null +++ b/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,25 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card.svelte b/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..3e3a4ed --- /dev/null +++ b/src/lib/components/ui/card/card.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/index.ts b/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..0f9084d --- /dev/null +++ b/src/lib/components/ui/card/index.ts @@ -0,0 +1,22 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, +}; diff --git a/src/lib/components/ui/table/index.ts b/src/lib/components/ui/table/index.ts new file mode 100644 index 0000000..14695c8 --- /dev/null +++ b/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from "./table.svelte"; +import Body from "./table-body.svelte"; +import Caption from "./table-caption.svelte"; +import Cell from "./table-cell.svelte"; +import Footer from "./table-footer.svelte"; +import Head from "./table-head.svelte"; +import Header from "./table-header.svelte"; +import Row from "./table-row.svelte"; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow, +}; diff --git a/src/lib/components/ui/table/table-body.svelte b/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 0000000..6c20c01 --- /dev/null +++ b/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,16 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-caption.svelte b/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 0000000..2b0cba5 --- /dev/null +++ b/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,16 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-cell.svelte b/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 0000000..0536552 --- /dev/null +++ b/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-footer.svelte b/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 0000000..0267c47 --- /dev/null +++ b/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,16 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-head.svelte b/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 0000000..5300ce6 --- /dev/null +++ b/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-header.svelte b/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 0000000..684a57b --- /dev/null +++ b/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,16 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table-row.svelte b/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 0000000..9e693bc --- /dev/null +++ b/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/table/table.svelte b/src/lib/components/ui/table/table.svelte new file mode 100644 index 0000000..e3a95b6 --- /dev/null +++ b/src/lib/components/ui/table/table.svelte @@ -0,0 +1,18 @@ + + +
+ + {@render children?.()} +
+
diff --git a/src/lib/components/user-avatar.svelte b/src/lib/components/user-avatar.svelte new file mode 100644 index 0000000..d24a145 --- /dev/null +++ b/src/lib/components/user-avatar.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/src/lib/server/api/databases/postgres/drizzle-schema.ts b/src/lib/server/api/databases/postgres/drizzle-schema.ts index d83f81f..3157b16 100644 --- a/src/lib/server/api/databases/postgres/drizzle-schema.ts +++ b/src/lib/server/api/databases/postgres/drizzle-schema.ts @@ -1 +1,3 @@ +export * from '../../roles/tables/roles.table'; +export * from '../../users/tables/user-roles.table'; export * from '../../users/tables/users.table'; diff --git a/src/lib/server/api/mfa/mfa.controller.ts b/src/lib/server/api/mfa/mfa.controller.ts new file mode 100644 index 0000000..8542768 --- /dev/null +++ b/src/lib/server/api/mfa/mfa.controller.ts @@ -0,0 +1,107 @@ +import { requireFullAuth, requireTempAuth } from '$lib/server/api/common/middleware/require-auth.middleware'; +import { Controller } from '$lib/server/api/common/types/controller'; +import { verifyTotpDto } from '$lib/server/api/dtos/verify-totp.dto'; +import { TotpService } from '$lib/server/api/mfa/totp.service'; +import { RecoveryCodesService } from '$lib/server/api/users/recovery-codes.service'; +import { UsersService } from '$lib/server/api/users/users.service'; +import { StatusCodes } from '$lib/utils/status-codes'; +import { zValidator } from '@hono/zod-validator'; +import { inject, injectable } from '@needle-di/core'; +import { decodeBase64 } from '@oslojs/encoding'; +import { cookieExpiresAt, createSessionTokenCookie, setSessionCookie } from '../common/utils/cookies'; +import { CredentialsType } from '../databases/postgres/tables'; +import { createTwoFactorSchema } from '../dtos/create-totp.dto'; +import { LoginRequestsService } from '../login/loginrequest.service'; + +@injectable() +export class MfaController extends Controller { + constructor( + private loginRequestService = inject(LoginRequestsService), + private recoveryCodesService = inject(RecoveryCodesService), + private totpService = inject(TotpService), + private usersService = inject(UsersService), + ) { + super(); + } + + routes() { + return this.controller + .get('/totp', requireTempAuth, async (c) => { + const user = c.var.user; + c.var.logger.info(`The user ${user.id} is requesting TOTP credentials`); + const totpCredential = await this.totpService.findOneByUserId(user.id); + return c.json({ totpCredential }); + }) + .post('/totp', requireTempAuth, zValidator('json', createTwoFactorSchema), async (c) => { + const user = c.var.user; + const { key } = c.req.valid('json'); + const totpCredential = await this.totpService.create(user.id, decodeBase64(key)); + if (totpCredential) { + await this.usersService.updateUser(user.id, { mfa_enabled: true }); + return c.json({ totpCredential }); + } + return c.status(StatusCodes.INTERNAL_SERVER_ERROR); + }) + .delete('/totp', requireFullAuth, async (c) => { + const user = c.var.user; + try { + await this.totpService.deleteOneByUserIdAndType(user.id, CredentialsType.TOTP); + await this.recoveryCodesService.deleteAllRecoveryCodesByUserId(user.id); + await this.usersService.updateUser(user.id, { mfa_enabled: false }); + console.log('TOTP deleted'); + return c.body(null, StatusCodes.NO_CONTENT); + } catch (e) { + console.error(e); + return c.status(StatusCodes.INTERNAL_SERVER_ERROR); + } + }) + .get('/totp/recoveryCodes', requireFullAuth, async (c) => { + const user = c.var.user; + // You can only view recovery codes once and that is on creation + const existingCodes = await this.recoveryCodesService.findAllRecoveryCodesByUserId(user.id); + if (existingCodes && existingCodes.length > 0) { + console.log('Recovery Codes found', existingCodes); + // Filter out codes that are not used and only return the code + const codes = existingCodes.filter((code) => !code.used).map((code) => code.code); + return c.json({ recoveryCodes: codes }); + } + const recoveryCodes = await this.recoveryCodesService.createRecoveryCodes(user.id); + return c.json({ recoveryCodes }); + }) + .post('/totp/recoveryCodes', requireFullAuth, zValidator('json', verifyTotpDto), async (c) => { + try { + const user = c.var.user; + const { code } = c.req.valid('json'); + c.var.logger.info(`Verifying code ${code} for user ${user.id}`); + const verified = await this.recoveryCodesService.verify(user.id, code); + if (verified) { + return c.json({}, StatusCodes.OK); + } + return c.json('Invalid code', StatusCodes.BAD_REQUEST); + } catch (e) { + console.error(e); + return c.status(StatusCodes.INTERNAL_SERVER_ERROR); + } + }) + .post('/totp/verify', requireTempAuth, zValidator('json', verifyTotpDto), async (c) => { + try { + const user = c.var.user; + const { code } = c.req.valid('json'); + c.var.logger.info(`Verifying code ${code} for user ${user.id}`); + const verified = await this.totpService.verify(user.id, code); + if (verified) { + await this.usersService.updateUser(user.id, { mfa_enabled: true }); + const session = await this.loginRequestService.createUserSession(user.id, c.req, true, true); + const sessionCookie = createSessionTokenCookie(session.id, cookieExpiresAt); + console.log('set cookie', sessionCookie); + setSessionCookie(c, sessionCookie); + return c.json({}, StatusCodes.OK); + } + return c.json('Invalid code', StatusCodes.BAD_REQUEST); + } catch (e) { + console.error(e); + return c.status(StatusCodes.INTERNAL_SERVER_ERROR); + } + }); + } +} diff --git a/src/lib/server/api/mfa/tables/two-factor.table.ts b/src/lib/server/api/mfa/tables/two-factor.table.ts new file mode 100644 index 0000000..b432619 --- /dev/null +++ b/src/lib/server/api/mfa/tables/two-factor.table.ts @@ -0,0 +1,36 @@ +import { type InferSelectModel, relations } from 'drizzle-orm'; +import { boolean, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'; +import { id, timestamps } from '../../common/utils/drizzle'; +import { usersTable } from '../../users/tables/users.table'; +import { generateId } from '../../common/utils/crypto'; + +/* -------------------------------------------------------------------------- */ +/* Table */ +/* -------------------------------------------------------------------------- */ +export const twoFactorTable = pgTable('two_factor', { + id: id() + .primaryKey() + .$defaultFn(() => generateId()), + secret: text().notNull(), + enabled: boolean().notNull().default(false), + user_id: id() + .notNull() + .references(() => usersTable.id) + .unique('two_factor_user_id_unique'), + ...timestamps, +}); + +/* -------------------------------------------------------------------------- */ +/* Relations */ +/* -------------------------------------------------------------------------- */ +export const emailVerificationsRelations = relations(twoFactorTable, ({ one }) => ({ + user: one(usersTable, { + fields: [twoFactorTable.user_id], + references: [usersTable.id], + }), +})); + +/* -------------------------------------------------------------------------- */ +/* Types */ +/* -------------------------------------------------------------------------- */ +export type TwoFactor = InferSelectModel; diff --git a/src/lib/server/api/mfa/totp.service.ts b/src/lib/server/api/mfa/totp.service.ts new file mode 100644 index 0000000..1ff3499 --- /dev/null +++ b/src/lib/server/api/mfa/totp.service.ts @@ -0,0 +1,57 @@ +import { inject, injectable } from '@needle-di/core'; +import { decodeBase64, encodeBase64 } from '@oslojs/encoding'; +import { generateTOTP, verifyTOTP } from '@oslojs/otp'; +import { EncryptionService } from '../common/services/encryption.service'; +import type { CredentialsType } from '../databases/postgres/tables'; +import { CredentialsRepository } from '../users/credentials.repository'; + +@injectable() +export class TotpService { + constructor( + private credentialsRepository = inject(CredentialsRepository), + private encryptionService = inject(EncryptionService), + ) {} + + async findOneByUserId(userId: string) { + return this.credentialsRepository.findTOTPCredentialsByUserId(userId); + } + + async findOneByUserIdOrThrow(userId: string) { + const credential = await this.findOneByUserId(userId); + if (!credential) { + throw new Error('TOTP credential not found'); + } + return credential; + } + + async create(userId: string, key: Uint8Array) { + try { + return await this.credentialsRepository.create({ + user_id: userId, + secret_data: encodeBase64(this.encryptionService.encrypt(key)), + type: 'totp', + }); + } catch (e) { + console.error(e); + return null; + } + } + + async deleteOneByUserId(userId: string) { + return this.credentialsRepository.deleteByUserId(userId); + } + + async deleteOneByUserIdAndType(userId: string, type: CredentialsType) { + return this.credentialsRepository.deleteByUserIdAndType(userId, type); + } + + async verify(userId: string, code: string) { + const credential = await this.credentialsRepository.findTOTPCredentialsByUserId(userId); + console.log(`TOTP credential: ${JSON.stringify(credential)}`); + if (!credential) { + throw new Error('TOTP credential not found'); + } + const secret = this.encryptionService.decrypt(decodeBase64(credential.secret_data)); + return verifyTOTP(secret, 30, 6, code); + } +} diff --git a/src/lib/server/api/roles/tables/roles.table.ts b/src/lib/server/api/roles/tables/roles.table.ts new file mode 100644 index 0000000..d9736e5 --- /dev/null +++ b/src/lib/server/api/roles/tables/roles.table.ts @@ -0,0 +1,38 @@ +import { type InferSelectModel, relations } from 'drizzle-orm'; +import { pgTable, text } from 'drizzle-orm/pg-core'; +import { user_roles } from '../../users/tables/user-roles.table'; +import { timestamps } from '../../common/utils/drizzle'; +import { id } from '../../common/utils/drizzle'; +import { generateId } from '../../common/utils/crypto'; + +export enum RoleName { + ADMIN = 'admin', + EDITOR = 'editor', + MODERATOR = 'moderator', + USER = 'user', +} + +/* -------------------------------------------------------------------------- */ +/* Table */ +/* -------------------------------------------------------------------------- */ +export const rolesTable = pgTable('roles', { + id: id() + .primaryKey() + .$defaultFn(() => generateId()), + name: text().unique().notNull(), + ...timestamps, +}); + +/* -------------------------------------------------------------------------- */ +/* Relations */ +/* -------------------------------------------------------------------------- */ +export const role_relations = relations(rolesTable, ({ many }) => ({ + user_roles: many(user_roles), +})); + +/* -------------------------------------------------------------------------- */ +/* Types */ +/* -------------------------------------------------------------------------- */ +export type Roles = InferSelectModel; + + diff --git a/src/lib/server/api/users/tables/credentials.table.ts b/src/lib/server/api/users/tables/credentials.table.ts new file mode 100644 index 0000000..9dbc99f --- /dev/null +++ b/src/lib/server/api/users/tables/credentials.table.ts @@ -0,0 +1,29 @@ +import type { InferSelectModel } from 'drizzle-orm'; +import { pgTable, text } from 'drizzle-orm/pg-core'; +import { id, timestamps } from '../../common/utils/drizzle'; +import { usersTable } from './users.table'; +import { generateId } from '../../common/utils/crypto'; + +/* -------------------------------------------------------------------------- */ +/* Table */ +/* -------------------------------------------------------------------------- */ +export enum CredentialsType { + SECRET = 'secret', + PASSWORD = 'password', + TOTP = 'totp', + HOTP = 'hotp', +} + +export const credentialsTable = pgTable('credentials', { + id: id() + .primaryKey() + .$defaultFn(() => generateId()), + user_id: id() + .notNull() + .references(() => usersTable.id, { onDelete: 'cascade' }), + type: text().notNull().default(CredentialsType.PASSWORD), + secret_data: text().notNull(), + ...timestamps, +}); + +export type Credentials = InferSelectModel; diff --git a/src/lib/server/api/users/tables/user-roles.table.ts b/src/lib/server/api/users/tables/user-roles.table.ts new file mode 100644 index 0000000..adbb1b4 --- /dev/null +++ b/src/lib/server/api/users/tables/user-roles.table.ts @@ -0,0 +1,50 @@ +import { type InferSelectModel, relations, getTableColumns } from 'drizzle-orm'; +import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'; +import { rolesTable } from '../../roles/tables/roles.table'; +import { usersTable } from './users.table'; +import { generateId } from '../../common/utils/crypto'; +import { id, timestamps } from '../../common/utils/drizzle'; + +/* -------------------------------------------------------------------------- */ +/* Table */ +/* -------------------------------------------------------------------------- */ +export const user_roles = pgTable('user_roles', { + id: id() + .primaryKey() + .$defaultFn(() => generateId()), + user_id: uuid() + .notNull() + .references(() => usersTable.id, { onDelete: 'cascade' }), + role_id: uuid() + .notNull() + .references(() => rolesTable.id, { onDelete: 'cascade' }), + primary: boolean().default(false), + ...timestamps, +}); + +/* -------------------------------------------------------------------------- */ +/* Relations */ +/* -------------------------------------------------------------------------- */ +export const user_role_relations = relations(user_roles, ({ one }) => ({ + role: one(rolesTable, { + fields: [user_roles.role_id], + references: [rolesTable.id], + }), + user: one(usersTable, { + fields: [user_roles.user_id], + references: [usersTable.id], + }), +})); + +/* -------------------------------------------------------------------------- */ +/* Types */ +/* -------------------------------------------------------------------------- */ +export type UserRoles = InferSelectModel; +export type UserRolesWithRelations = UserRoles & {}; + +const userRolesColumns = getTableColumns(user_roles); + +export const publicUserColumns = { + id: userRolesColumns.id, + ...timestamps, +}; diff --git a/src/lib/server/api/users/tables/users.table.ts b/src/lib/server/api/users/tables/users.table.ts index 25150b0..ca00577 100644 --- a/src/lib/server/api/users/tables/users.table.ts +++ b/src/lib/server/api/users/tables/users.table.ts @@ -1,24 +1,32 @@ -import { pgTable, text } from 'drizzle-orm/pg-core'; +import { boolean, pgTable, text } from 'drizzle-orm/pg-core'; import { getTableColumns, type InferSelectModel, relations } from 'drizzle-orm'; import { citext, id, timestamps } from '../../common/utils/drizzle'; import { generateId } from '../../common/utils/crypto'; +import { user_roles } from './user-roles.table'; /* -------------------------------------------------------------------------- */ /* Table */ /* -------------------------------------------------------------------------- */ export const usersTable = pgTable('users', { - id: id() - .primaryKey() - .$defaultFn(() => generateId()), - email: citext().unique().notNull(), - avatar: text(), - ...timestamps + id: id() + .primaryKey() + .$defaultFn(() => generateId()), + username: text().unique(), + email: citext().unique().notNull(), + first_name: text(), + last_name: text(), + email_verified: boolean().default(false), + mfa_enabled: boolean().notNull().default(false), + avatar: text(), + ...timestamps, }); /* -------------------------------------------------------------------------- */ /* Relations */ /* -------------------------------------------------------------------------- */ -export const usersRelations = relations(usersTable, () => ({})); +export const userRelations = relations(usersTable, ({ many }) => ({ + user_roles: many(user_roles), +})); /* -------------------------------------------------------------------------- */ /* Types */ @@ -29,8 +37,11 @@ export type UserWithRelations = User & {}; const userColumns = getTableColumns(usersTable); export const publicUserColumns = { - id: userColumns.id, - email: userColumns.email, - avatar: userColumns.avatar, - ...timestamps + id: userColumns.id, + username: userColumns.username, + email: userColumns.email, + avatar: userColumns.avatar, + first_name: userColumns.first_name, + last_name: userColumns.last_name, + ...timestamps, }; diff --git a/src/lib/tanstack-query/iam.ts b/src/lib/tanstack-query/iam.ts new file mode 100644 index 0000000..d93eb1d --- /dev/null +++ b/src/lib/tanstack-query/iam.ts @@ -0,0 +1,34 @@ +import type { InferRequestType } from 'hono'; +import { parseApiResponse } from '$lib/utils/api'; +import type { Api, ApiMutation } from '$lib/utils/types'; +import { TanstackQueryModule } from './query-module'; + +/* -------------------------------------------------------------------------- */ +/* Types */ +/* -------------------------------------------------------------------------- */ +type RequestLogin = Api['iam']['login']['request']['$post']; +type VerifyLogin = Api['iam']['login']['verify']['$post']; +type Logout = Api['iam']['logout']['$post']; + +/* -------------------------------------------------------------------------- */ +/* Api */ +/* -------------------------------------------------------------------------- */ +export class IamModule extends TanstackQueryModule<'iam'> { + logout(): ApiMutation { + return { + mutationFn: async () => await this.api.iam.logout.$post().then(parseApiResponse) + }; + } + requestLogin(): ApiMutation { + return { + mutationFn: async (data: InferRequestType) => + await this.api.iam.login.request.$post(data).then(parseApiResponse) + }; + } + verifyLogin(): ApiMutation { + return { + mutationFn: async (data: InferRequestType) => + await this.api.iam.login.verify.$post(data).then(parseApiResponse) + }; + } +} diff --git a/src/lib/tanstack-query/index.ts b/src/lib/tanstack-query/index.ts new file mode 100644 index 0000000..2e5fc1a --- /dev/null +++ b/src/lib/tanstack-query/index.ts @@ -0,0 +1,11 @@ +import { IamModule } from './iam'; +import type { ClientRequestOptions } from 'hono'; +import { UsersModule } from './users'; +import { TanstackQueryModule } from './query-module'; + +class TanstackQueryHandler extends TanstackQueryModule { + iam = new IamModule(this.opts); + users = new UsersModule(this.opts); +} + +export const queryHandler = (opts?: ClientRequestOptions) => new TanstackQueryHandler(opts); diff --git a/src/lib/tanstack-query/query-module.ts b/src/lib/tanstack-query/query-module.ts new file mode 100644 index 0000000..6eede2f --- /dev/null +++ b/src/lib/tanstack-query/query-module.ts @@ -0,0 +1,13 @@ +import type { ClientRequestOptions } from 'hono'; +import { api } from '$lib/utils/api'; + +export abstract class TanstackQueryModule { + protected readonly opts: ClientRequestOptions | undefined; + protected readonly api: ReturnType; + public namespace: T | null = null; + + constructor(opts?: ClientRequestOptions) { + this.opts = opts; + this.api = api(opts); + } +} diff --git a/src/lib/tanstack-query/users.ts b/src/lib/tanstack-query/users.ts new file mode 100644 index 0000000..dff5916 --- /dev/null +++ b/src/lib/tanstack-query/users.ts @@ -0,0 +1,37 @@ +import { parseApiResponse } from '$lib/utils/api'; +import type { Api, ApiMutation, ApiQuery } from '$lib/utils/types'; +import type { InferRequestType } from 'hono'; +import { TanstackQueryModule } from './query-module'; + +/* -------------------------------------------------------------------------- */ +/* Types */ +/* -------------------------------------------------------------------------- */ +type Me = Api['users']['me']['$get']; +type UpdateEmailRequest = Api['users']['me']['email']['request']['$post']; +type VerifyEmailRequest = Api['users']['me']['email']['verify']['$post']; + +/* -------------------------------------------------------------------------- */ +/* Api */ +/* -------------------------------------------------------------------------- */ +export class UsersModule extends TanstackQueryModule<'users'> { + me(): ApiQuery { + return { + queryKey: [this.namespace, 'me'], + queryFn: async () => await this.api.users.me.$get().then(parseApiResponse) + }; + } + + updateEmailRequest(): ApiMutation { + return { + mutationFn: async (args: InferRequestType) => + await this.api.users.me.email.request.$post(args).then(parseApiResponse) + }; + } + + verifyEmailRequest(): ApiMutation { + return { + mutationFn: async (args: InferRequestType) => + await this.api.users.me.email.verify.$post(args).then(parseApiResponse) + }; + } +} diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 4694389..723734f 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -6,9 +6,28 @@ import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js'; import { Input } from '$lib/components/ui/input/index.js'; import * as Sheet from '$lib/components/ui/sheet/index.js'; - import ThemeDropdown from '@/components/theme-dropdown.svelte'; + import { createMutation } from '@tanstack/svelte-query'; + import { authContext } from '$lib/hooks/session.svelte.js'; + import { queryHandler } from '$lib/tanstack-query/index.js'; + import { goto, invalidateAll } from '$app/navigation'; + import UserAvatar from '$lib/components/user-avatar.svelte'; + import ThemeDropdown from '$lib/components/theme-dropdown.svelte'; const { children, data } = $props(); + + $effect.pre(() => { + authContext.setAuthedUser(data.authedUser); + }); + + const logoutMutation = createMutation({ + ...queryHandler().iam.logout(), + onSuccess: async () => { + await data.queryClient.invalidateQueries(); + invalidateAll(); + goto('/login'); + } + }); + queryHandler;
@@ -71,7 +90,7 @@ @@ -82,7 +101,7 @@ Logout - + onclick={$logoutMutation.mutate} diff --git a/src/routes/(app)/+layout.ts b/src/routes/(app)/+layout.ts new file mode 100644 index 0000000..03ab644 --- /dev/null +++ b/src/routes/(app)/+layout.ts @@ -0,0 +1,7 @@ +import { queryHandler } from '$lib/tanstack-query'; + +export const load = async ({ parent, fetch }) => { + const { queryClient } = await parent(); + const authedUser = await queryClient.fetchQuery(queryHandler({ fetch }).users.me()); + return { authedUser }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index fd0270c..856a3fe 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,14 +1,20 @@ - - -
- {@render children()} -
-
+ + + +
+ {@render children()} +
+ +
+
diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 0000000..d64b1e0 --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1,15 @@ +import { browser } from '$app/environment'; +import { QueryClient } from '@tanstack/svelte-query'; + +export const load = async () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + enabled: browser, + staleTime: 60 * 1000 + } + } + }); + + return { queryClient }; +}; diff --git a/src/routes/demo/+page.svelte b/src/routes/demo/+page.svelte deleted file mode 100644 index a815390..0000000 --- a/src/routes/demo/+page.svelte +++ /dev/null @@ -1 +0,0 @@ -paraglide diff --git a/src/routes/demo/paraglide/+page.svelte b/src/routes/demo/paraglide/+page.svelte deleted file mode 100644 index 22d4928..0000000 --- a/src/routes/demo/paraglide/+page.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - - - -

{m.hello_world({ name: 'SvelteKit User' })}

-
- - -