feat: new dropdown, friendship stuffz, passwords :3c
Co-authored-by: Evan Song <ferothefox@users.noreply.github.com>
This commit is contained in:
parent
23c20112c9
commit
7e49d2dc92
27 changed files with 878 additions and 153 deletions
208
package-lock.json
generated
208
package-lock.json
generated
|
@ -11,6 +11,7 @@
|
|||
"@benzara/svelte-animated-counter": "^0.0.3",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/enhanced-img": "^0.1.8",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^9.4.3",
|
||||
"carbon-components-svelte": "^0.84.0",
|
||||
"cookie": "^0.6.0",
|
||||
|
@ -39,6 +40,7 @@
|
|||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
|
@ -58,6 +60,7 @@
|
|||
"sharp": "^0.33.2",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"sveltekit-rate-limiter": "^0.5.0",
|
||||
"svgo": "^3.2.0",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"tslib": "^2.4.1",
|
||||
|
@ -946,6 +949,15 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/ttlcache": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz",
|
||||
"integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
|
@ -989,6 +1001,145 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"make-dir": "^3.1.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"nopt": "^5.0.0",
|
||||
"npmlog": "^5.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"tar": "^6.1.11"
|
||||
},
|
||||
"bin": {
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/are-we-there-yet": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
|
||||
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
|
||||
"dependencies": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/gauge": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
|
||||
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
|
||||
"dependencies": {
|
||||
"aproba": "^1.0.3 || ^2.0.0",
|
||||
"color-support": "^1.1.2",
|
||||
"console-control-strings": "^1.0.0",
|
||||
"has-unicode": "^2.0.1",
|
||||
"object-assign": "^4.1.1",
|
||||
"signal-exit": "^3.0.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wide-align": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"dependencies": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/npmlog": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
|
||||
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
|
||||
"dependencies": {
|
||||
"are-we-there-yet": "^2.0.0",
|
||||
"console-control-strings": "^1.1.0",
|
||||
"gauge": "^3.0.0",
|
||||
"set-blocking": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
@ -1602,6 +1753,15 @@
|
|||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcrypt": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
|
||||
"integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
|
@ -1700,8 +1860,7 @@
|
|||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"optional": true
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
|
@ -1731,7 +1890,6 @@
|
|||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
|
@ -1890,8 +2048,7 @@
|
|||
"node_modules/aproba": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
||||
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
|
||||
"optional": true
|
||||
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
|
||||
},
|
||||
"node_modules/arch": {
|
||||
"version": "2.2.0",
|
||||
|
@ -2359,6 +2516,19 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
|
||||
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"node-addon-api": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
|
@ -2367,6 +2537,11 @@
|
|||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt/node_modules/node-addon-api": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "9.4.3",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.4.3.tgz",
|
||||
|
@ -3643,7 +3818,6 @@
|
|||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
|
@ -7143,7 +7317,6 @@
|
|||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
|
@ -8845,7 +9018,6 @@
|
|||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
|
@ -8910,7 +9082,6 @@
|
|||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"abbrev": "1"
|
||||
},
|
||||
|
@ -12278,6 +12449,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/sveltekit-rate-limiter": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/sveltekit-rate-limiter/-/sveltekit-rate-limiter-0.5.0.tgz",
|
||||
"integrity": "sha512-q5RnqgAWP3fhDR+InWdOvsmYZ/x27Qojrk00scy+ob3Q+hawzHmLjQSL6XmSJrx82ceGZlw3G/hKyb/952oGWA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@isaacs/ttlcache": "^1.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@sveltejs/kit": "1.x || 2.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sver-compat": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz",
|
||||
|
@ -12890,8 +13073,7 @@
|
|||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/trim-repeated": {
|
||||
"version": "1.0.0",
|
||||
|
@ -14072,14 +14254,12 @@
|
|||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
|
@ -37,6 +38,7 @@
|
|||
"sharp": "^0.33.2",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"sveltekit-rate-limiter": "^0.5.0",
|
||||
"svgo": "^3.2.0",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"tslib": "^2.4.1",
|
||||
|
@ -51,6 +53,7 @@
|
|||
"@benzara/svelte-animated-counter": "^0.0.3",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/enhanced-img": "^0.1.8",
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^9.4.3",
|
||||
"carbon-components-svelte": "^0.84.0",
|
||||
"cookie": "^0.6.0",
|
||||
|
|
|
@ -23,6 +23,6 @@
|
|||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
%sveltekit.body%
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
78
src/components/AccountButton.svelte
Normal file
78
src/components/AccountButton.svelte
Normal file
|
@ -0,0 +1,78 @@
|
|||
<script lang="ts">
|
||||
import { transition } from "$lib/util/animation";
|
||||
import type { GetUserResponseSuccess } from "$types/api";
|
||||
import { getContext, onMount } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { PageData } from "../routes/$types";
|
||||
import cookie from "cookiejs";
|
||||
|
||||
export let user: PageData["user"];
|
||||
const token = getContext<Writable<string>>("token");
|
||||
|
||||
let open = false;
|
||||
|
||||
function signOut() {
|
||||
setTimeout(() => {
|
||||
$token = "";
|
||||
cookie.remove("token");
|
||||
}, 330); // 360ms is transition duration, 330ms
|
||||
// is to prevent GC on chromium. :3c
|
||||
// hi evan i know ur reading thisss
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
open = !open;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
function closeMenu(e: MouseEvent) {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
if (!e.target.closest(".user-profile-menu")) {
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
window.addEventListener("mousedown", closeMenu);
|
||||
return () => window.removeEventListener("mousedown", closeMenu);
|
||||
});
|
||||
</script>
|
||||
|
||||
<button class="user-profile-menu relative ml-3" on:click={toggleMenu}>
|
||||
<img
|
||||
style="transition: 240ms transform {transition}"
|
||||
src={`${user.avatarUrl}`}
|
||||
alt="{user.username}'s avatar"
|
||||
class="h-6 w-6 rounded-full"
|
||||
/>
|
||||
<div
|
||||
style="transition: 360ms {transition}"
|
||||
class={`${open ? "rotate-0 scale-100 opacity-100" : "-rotate-90 scale-0 opacity-0"} absolute right-0 top-full mt-2 flex h-fit origin-top-right transform-gpu flex-col overflow-hidden rounded-[20px] rounded-tr-none border-2 border-solid border-[#ffffff34] bg-[#110d10] p-[2px] opacity-0 shadow-lg shadow-[rgba(0,0,0,0.25)] motion-reduce:transition-none [&>.nav-btn:first-child]:rounded-tl-[16px] [&>.nav-btn:first-child]:rounded-tr-none [&>.nav-btn:last-child]:rounded-bl-[16px] [&>.nav-btn:last-child]:rounded-br-[16px]`}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
class="nav-btn flex items-center whitespace-nowrap hover:bg-[#1d1d1d] [&>*]:w-full [&>*]:px-4 [&>*]:py-2 [&>*]:text-left"
|
||||
>
|
||||
<a href="/account">Multiplayer</a>
|
||||
</div>
|
||||
<div
|
||||
role="separator"
|
||||
class="-ml-[2px] mb-[2px] mt-[2px] h-[2px] w-[calc(100%+4px)] bg-[#423e41]"
|
||||
/>
|
||||
<div
|
||||
role="button"
|
||||
class="nav-btn flex items-center whitespace-nowrap hover:bg-[#1d1d1d] [&>*]:w-full [&>*]:px-4 [&>*]:py-2 [&>*]:text-left"
|
||||
>
|
||||
<button on:click={signOut}>Sign out</button>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.user-profile-menu > img:hover {
|
||||
transform: scale(1.17) rotate(7deg);
|
||||
}
|
||||
|
||||
.user-profile-menu > img:active {
|
||||
transform: scale(0.85) rotate(0deg);
|
||||
}
|
||||
</style>
|
101
src/components/Dropdown.svelte
Normal file
101
src/components/Dropdown.svelte
Normal file
|
@ -0,0 +1,101 @@
|
|||
<script lang="ts">
|
||||
import { onMount, tick } from "svelte";
|
||||
|
||||
export let items: { name: string; value: string }[] = [];
|
||||
export let selected: (typeof items)[0] = items[0];
|
||||
let selectedIndex = 0;
|
||||
$: selected = items[selectedIndex];
|
||||
let expanded = false;
|
||||
let navItems: HTMLUListElement;
|
||||
|
||||
function recalculatePos() {
|
||||
const { right } = navItems.getBoundingClientRect();
|
||||
console.log(right, window.innerWidth);
|
||||
if (right > window.innerWidth) {
|
||||
navItems.style.left = `${window.innerWidth - right - 36}px`;
|
||||
} else {
|
||||
navItems.style.left = "0";
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle() {
|
||||
expanded = !expanded;
|
||||
await tick();
|
||||
// do we have enough space to the right of the navItems?
|
||||
recalculatePos();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
function close(e: MouseEvent | UIEvent) {
|
||||
if ("clientX" in e) {
|
||||
// check if we're clicking outside the dropdown
|
||||
if (
|
||||
e.target instanceof HTMLElement &&
|
||||
!e.target.classList.contains("dropdown") &&
|
||||
!e.target.closest(".dropdown")
|
||||
) {
|
||||
expanded = false;
|
||||
}
|
||||
} else {
|
||||
expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("mousedown", close);
|
||||
window.addEventListener("resize", close);
|
||||
return () => {
|
||||
window.removeEventListener("click", close);
|
||||
window.removeEventListener("resize", close);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative flex">
|
||||
<button
|
||||
class="flex w-full items-center justify-between rounded-2xl bg-zinc-950 p-2 ring ring-[#ffffff11] focus:ring-[#ffffff44]"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={expanded}
|
||||
on:click={toggle}
|
||||
type="button"
|
||||
>
|
||||
<span class="ml-1 mr-4">{selected.name}</span>
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<ul
|
||||
bind:this={navItems}
|
||||
class="absolute {expanded
|
||||
? 'block'
|
||||
: 'hidden'} top-full z-[9999] mt-2 max-h-[50vh] w-fit overflow-y-auto overflow-x-hidden rounded-2xl bg-zinc-950 ring ring-[#ffffff11] focus:ring-[#ffffff44]"
|
||||
role="listbox"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{#each items as item, i}
|
||||
<button
|
||||
role="option"
|
||||
aria-selected={selectedIndex === i}
|
||||
on:click={() => {
|
||||
selectedIndex = i;
|
||||
toggle();
|
||||
}}
|
||||
class="w-full cursor-pointer whitespace-nowrap px-4 py-3 text-left hover:bg-zinc-900"
|
||||
>
|
||||
{item.name}
|
||||
</button>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
[aria-expanded="true"] > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
|
@ -18,21 +18,26 @@
|
|||
); mask-image: var(--mask-image); -webkit-mask-image: var(--mask-image);
|
||||
"
|
||||
/>
|
||||
<div class="relative z-30 flex items-stretch gap-6">
|
||||
<div class="relative z-30 flex min-h-[0] min-w-[0] items-center gap-6">
|
||||
{#if room.game?.iconUrl}
|
||||
<img
|
||||
src={room.game.iconUrl}
|
||||
alt="Icon for '{room.preferredGameName}'"
|
||||
class="w-[100px] rounded-2xl object-cover"
|
||||
class="flex aspect-square max-h-[84px] max-w-[84px] shrink-0 rounded-2xl object-cover md:max-h-[148px] md:max-w-[148px]"
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<h2 class="mb-2 text-[20px] leading-[1.41] md:text-[28px] md:leading-[1.1]">
|
||||
<div class="flex h-full w-full flex-col overflow-hidden">
|
||||
<div class="flex items-center">
|
||||
<h2
|
||||
class="mb-2 overflow-hidden text-ellipsis whitespace-nowrap text-[20px] leading-[1.41] md:text-[28px] md:leading-[1.1]"
|
||||
>
|
||||
{room.name}
|
||||
<span class="ml-1 text-base font-normal text-gray-300"
|
||||
</h2>
|
||||
<span
|
||||
class="mb-[6px] ml-2 overflow-hidden text-ellipsis whitespace-nowrap text-base font-normal text-gray-300"
|
||||
>({room.game?.name || "No preferred game"})</span
|
||||
>
|
||||
</h2>
|
||||
</div>
|
||||
<p class="flex-grow">{room.description}</p>
|
||||
<div class="mt-2 text-sm text-gray-300">
|
||||
{room.players.length} / {room.maxPlayers} | {#if room.players.length > 4}
|
||||
|
|
|
@ -39,10 +39,6 @@ async function setupGames() {
|
|||
|
||||
const runAllTheInitFunctions = async () => {
|
||||
if (!db.isInitialized) await db.initialize();
|
||||
// sigh.
|
||||
const user = await userRepo.findOne({ where: { username: "nullptr" } });
|
||||
user!.roles = ["moderator"];
|
||||
await userRepo.save(user!);
|
||||
if (!server)
|
||||
try {
|
||||
initServer();
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
import type { CreateAccountRequest, CreateAccountResponse, GetUserResponse } from "$types/api";
|
||||
import type {
|
||||
CreateAccountRequest,
|
||||
CreateAccountResponse,
|
||||
GetUserResponse,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
} from "$types/api";
|
||||
|
||||
const apiUsers = {
|
||||
async createAccount(body: CreateAccountRequest): Promise<CreateAccountResponse> {
|
||||
return await SuyuAPI.req("POST", "/api/user", body);
|
||||
},
|
||||
|
||||
async login(body: LoginRequest): Promise<LoginResponse> {
|
||||
return await SuyuAPI.req("POST", "/api/user/login", body);
|
||||
},
|
||||
|
||||
async deleteAccount() {
|
||||
return await SuyuAPI.req("DELETE", "/api/user");
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { db } from "../db";
|
||||
import { SuyuUser } from "../schema";
|
||||
import { FriendshipRequest, SuyuUser } from "../schema";
|
||||
|
||||
export const userRepo = db.getRepository(SuyuUser);
|
||||
export const friendshipRepo = db.getRepository(FriendshipRequest);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Role } from "$types/db";
|
||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { BaseEntity, Column, Entity, ManyToMany, OneToOne, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export class SuyuUser extends BaseEntity {
|
||||
|
@ -27,4 +27,23 @@ export class SuyuUser extends BaseEntity {
|
|||
select: false,
|
||||
})
|
||||
email: string;
|
||||
|
||||
@Column("text", {
|
||||
select: false,
|
||||
})
|
||||
password: string;
|
||||
|
||||
@ManyToMany(() => SuyuUser)
|
||||
friends: SuyuUser[];
|
||||
}
|
||||
|
||||
export class FriendshipRequest extends BaseEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id: string;
|
||||
|
||||
@OneToOne(() => SuyuUser)
|
||||
from: SuyuUser;
|
||||
|
||||
@OneToOne(() => SuyuUser)
|
||||
to: SuyuUser;
|
||||
}
|
||||
|
|
|
@ -19,3 +19,23 @@ export async function getJwtData(token: string): Promise<IJwtData> {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
export class RateLimiter {
|
||||
// allow 5 requests per minute
|
||||
|
||||
cache = new Map<string, number>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
isLimited(ip: string): boolean {
|
||||
// if the last request was in the last minute, return true
|
||||
if (this.cache.has(ip)) {
|
||||
if (Date.now() - this.cache.get(ip)! < 5000) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// set the last request to now
|
||||
this.cache.set(ip, Date.now());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
export function load({ cookies, url }) {
|
||||
import { useAuth } from "$lib/util/api/index.js";
|
||||
|
||||
export async function load({ cookies, url }) {
|
||||
const token = cookies.get("token");
|
||||
const user = await useAuth(token || "");
|
||||
return {
|
||||
tokenCookie: token,
|
||||
url: url.pathname,
|
||||
user: { ...user },
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
import { generateTransition, transition } from "$lib/util/animation";
|
||||
import { reducedMotion } from "$lib/accessibility";
|
||||
import BackgroundProvider from "$components/BackgroundProvider.svelte";
|
||||
import AccountButton from "$components/AccountButton.svelte";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
|
@ -146,7 +147,7 @@
|
|||
},
|
||||
{
|
||||
name: "GitLab",
|
||||
href: "https://gitlab.com/suyu-emu/",
|
||||
href: "https://gitlab.com/suyu-emu/suyu",
|
||||
},
|
||||
{
|
||||
name: $token || data.tokenCookie ? "Account" : "Sign up",
|
||||
|
@ -286,9 +287,16 @@
|
|||
>
|
||||
<DiscordSolid />
|
||||
</a>
|
||||
<a href={$token ? "/account" : "/signup"} class="button-sm"
|
||||
{#if $token}
|
||||
<!-- <a href={$token ? "/account" : "/signup"} class="button-sm"
|
||||
>{$token ? "Account" : "Sign up"}</a
|
||||
>
|
||||
> -->
|
||||
<!-- <a href="/account" class="button-sm">Account</a> -->
|
||||
<AccountButton user={data.user} />
|
||||
{:else}
|
||||
<a href="/login" class="button-sm">Log in</a>
|
||||
<a href="/signup" class="button-sm">Sign up</a>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="relative mr-4 hidden flex-row gap-4 max-[625px]:flex">
|
||||
<button
|
||||
|
@ -319,7 +327,7 @@
|
|||
<div
|
||||
style="transition: 180ms ease;"
|
||||
aria-hidden={!dropdownOpenFinished && !dropdownOpen}
|
||||
class={`fixed left-0 z-[99999] h-screen w-full bg-[#0e0d10] p-9 pt-[120px] ${dropdownOpen ? "pointer-events-auto visible opacity-100" : "pointer-events-none opacity-0"} ${!dropdownOpen && dropdownCloseFinished ? "invisible" : ""}`}
|
||||
class={`fixed left-0 z-[100] h-screen w-full bg-[#0e0d10] p-9 pt-[120px] ${dropdownOpen ? "pointer-events-auto visible opacity-100" : "pointer-events-none opacity-0"} ${!dropdownOpen && dropdownCloseFinished ? "invisible" : ""}`}
|
||||
>
|
||||
<div class={`flex flex-col gap-8`}>
|
||||
<!-- <a href="##"><h1 class="w-full text-5xl">Blog</h1></a>
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<a
|
||||
href="https://gitlab.com/suyu-emu/"
|
||||
href="https://gitlab.com/suyu-emu/suyu"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="button text-[#8A8F98]"
|
||||
|
@ -259,7 +259,7 @@
|
|||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://gitlab.com/suyu-emu/"
|
||||
href="https://gitlab.com/suyu-emu/suyu"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="relative w-full rounded-[2.25rem] bg-[#f78c40] p-12 text-black"
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
href: "/account",
|
||||
},
|
||||
{
|
||||
name: "Lobbies",
|
||||
href: "/account/lobbies",
|
||||
name: "Public Game Lobby",
|
||||
href: "/account/lobby",
|
||||
},
|
||||
// {
|
||||
// name: "Friends",
|
||||
|
@ -61,6 +61,7 @@
|
|||
return;
|
||||
indicator.offsetHeight;
|
||||
const transformFactor = bounds.left - pillBounds.left;
|
||||
|
||||
navBar.animate(
|
||||
[
|
||||
{
|
||||
|
@ -77,9 +78,13 @@
|
|||
easing: "ease-in",
|
||||
},
|
||||
],
|
||||
{
|
||||
$reducedMotion
|
||||
? {
|
||||
duration: 360,
|
||||
delay: 0,
|
||||
}
|
||||
: {
|
||||
duration: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -87,7 +92,6 @@
|
|||
afterNavigate(({ from }) => {
|
||||
if (from) {
|
||||
if (!from.url.pathname.startsWith("/account")) {
|
||||
console.log("!");
|
||||
navBar.style.opacity = "0";
|
||||
navBar.animate(
|
||||
[
|
||||
|
@ -164,6 +168,14 @@
|
|||
};
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
const items = Array.from(document.querySelectorAll(".navitem")) as HTMLAnchorElement[];
|
||||
const item = items.find((i) => new URL(i.href).pathname === data.url);
|
||||
navClick({ target: item } as unknown as MouseEvent);
|
||||
}, 10);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#key data.url}
|
||||
|
@ -196,3 +208,12 @@
|
|||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media (max-width: 750px) {
|
||||
.navbar {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { getContext } from "svelte";
|
||||
import type { PageData } from "./$types";
|
||||
import type { Writable } from "svelte/store";
|
||||
import cookie from "cookiejs";
|
||||
|
||||
const token = getContext<Writable<string>>("token");
|
||||
|
||||
|
@ -28,6 +29,12 @@
|
|||
copyText = "Copy token";
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function signOut() {
|
||||
$token = "";
|
||||
cookie.remove("token");
|
||||
goto("/login");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative h-[calc(100vh-200px)] flex-col gap-6 overflow-hidden">
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Room from "$components/Room.svelte";
|
||||
import { reducedMotion } from "$lib/accessibility";
|
||||
import { transition } from "$lib/util/animation";
|
||||
import { onMount } from "svelte";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
function transitionIn() {
|
||||
const rooms = document.querySelectorAll<HTMLDivElement>(".room");
|
||||
rooms.forEach((room, i) => {
|
||||
const x = parseInt(room.dataset.index!);
|
||||
room.getAnimations().forEach((animation) => animation.cancel());
|
||||
room.style.zIndex = ((i + 1) * 5).toString();
|
||||
room.animate(
|
||||
[
|
||||
{
|
||||
transform: "translateY(-200px)",
|
||||
opacity: "0",
|
||||
filter: "blur(20px)",
|
||||
},
|
||||
{
|
||||
transform: "translateY(0px)",
|
||||
opacity: "1",
|
||||
filter: "blur(0px)",
|
||||
},
|
||||
],
|
||||
$reducedMotion
|
||||
? {
|
||||
duration: 0,
|
||||
fill: "forwards",
|
||||
}
|
||||
: {
|
||||
duration: 700,
|
||||
easing: transition,
|
||||
delay: x * 80,
|
||||
fill: "forwards",
|
||||
},
|
||||
).onfinish = () => {
|
||||
room.style.opacity = "1";
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
transitionIn();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative h-[calc(100vh-200px)]">
|
||||
<div class="room-grid relative flex w-full gap-4 pb-6">
|
||||
{#each data.rooms as room, i}
|
||||
<div class="room opacity-0" data-index={i}>
|
||||
<Room {room} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.room-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
grid-auto-rows: auto;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@media (min-width: 750px) {
|
||||
.room-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
162
src/routes/account/lobby/+page.svelte
Normal file
162
src/routes/account/lobby/+page.svelte
Normal file
|
@ -0,0 +1,162 @@
|
|||
<script lang="ts">
|
||||
import Room from "$components/Room.svelte";
|
||||
import { reducedMotion } from "$lib/accessibility";
|
||||
import { transition } from "$lib/util/animation";
|
||||
import { onMount, tick } from "svelte";
|
||||
import type { PageData } from "./$types";
|
||||
import Dropdown from "$components/Dropdown.svelte";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
function transitionIn() {
|
||||
const rooms = document.querySelectorAll<HTMLDivElement>(".room");
|
||||
rooms.forEach((room, i) => {
|
||||
const x = parseInt(room.dataset.index!);
|
||||
room.getAnimations().forEach((animation) => animation.cancel());
|
||||
room.style.zIndex = ((i + 1) * 5).toString();
|
||||
room.animate(
|
||||
[
|
||||
{
|
||||
transform: "translateY(-200px)",
|
||||
opacity: "0",
|
||||
filter: "blur(20px)",
|
||||
},
|
||||
{
|
||||
transform: "translateY(0px)",
|
||||
opacity: "1",
|
||||
filter: "blur(0px)",
|
||||
},
|
||||
],
|
||||
$reducedMotion
|
||||
? {
|
||||
duration: 0,
|
||||
fill: "forwards",
|
||||
}
|
||||
: {
|
||||
duration: 700,
|
||||
easing: transition,
|
||||
delay: x * 80,
|
||||
fill: "forwards",
|
||||
},
|
||||
).onfinish = () => {
|
||||
room.style.opacity = "1";
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
transitionIn();
|
||||
});
|
||||
|
||||
let filter: {
|
||||
name: string;
|
||||
value: string;
|
||||
} = { name: "", value: "" };
|
||||
|
||||
let extendedContainer: HTMLDivElement;
|
||||
|
||||
$: gamesFilters = [
|
||||
{
|
||||
name: "All",
|
||||
value: "",
|
||||
},
|
||||
...data.rooms
|
||||
.map((room) => room.game)
|
||||
.filter((game) => typeof game !== "undefined" && Boolean(game))
|
||||
.map((game) => ({
|
||||
name: game!.name,
|
||||
value: game!.name,
|
||||
}))
|
||||
.filter((game, i, arr) => arr.findIndex((g) => g.value === game.value) === i),
|
||||
];
|
||||
$: {
|
||||
if (browser) {
|
||||
filter;
|
||||
(async () => {
|
||||
await tick(); // wait for dom update :333
|
||||
transitionIn();
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (extendedContainer.style.opacity === "1") return;
|
||||
extendedContainer.animate(
|
||||
[
|
||||
{
|
||||
opacity: "0",
|
||||
},
|
||||
{
|
||||
opacity: "1",
|
||||
},
|
||||
],
|
||||
$reducedMotion
|
||||
? {
|
||||
duration: 0,
|
||||
fill: "forwards",
|
||||
}
|
||||
: {
|
||||
duration: 400,
|
||||
easing: transition,
|
||||
fill: "forwards",
|
||||
delay: 150,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
$: rooms =
|
||||
filter.value !== "" ? data.rooms.filter((r) => r.game?.name === filter.value) : data.rooms;
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={extendedContainer}
|
||||
class="pointer-events-none absolute -top-[60px] left-[50%] z-[999] flex h-11 w-full translate-x-[-50%] items-center opacity-0"
|
||||
>
|
||||
<div class="dropdown pointer-events-auto">
|
||||
<Dropdown items={gamesFilters} bind:selected={filter} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative h-[calc(100vh-200px)]">
|
||||
{#if rooms.length > 0}
|
||||
<div class="room-grid relative flex w-full gap-4 pb-6">
|
||||
{#each rooms as room, i}
|
||||
<div class="room min-h-0 min-w-0 overflow-hidden opacity-0" data-index={i}>
|
||||
<Room {room} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<i class="mt-4 block w-full text-center text-gray-500">
|
||||
{filter.value ? "No rooms matched your filter" : "No rooms are currently open"}...
|
||||
</i>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.room-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
grid-auto-rows: auto;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 750px) {
|
||||
.room-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { userRepo } from "$lib/server/repo";
|
||||
import type { SuyuUser } from "$lib/server/schema";
|
||||
import { json } from "$lib/server/util";
|
||||
import { RateLimiter, json } from "$lib/server/util";
|
||||
import { useAuth } from "$lib/util/api";
|
||||
import type {
|
||||
CreateAccountRequest,
|
||||
|
@ -16,6 +16,9 @@ import { verify } from "hcaptcha";
|
|||
import { PUBLIC_SITE_KEY } from "$env/static/public";
|
||||
import { HCAPTCHA_KEY } from "$env/static/private";
|
||||
import validator from "validator";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
const rateLimit = new RateLimiter();
|
||||
|
||||
const randomBytes = promisify(crypto.randomBytes);
|
||||
|
||||
|
@ -32,8 +35,14 @@ async function genKey(username: string) {
|
|||
}
|
||||
|
||||
export async function POST({ request, getClientAddress }) {
|
||||
if (rateLimit.isLimited(getClientAddress())) {
|
||||
return json<CreateAccountResponse>({
|
||||
success: false,
|
||||
error: "rate limited",
|
||||
});
|
||||
}
|
||||
const body: CreateAccountRequest = await request.json();
|
||||
if (!body.username || !body.email || !body.captchaToken) {
|
||||
if (!body.username || !body.email || !body.captchaToken || !body.password) {
|
||||
return json<CreateAccountResponse>({
|
||||
success: false,
|
||||
error: "missing fields",
|
||||
|
@ -77,14 +86,22 @@ export async function POST({ request, getClientAddress }) {
|
|||
}
|
||||
// the api key can only be 80 characters total, including the username and colon
|
||||
const key = await genKey(body.username);
|
||||
const password = await bcrypt.hash(body.password, 10);
|
||||
// sha256 hash of the email, trimmed and to lowercase
|
||||
const emailHash = crypto
|
||||
.createHash("sha256")
|
||||
.update(body.email.trim().toLowerCase())
|
||||
.digest("hex");
|
||||
const createdUser: SuyuUser = userRepo.create({
|
||||
username: body.username,
|
||||
avatarUrl: `https://avatars.githubusercontent.com/u/${Math.floor(Math.random() * 100000000)}?v=4`,
|
||||
avatarUrl: `https://gravatar.com/avatar/${emailHash}?d=retro`,
|
||||
displayName: body.username,
|
||||
roles: ["user"],
|
||||
apiKey: key,
|
||||
email: body.email,
|
||||
password,
|
||||
});
|
||||
console.log(createdUser);
|
||||
await userRepo.save(createdUser);
|
||||
return json<CreateAccountResponse>({
|
||||
success: true,
|
||||
|
@ -93,7 +110,12 @@ export async function POST({ request, getClientAddress }) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function GET({ request }) {
|
||||
export async function GET({ request, getClientAddress }) {
|
||||
if (rateLimit.isLimited(getClientAddress()))
|
||||
return json<GetUserResponse>({
|
||||
success: false,
|
||||
error: "rate limited",
|
||||
});
|
||||
const user = await useAuth(request);
|
||||
if (!user) {
|
||||
return json<GetUserResponse>({
|
||||
|
@ -107,7 +129,12 @@ export async function GET({ request }) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function DELETE({ request }) {
|
||||
export async function DELETE({ request, getClientAddress }) {
|
||||
if (rateLimit.isLimited(getClientAddress()))
|
||||
return json<DeleteAccountResponse>({
|
||||
success: false,
|
||||
error: "rate limited",
|
||||
});
|
||||
const user = await useAuth(request);
|
||||
if (!user) {
|
||||
return json<DeleteAccountResponse>({
|
||||
|
|
48
src/routes/api/user/login/+server.ts
Normal file
48
src/routes/api/user/login/+server.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { userRepo } from "$lib/server/repo";
|
||||
import { RateLimiter, json } from "$lib/server/util/index.js";
|
||||
import type { LoginResponse, LoginRequest } from "$types/api";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
const rateLimit = new RateLimiter();
|
||||
|
||||
export async function POST({ request, getClientAddress }) {
|
||||
if (rateLimit.isLimited(getClientAddress()))
|
||||
return json<LoginResponse>({
|
||||
success: false,
|
||||
error: "rate limited",
|
||||
});
|
||||
const body: LoginRequest = await request.json();
|
||||
if (
|
||||
!body.email ||
|
||||
!body.password ||
|
||||
body.email.trim() === "" ||
|
||||
body.password.trim() === "" ||
|
||||
body.email.length > 320 ||
|
||||
body.password.length > 320
|
||||
)
|
||||
return json<LoginResponse>({
|
||||
success: false,
|
||||
error: "missing fields",
|
||||
});
|
||||
const user = await userRepo.findOne({
|
||||
where: {
|
||||
email: body.email,
|
||||
},
|
||||
select: ["password", "apiKey"],
|
||||
});
|
||||
if (!user)
|
||||
return json<LoginResponse>({
|
||||
success: false,
|
||||
error: "user not found",
|
||||
});
|
||||
if (!(await bcrypt.compare(body.password, user.password))) {
|
||||
return json<LoginResponse>({
|
||||
success: false,
|
||||
error: "invalid password",
|
||||
});
|
||||
}
|
||||
return json<LoginResponse>({
|
||||
success: true,
|
||||
token: user.apiKey,
|
||||
});
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="relative flex w-full flex-col gap-6 overflow-hidden rounded-[2.25rem] bg-[#110d10] md:p-12"
|
||||
class="relative flex w-full flex-col gap-6 overflow-hidden rounded-[2.25rem] bg-[#110d10] p-8 md:p-12"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
12
src/routes/login/+page.server.ts
Normal file
12
src/routes/login/+page.server.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { RoomManager } from "$lib/server/class/Room.js";
|
||||
import { useAuth } from "$lib/util/api";
|
||||
|
||||
export async function load(opts) {
|
||||
const apiKey = opts.cookies.get("token");
|
||||
const user = await useAuth(apiKey || "unused");
|
||||
|
||||
return {
|
||||
user: { ...user },
|
||||
token: apiKey,
|
||||
};
|
||||
}
|
73
src/routes/login/+page.svelte
Normal file
73
src/routes/login/+page.svelte
Normal file
|
@ -0,0 +1,73 @@
|
|||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import { goto } from "$app/navigation";
|
||||
import HCaptcha from "$components/HCaptcha.svelte";
|
||||
import { PUBLIC_SITE_KEY } from "$env/static/public";
|
||||
import { SuyuAPI } from "$lib/client/api";
|
||||
import type { PageData } from "./$types";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { getContext, onMount } from "svelte";
|
||||
|
||||
const token = getContext<Writable<string>>("token");
|
||||
if ($token) goto("/account");
|
||||
|
||||
let emailInput = "";
|
||||
let passwordInput = "";
|
||||
$: disabled = !emailInput || !passwordInput;
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
if (Object.keys(data.user).length !== 0 && browser) goto("/account");
|
||||
|
||||
async function logIn() {
|
||||
const res = await SuyuAPI.users.login({
|
||||
email: emailInput,
|
||||
password: passwordInput,
|
||||
});
|
||||
if (!res.success) {
|
||||
// TODO: modal
|
||||
alert(res.error);
|
||||
return;
|
||||
}
|
||||
// set "token" cookie
|
||||
document.cookie = `token=${res.token}; path=/; max-age=31536000; samesite=strict`;
|
||||
$token = res.token;
|
||||
goto("/account");
|
||||
}
|
||||
|
||||
function enter(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") logIn();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="align-center relative flex h-[calc(100vh-200px)] flex-col items-center justify-center gap-6"
|
||||
>
|
||||
<div class="flex h-fit w-full max-w-[500px] flex-col rounded-[2.25rem] bg-[#110d10] p-10">
|
||||
<h1 class="text-[48px] md:text-[60px] md:leading-[1.1]">Log in</h1>
|
||||
<div class="mt-4 flex flex-col gap-4">
|
||||
<p>
|
||||
Lost your account? <a class="link" href="https://discord.gg/suyu" target="_blank"
|
||||
>Contact us</a
|
||||
>.
|
||||
</p>
|
||||
<input
|
||||
bind:value={emailInput}
|
||||
maxlength="128"
|
||||
class="input"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
placeholder="Email"
|
||||
/>
|
||||
<input
|
||||
autocomplete="current-password"
|
||||
bind:value={passwordInput}
|
||||
class="input"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
on:keydown={enter}
|
||||
/>
|
||||
<button {disabled} on:click={logIn} class="cta-button mt-2">Log in</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -4,10 +4,8 @@ import { useAuth } from "$lib/util/api";
|
|||
export async function load(opts) {
|
||||
const apiKey = opts.cookies.get("token");
|
||||
const user = await useAuth(apiKey || "unused");
|
||||
const rooms = RoomManager.getRooms().map((r) => r.toJSON());
|
||||
return {
|
||||
user: { ...user },
|
||||
rooms,
|
||||
token: apiKey,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,8 +13,9 @@
|
|||
|
||||
let usernameInput = "";
|
||||
let emailInput = "";
|
||||
let passwordInput = "";
|
||||
let captchaToken = "";
|
||||
$: disabled = !usernameInput || !emailInput || !captchaToken;
|
||||
$: disabled = !usernameInput || !emailInput || !captchaToken || !passwordInput;
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
|
@ -25,6 +26,7 @@
|
|||
username: usernameInput,
|
||||
email: emailInput,
|
||||
captchaToken,
|
||||
password: passwordInput,
|
||||
});
|
||||
if (!res.success) {
|
||||
// TODO: modal
|
||||
|
@ -48,37 +50,48 @@
|
|||
<div class="flex h-fit w-full max-w-[500px] flex-col rounded-[2.25rem] bg-[#110d10] p-10">
|
||||
<h1 class="text-[48px] md:text-[60px] md:leading-[1.1]">Sign up</h1>
|
||||
<div class="mt-4 flex flex-col gap-4">
|
||||
<p class="useless-text">
|
||||
suyu believes in user privacy; as such, usernames are distributed on a first-come,
|
||||
first-serve basis, with no password required. Accounts are used for:
|
||||
</p>
|
||||
<p class="useless-text">Accounts are used for:</p>
|
||||
<ul class="list [&>*]:before:mr-3 [&>*]:before:content-['•']">
|
||||
<li>Creating rooms</li>
|
||||
<li>Adding friends</li>
|
||||
</ul>
|
||||
<p>
|
||||
Lost your account? <a class="link" href="https://discord.gg/suyu" target="_blank"
|
||||
>Contact us</a
|
||||
>.
|
||||
</p>
|
||||
<form
|
||||
class="contents"
|
||||
on:submit={(e) => {
|
||||
e.preventDefault();
|
||||
signUp();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
bind:value={emailInput}
|
||||
maxlength="128"
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Recovery Email"
|
||||
placeholder="Email"
|
||||
autocomplete="email"
|
||||
/>
|
||||
<input
|
||||
bind:value={usernameInput}
|
||||
maxlength="24"
|
||||
class="input"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="Username"
|
||||
/>
|
||||
<input
|
||||
bind:value={passwordInput}
|
||||
class="input"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<div class="h-[78px]">
|
||||
<HCaptcha on:success={captchaComplete} theme="dark" sitekey={PUBLIC_SITE_KEY} />
|
||||
</div>
|
||||
<button {disabled} on:click={signUp} class="cta-button mt-2">Sign up</button>
|
||||
<button {disabled} type="submit" on:click={signUp} class="cta-button mt-2"
|
||||
>Sign up</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
13
src/types/api.d.ts
vendored
13
src/types/api.d.ts
vendored
|
@ -11,6 +11,7 @@ export interface CreateAccountRequest {
|
|||
username: string;
|
||||
email: string;
|
||||
captchaToken: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface CreateAccountResponseSuccess {
|
||||
|
@ -32,3 +33,15 @@ export interface GetUserResponseSuccess {
|
|||
}
|
||||
|
||||
export type GetUserResponse = GetUserResponseSuccess | GenericFailureResponse;
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponseSuccess {
|
||||
success: true;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export type LoginResponse = LoginResponseSuccess | GenericFailureResponse;
|
||||
|
|
Loading…
Reference in a new issue