Compare commits
5 Commits
feature/pr
...
190b1d0c90
| Author | SHA1 | Date | |
|---|---|---|---|
|
190b1d0c90
|
|||
|
5b72ab28dd
|
|||
|
8f20c168a2
|
|||
|
12a3001aec
|
|||
|
06a7f1bd51
|
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
||||
[*]
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
max_line_length = 100
|
||||
quote_type = single
|
||||
trim_trailing_whitespace = true
|
||||
7
.zed/settings.json
Normal file
7
.zed/settings.json
Normal file
@@ -0,0 +1,7 @@
|
||||
// Folder-specific settings
|
||||
//
|
||||
// For a full list of overridable settings, and general information on folder-specific settings,
|
||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
{
|
||||
"formatter": "prettier"
|
||||
}
|
||||
11
README.md
11
README.md
@@ -27,3 +27,14 @@ bun install
|
||||
bun run prepare
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## To Do
|
||||
|
||||
- [ ] Proper invite page
|
||||
- [ ] Proper error page for login without invite
|
||||
- [ ] Support file provider (for wg-quick)
|
||||
- [ ] Fix accessibility issues (lighthouse)
|
||||
- [ ] Add title to profile page (lighthouse)
|
||||
- [ ] wg-quick scripts (maybe?)
|
||||
- [ ] Get rid of api routes (maybe?)
|
||||
- [ ] Get rid of custom result types (maybe?)
|
||||
|
||||
32
bun.lock
32
bun.lock
@@ -9,6 +9,7 @@
|
||||
"drizzle-orm": "^0.38.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lucide/svelte": "^0.487.0",
|
||||
"@oslojs/crypto": "^1.0.1",
|
||||
"@oslojs/encoding": "^1.1.0",
|
||||
"@ryoppippi/unplugin-typia": "npm:@jsr/ryoppippi__unplugin-typia",
|
||||
@@ -24,14 +25,13 @@
|
||||
"@types/qrcode-svg": "^1.1.5",
|
||||
"arctic": "^2.3.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"bits-ui": "^0.22.0",
|
||||
"bits-ui": "^1.3.18",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"globals": "^15.15.0",
|
||||
"ip-address": "^10.0.1",
|
||||
"lucide-svelte": "^0.469.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
@@ -181,7 +181,7 @@
|
||||
|
||||
"@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.4.7", "", { "os": "win32", "cpu": "x64" }, "sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw=="],
|
||||
|
||||
"@melt-ui/svelte": ["@melt-ui/svelte@0.76.2", "", { "dependencies": { "@floating-ui/core": "^1.3.1", "@floating-ui/dom": "^1.4.5", "@internationalized/date": "^3.5.0", "dequal": "^2.0.3", "focus-trap": "^7.5.2", "nanoid": "^5.0.4" }, "peerDependencies": { "svelte": ">=3 <5" } }, "sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA=="],
|
||||
"@lucide/svelte": ["@lucide/svelte@0.487.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-27b/wUzWrqDJu97+1iSV2X8L2JGRWH/mAWAjHgazWxhGxVu/kS0p3SbNu6w3skNmQNEku33EKU1v44IVwULzbw=="],
|
||||
|
||||
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"bits-ui": ["bits-ui@0.22.0", "", { "dependencies": { "@internationalized/date": "^3.5.1", "@melt-ui/svelte": "0.76.2", "nanoid": "^5.0.5" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0" } }, "sha512-r7Fw1HNgA4YxZBRcozl7oP0bheQ8EHh+kfMBZJgyFISix8t4p/nqDcHLmBgIiJ3T5XjYnJRorYDjIWaCfhb5fw=="],
|
||||
"bits-ui": ["bits-ui@1.3.18", "", { "dependencies": { "@floating-ui/core": "^1.6.4", "@floating-ui/dom": "^1.6.7", "@internationalized/date": "^3.5.6", "esm-env": "^1.1.2", "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1", "tabbable": "^6.2.0" }, "peerDependencies": { "svelte": "^5.11.0" } }, "sha512-4ZsJqMZUpCEGYMHfVSNZDbOTg3Haxwe01I5LMs4ZImjY7huDac7Dkh2KWTtOdc7jhj2vJprE9bJI4Ayg8gexXQ=="],
|
||||
|
||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
@@ -421,8 +421,6 @@
|
||||
|
||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
|
||||
|
||||
"devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="],
|
||||
@@ -515,8 +513,6 @@
|
||||
|
||||
"flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
|
||||
|
||||
"focus-trap": ["focus-trap@7.6.2", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-9FhUxK1hVju2+AiQIDJ5Dd//9R2n2RAfJ0qfhF4IHGHgcoEUTMpbTeG/zbEuwaiYXfuAH6XE0/aCyxDdRM+W5w=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
|
||||
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
@@ -563,6 +559,8 @@
|
||||
|
||||
"ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
|
||||
|
||||
"inquirer": ["inquirer@8.2.6", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^6.0.1" } }, "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg=="],
|
||||
|
||||
"ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
|
||||
@@ -635,8 +633,6 @@
|
||||
|
||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"lucide-svelte": ["lucide-svelte@0.469.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-PMIJ8jrFqVUsXJz4d1yfAQplaGhNOahwwkzbunha8DhpiD73xqX24n8dE1dPpUk3vcrdWVsHc1y/liHHotOnGQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
@@ -665,7 +661,7 @@
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
||||
"nanoid": ["nanoid@5.0.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q=="],
|
||||
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
@@ -787,6 +783,8 @@
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
|
||||
|
||||
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||
|
||||
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
|
||||
@@ -827,6 +825,8 @@
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
|
||||
|
||||
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
@@ -839,6 +839,8 @@
|
||||
|
||||
"svelte-eslint-parser": ["svelte-eslint-parser@0.43.0", "", { "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "postcss": "^8.4.39", "postcss-scss": "^4.0.9" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA=="],
|
||||
|
||||
"svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
|
||||
|
||||
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="],
|
||||
@@ -969,8 +971,6 @@
|
||||
|
||||
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"postcss-load-config/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
|
||||
|
||||
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
@@ -1099,8 +1099,6 @@
|
||||
|
||||
"drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
|
||||
|
||||
"eslint-plugin-svelte/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"gel/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
@@ -1111,14 +1109,10 @@
|
||||
|
||||
"pkg-dir/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
|
||||
|
||||
"svelte-eslint-parser/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"tailwindcss/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
|
||||
"tailwindcss/postcss-load-config/yaml": ["yaml@2.6.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg=="],
|
||||
|
||||
"vite/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.8", "", { "os": "android", "cpu": "arm" }, "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw=="],
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"prepare": "ts-patch install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lucide/svelte": "^0.487.0",
|
||||
"@oslojs/crypto": "^1.0.1",
|
||||
"@oslojs/encoding": "^1.1.0",
|
||||
"@ryoppippi/unplugin-typia": "npm:@jsr/ryoppippi__unplugin-typia",
|
||||
@@ -34,14 +35,13 @@
|
||||
"@types/qrcode-svg": "^1.1.5",
|
||||
"arctic": "^2.3.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"bits-ui": "^0.22.0",
|
||||
"bits-ui": "^1.3.18",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"globals": "^15.15.0",
|
||||
"ip-address": "^10.0.1",
|
||||
"lucide-svelte": "^0.469.0",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { LucideLoaderCircle } from 'lucide-svelte';
|
||||
import { LucideLoaderCircle } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import googleIcon from '$lib/assets/google.svg';
|
||||
|
||||
let { inviteToken, class: className, ...rest }: { inviteToken?: string; class?: string; rest?: { [p: string]: unknown } } = $props();
|
||||
let { inviteToken, class: className, ...rest }: {
|
||||
inviteToken?: string;
|
||||
class?: string;
|
||||
rest?: { [p: string]: unknown }
|
||||
} = $props();
|
||||
|
||||
let isLoading = $state(false);
|
||||
let submitted = $state(false);
|
||||
</script>
|
||||
|
||||
<div class={cn('flex gap-6', className)} {...rest}>
|
||||
<form method="get" action="/auth/authentik{inviteToken ? `?invite=${inviteToken}` : ''}">
|
||||
<form method="get" onsubmit={() => submitted = true}
|
||||
action="/auth/authentik{inviteToken ? `?invite=${inviteToken}` : ''}">
|
||||
<input type="hidden" value={inviteToken} name="invite" />
|
||||
<Button
|
||||
type="submit"
|
||||
onclick={() => {
|
||||
isLoading = true;
|
||||
}}
|
||||
>
|
||||
{#if isLoading}
|
||||
<Button type="submit" disabled={submitted}>
|
||||
{#if submitted}
|
||||
<LucideLoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<img
|
||||
@@ -30,15 +30,11 @@
|
||||
Sign in with Authentik
|
||||
</Button>
|
||||
</form>
|
||||
<form method="get" action="/auth/google">
|
||||
<form method="get" onsubmit={() => submitted = true}
|
||||
action="/auth/google{inviteToken ? `?invite=${inviteToken}` : ''}">
|
||||
<input type="hidden" value={inviteToken} name="invite" />
|
||||
<Button
|
||||
type="submit"
|
||||
onclick={() => {
|
||||
isLoading = true;
|
||||
}}
|
||||
>
|
||||
{#if isLoading}
|
||||
<Button type="submit" disabled={submitted}>
|
||||
{#if submitted}
|
||||
<LucideLoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<img
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { LucideClipboardCopy, LucideDownload } from 'lucide-svelte';
|
||||
import { LucideClipboardCopy, LucideDownload } from '@lucide/svelte';
|
||||
|
||||
const {
|
||||
data,
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
this={href ? "a" : "span"}
|
||||
bind:this={ref}
|
||||
{href}
|
||||
class={cn(badgeVariants({ variant, className }))}
|
||||
class={cn(badgeVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
{#if href}
|
||||
<a
|
||||
bind:this={ref}
|
||||
class={cn(buttonVariants({ variant, size, className }))}
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{href}
|
||||
{...restProps}
|
||||
>
|
||||
@@ -65,7 +65,7 @@
|
||||
{:else}
|
||||
<button
|
||||
bind:this={ref}
|
||||
class={cn(buttonVariants({ variant, size, className }))}
|
||||
class={cn(buttonVariants({ variant, size }), className)}
|
||||
{type}
|
||||
{...restProps}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||
import Check from "lucide-svelte/icons/check";
|
||||
import Minus from "lucide-svelte/icons/minus";
|
||||
import Check from "@lucide/svelte/icons/check";
|
||||
import Minus from "@lucide/svelte/icons/minus";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import X from "@lucide/svelte/icons/x";
|
||||
import type { Snippet } from "svelte";
|
||||
import * as Dialog from "./index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
@@ -17,7 +17,7 @@
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Portal class="absolute" {...portalProps}>
|
||||
<Dialog.Portal {...portalProps}>
|
||||
<Dialog.Overlay />
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: DialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
@@ -14,6 +13,4 @@
|
||||
bind:ref
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</DialogPrimitive.Description>
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: DialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
@@ -17,6 +16,4 @@
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</DialogPrimitive.Overlay>
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: DialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
@@ -14,6 +13,4 @@
|
||||
bind:ref
|
||||
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</DialogPrimitive.Title>
|
||||
/>
|
||||
|
||||
@@ -1,22 +1,46 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes } from "svelte/elements";
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLInputAttributes> = $props();
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{#if type === "file"}
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: LabelPrimitive.RootProps = $props();
|
||||
</script>
|
||||
@@ -17,6 +16,4 @@
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</LabelPrimitive.Root>
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: TabsPrimitive.ContentProps = $props();
|
||||
</script>
|
||||
@@ -17,6 +16,4 @@
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</TabsPrimitive.Content>
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: TabsPrimitive.ListProps = $props();
|
||||
</script>
|
||||
@@ -17,6 +16,4 @@
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</TabsPrimitive.List>
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: TabsPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
@@ -17,6 +16,4 @@
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</TabsPrimitive.Trigger>
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { sha256 } from '@oslojs/crypto/sha2';
|
||||
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
|
||||
import { db } from '$lib/server/db';
|
||||
import * as table from '$lib/server/db/schema';
|
||||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
import { dev } from '$app/environment';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
@@ -22,14 +22,14 @@ export async function createSession(userId: string): Promise<table.Session> {
|
||||
const session: table.Session = {
|
||||
id: sessionId,
|
||||
userId,
|
||||
expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
|
||||
expiresAt: new Date(Date.now() + DAY_IN_MS * 30),
|
||||
};
|
||||
await db.insert(table.sessions).values(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
export function setSessionTokenCookie(event: RequestEvent, sessionId: string, expiresAt: Date) {
|
||||
event.cookies.set(sessionCookieName, sessionId, {
|
||||
export function setSessionTokenCookie(cookies: Cookies, sessionId: string, expiresAt: Date) {
|
||||
cookies.set(sessionCookieName, sessionId, {
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
httpOnly: true,
|
||||
@@ -42,16 +42,21 @@ export async function invalidateSession(sessionId: string): Promise<void> {
|
||||
await db.delete(table.sessions).where(eq(table.sessions.id, sessionId));
|
||||
}
|
||||
|
||||
export function deleteSessionTokenCookie(event: RequestEvent) {
|
||||
event.cookies.delete(sessionCookieName, { path: '/' });
|
||||
export function deleteSessionTokenCookie(cookies: Cookies) {
|
||||
cookies.delete(sessionCookieName, { path: '/' });
|
||||
}
|
||||
|
||||
export async function validateSession(sessionId: string) {
|
||||
const [result] = await db
|
||||
.select({
|
||||
// Adjust user table here to tweak returned data
|
||||
user: { id: table.users.id, authSource: table.users.authSource, username: table.users.username, name: table.users.name },
|
||||
session: table.sessions
|
||||
user: {
|
||||
id: table.users.id,
|
||||
authSource: table.users.authSource,
|
||||
username: table.users.username,
|
||||
name: table.users.name,
|
||||
},
|
||||
session: table.sessions,
|
||||
})
|
||||
.from(table.sessions)
|
||||
.innerJoin(table.users, eq(table.sessions.userId, table.users.id))
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { fail, redirect } from "@sveltejs/kit";
|
||||
import { invalidateSession, deleteSessionTokenCookie } from "$lib/server/auth";
|
||||
import type { Actions } from "./$types";
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/auth';
|
||||
import type { Actions } from './$types';
|
||||
|
||||
export const actions: Actions = {
|
||||
logout: async (event) => {
|
||||
if (event.locals.session === null) {
|
||||
logout: async ({ locals, cookies }) => {
|
||||
if (locals.session === null) {
|
||||
return fail(401);
|
||||
}
|
||||
await invalidateSession(event.locals.session.id);
|
||||
deleteSessionTokenCookie(event);
|
||||
return redirect(302, "/");
|
||||
}
|
||||
await invalidateSession(locals.session.id);
|
||||
deleteSessionTokenCookie(cookies);
|
||||
redirect(302, '/');
|
||||
},
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
|
||||
|
||||
if (existingUser) {
|
||||
const session = await createSession(existingUser.id);
|
||||
setSessionTokenCookie(event, session.id, session.expiresAt);
|
||||
setSessionTokenCookie(event.cookies, session.id, session.expiresAt);
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
@@ -67,7 +67,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
|
||||
try {
|
||||
await db.insert(table.users).values(user);
|
||||
const session = await createSession(user.id);
|
||||
setSessionTokenCookie(event, session.id, session.expiresAt);
|
||||
setSessionTokenCookie(event.cookies, session.id, session.expiresAt);
|
||||
} catch (e) {
|
||||
console.error('failed to create user', e);
|
||||
return new Response(null, {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const GET: RequestHandler = ({ url, cookies }) => {
|
||||
const state = generateState();
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
const scopes = ['openid', 'profile', 'email'];
|
||||
const authUrl = google.createAuthorizationURL(state, codeVerifier, scopes);
|
||||
const authUrl = google.createAuthorizationURL(state + inviteToken, codeVerifier, scopes);
|
||||
|
||||
cookies.set('google_oauth_state', state, {
|
||||
path: '/',
|
||||
@@ -22,12 +22,6 @@ export const GET: RequestHandler = ({ url, cookies }) => {
|
||||
maxAge: 60 * 10, // 10 minutes
|
||||
sameSite: 'lax',
|
||||
});
|
||||
if (inviteToken !== null) cookies.set('invite_token', inviteToken, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10, // 10 minutes
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import * as arctic from 'arctic';
|
||||
import { google } from '$lib/server/oauth';
|
||||
import { db } from '$lib/server/db';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import * as table from '$lib/server/db/schema';
|
||||
import { createSession, isValidInviteToken, setSessionTokenCookie } from '$lib/server/auth';
|
||||
import { db } from '$lib/server/db';
|
||||
import * as table from '$lib/server/db/schema';
|
||||
import { google } from '$lib/server/oauth';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { OAuth2Tokens } from 'arctic';
|
||||
import * as arctic from 'arctic';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { assertGuard } from 'typia';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const { url, cookies } = event;
|
||||
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
const storedState = cookies.get('google_oauth_state') ?? null;
|
||||
const codeVerifier = cookies.get('google_code_verifier', ) ?? null;
|
||||
const inviteToken = cookies.get('invite_token') ?? null;
|
||||
const codeVerifier = cookies.get('google_code_verifier') ?? null;
|
||||
|
||||
if (code === null || state === null || storedState === null || codeVerifier === null) {
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
});
|
||||
error(400, 'Missing url parameters');
|
||||
}
|
||||
|
||||
const stateGeneratedToken = state.slice(0, storedState.length);
|
||||
const stateInviteToken = state.slice(storedState.length);
|
||||
if (stateGeneratedToken !== storedState) {
|
||||
error(400, 'Invalid state in url');
|
||||
}
|
||||
|
||||
let tokens: OAuth2Tokens;
|
||||
@@ -28,20 +33,14 @@ export const GET: RequestHandler = async (event) => {
|
||||
} catch (e) {
|
||||
if (e instanceof arctic.OAuth2RequestError) {
|
||||
console.debug('Arctic: OAuth: invalid authorization code, credentials, or redirect URI', e);
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
});
|
||||
throw error(400, 'Invalid authorization code');
|
||||
}
|
||||
if (e instanceof arctic.ArcticFetchError) {
|
||||
console.debug('Arctic: failed to call `fetch()`', e);
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
});
|
||||
error(400, 'Failed to validate authorization code');
|
||||
}
|
||||
|
||||
return new Response(null, {
|
||||
status: 500,
|
||||
});
|
||||
console.error('Unxepcted error validating authorization code', code, e);
|
||||
error(500);
|
||||
}
|
||||
|
||||
const idToken = tokens.idToken();
|
||||
@@ -60,20 +59,14 @@ export const GET: RequestHandler = async (event) => {
|
||||
|
||||
if (existingUser) {
|
||||
const session = await createSession(existingUser.id);
|
||||
setSessionTokenCookie(event, session.id, session.expiresAt);
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/',
|
||||
},
|
||||
});
|
||||
setSessionTokenCookie(cookies, session.id, session.expiresAt);
|
||||
redirect(302, '/');
|
||||
}
|
||||
|
||||
// TODO: proper error page
|
||||
if (inviteToken === null || !isValidInviteToken(inviteToken)) {
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
});
|
||||
if (!isValidInviteToken(stateInviteToken)) {
|
||||
const message =
|
||||
stateInviteToken.length === 0 ? 'sign up with an invite link first' : 'invalid invite link';
|
||||
error(403, 'Not Authorized: ' + message);
|
||||
}
|
||||
|
||||
const user: table.User = {
|
||||
@@ -85,16 +78,11 @@ export const GET: RequestHandler = async (event) => {
|
||||
|
||||
// TODO: proper error handling, delete cookies
|
||||
await db.insert(table.users).values(user);
|
||||
console.log('created user', user, 'with invite token', inviteToken);
|
||||
console.log('created user', user, 'with invite token', stateInviteToken);
|
||||
|
||||
const session = await createSession(user.id);
|
||||
|
||||
setSessionTokenCookie(event, session.id, session.expiresAt);
|
||||
setSessionTokenCookie(cookies, session.id, session.expiresAt);
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/',
|
||||
},
|
||||
});
|
||||
redirect(302, '/');
|
||||
};
|
||||
@@ -4,7 +4,7 @@
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { LucideLoaderCircle, LucidePlus } from 'lucide-svelte';
|
||||
import { LucideLoaderCircle, LucidePlus } from '@lucide/svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { page } from '$app/state';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { LucideLoaderCircle, LucideTrash } from 'lucide-svelte';
|
||||
import { LucideLoaderCircle, LucideTrash } from '@lucide/svelte';
|
||||
|
||||
const { device } = $props();
|
||||
let submitted = $state(false);
|
||||
@@ -26,10 +26,12 @@
|
||||
<Button type="submit" variant="destructive" disabled={submitted}>
|
||||
Delete
|
||||
</Button>
|
||||
<Dialog.Close asChild let:builder>
|
||||
<button class={buttonVariants()} disabled={submitted} use:builder.action {...builder}>
|
||||
Cancel
|
||||
</button>
|
||||
<Dialog.Close>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} disabled={submitted}>
|
||||
Cancel
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Dialog.Close>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { invalidate, invalidateAll } from '$app/navigation';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { LucideLoaderCircle, LucideLogOut, LucideRefreshCw } from 'lucide-svelte';
|
||||
import { LucideLoaderCircle, LucideLogOut, LucideRefreshCw } from '@lucide/svelte';
|
||||
import { CodeSnippet } from '$lib/components/app/code-snippet/index.js';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
Reference in New Issue
Block a user