add cool page transitions, accounts page revamp

Co-authored-by: Evan Song <ferothefox@users.noreply.github.com>
This commit is contained in:
not-nullptr 2024-03-16 00:30:53 +00:00
parent 3a364092eb
commit 69f7976702
25 changed files with 985 additions and 147 deletions

250
package-lock.json generated
View file

@ -12,10 +12,13 @@
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/enhanced-img": "^0.1.8",
"better-sqlite3": "^9.4.3",
"carbon-components-svelte": "^0.84.0",
"cookie": "^0.6.0",
"email-validator": "^2.0.4",
"hcaptcha": "^0.1.1",
"jsonwebtoken": "^9.0.2",
"prism-themes": "^1.9.0",
"prismjs": "^1.29.0",
"radix-svelte": "^0.9.0",
"reflect-metadata": "^0.2.1",
"sequelize": "^6.37.1",
@ -34,8 +37,10 @@
"@sveltejs/adapter-node": "^5.0.1",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/typography": "^0.5.10",
"@types/cookie": "^0.6.0",
"@types/jsonwebtoken": "^9.0.6",
"@types/prismjs": "^1.26.3",
"@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"autoprefixer": "^10.4.16",
@ -485,6 +490,14 @@
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
"optional": true
},
"node_modules/@ibm/telemetry-js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ibm/telemetry-js/-/telemetry-js-1.3.0.tgz",
"integrity": "sha512-9gIkyF2B9RizWN6rsdQN76DN6D+/Xbr4HGTwm6EUujfXvEVtWbf4jzxDFwKvZkeTC2tjHpkUNJQKdivbMKt8yg==",
"bin": {
"ibmtelemetry": "dist/collect.js"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.2",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz",
@ -1542,6 +1555,34 @@
"vite": "^5.0.0"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz",
"integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==",
"dev": true,
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@ -1611,6 +1652,12 @@
"undici-types": "~5.26.4"
}
},
"node_modules/@types/prismjs": {
"version": "1.26.3",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.3.tgz",
"integrity": "sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw==",
"dev": true
},
"node_modules/@types/pug": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@ -2892,6 +2939,16 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"optional": true,
"peer": true,
"engines": {
"node": ">=4"
}
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
@ -3117,6 +3174,16 @@
}
]
},
"node_modules/carbon-components-svelte": {
"version": "0.84.0",
"resolved": "https://registry.npmjs.org/carbon-components-svelte/-/carbon-components-svelte-0.84.0.tgz",
"integrity": "sha512-NSIGFYsmUJ04aOuQaS8e5ldnufj8gJpYnaPnMM3O+jQJdd6bEpGKhuh7fpdB0U67QEQYbSrxmuRzThLh85Qpnw==",
"hasInstallScript": true,
"dependencies": {
"@ibm/telemetry-js": "^1.2.1",
"flatpickr": "4.6.9"
}
},
"node_modules/caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
@ -5582,6 +5649,11 @@
"node": ">= 0.10"
}
},
"node_modules/flatpickr": {
"version": "4.6.9",
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.9.tgz",
"integrity": "sha512-F0azNNi8foVWKSF+8X+ZJzz8r9sE1G4hl06RyceIaLvyltKvDl6vqk9Lm/6AUUCi5HWaIjiUbk7UpeE/fOXOpw=="
},
"node_modules/flowbite": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.3.0.tgz",
@ -7899,6 +7971,12 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"dev": true
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@ -7929,6 +8007,12 @@
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@ -9339,6 +9423,13 @@
"node": ">=4"
}
},
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
"optional": true,
"peer": true
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -9562,11 +9653,102 @@
"is-reference": "^3.0.0"
}
},
"node_modules/pg": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
"integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
"optional": true,
"peer": true,
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.6.2",
"pg-pool": "^3.6.1",
"pg-protocol": "^1.6.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.1.1"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
"optional": true,
"peer": true
},
"node_modules/pg-connection-string": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
"integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"optional": true,
"peer": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
"integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
"optional": true,
"peer": true,
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==",
"optional": true,
"peer": true
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"optional": true,
"peer": true,
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"optional": true,
"peer": true,
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -9794,6 +9976,49 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"optional": true,
"peer": true,
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"optional": true,
"peer": true,
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prebuild-install": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
@ -9961,6 +10186,19 @@
"node": ">= 0.8"
}
},
"node_modules/prism-themes": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/prism-themes/-/prism-themes-1.9.0.tgz",
"integrity": "sha512-tX2AYsehKDw1EORwBps+WhBFKc2kxfoFpQAjxBndbZKr4fRmMkv47XN0BghC/K1qwodB1otbe4oF23vUTFDokw=="
},
"node_modules/prismjs": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
"engines": {
"node": ">=6"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -11409,6 +11647,16 @@
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"optional": true,
"peer": true,
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
@ -14011,7 +14259,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=0.4"
}

View file

@ -17,8 +17,10 @@
"@sveltejs/adapter-node": "^5.0.1",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/typography": "^0.5.10",
"@types/cookie": "^0.6.0",
"@types/jsonwebtoken": "^9.0.6",
"@types/prismjs": "^1.26.3",
"@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"autoprefixer": "^10.4.16",
@ -50,10 +52,13 @@
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/enhanced-img": "^0.1.8",
"better-sqlite3": "^9.4.3",
"carbon-components-svelte": "^0.84.0",
"cookie": "^0.6.0",
"email-validator": "^2.0.4",
"hcaptcha": "^0.1.1",
"jsonwebtoken": "^9.0.2",
"prism-themes": "^1.9.0",
"prismjs": "^1.29.0",
"radix-svelte": "^0.9.0",
"reflect-metadata": "^0.2.1",
"sequelize": "^6.37.1",

View file

@ -12,6 +12,11 @@
font-display: swap;
}
@font-face {
font-family: "Consolas";
src: url(./assets/fonts/Consolas.ttf) format("truetype");
}
html,
body {
min-height: 100%;

Binary file not shown.

View file

@ -0,0 +1,29 @@
<script lang="ts">
import prism from "prismjs";
import "prismjs/components/prism-json";
import "prismjs/components/prism-bash";
import "prismjs/components/prism-css";
import "prismjs/components/prism-csv";
import "prismjs/components/prism-git";
import "prismjs/components/prism-javascript";
import "prismjs/components/prism-jsx";
import "prismjs/components/prism-markdown";
import "prismjs/components/prism-powershell";
import "prismjs/components/prism-scss";
import "prismjs/components/prism-typescript";
import "prism-themes/themes/prism-vsc-dark-plus.min.css";
import { CodeSnippet } from "carbon-components-svelte";
export let lang: string;
export let text: string;
let formatted = "";
$: if (prism.languages[lang]) {
formatted = prism.highlight(text, prism.languages[lang], lang);
}
</script>
<code
class="block whitespace-pre rounded-xl bg-zinc-900 p-4 font-normal *:font-[Consolas] before:content-none after:content-none"
>{@html formatted.trim()}
</code>

View file

@ -3,6 +3,7 @@ import "reflect-metadata";
import { building } from "$app/environment";
import type { Handle } from "@sveltejs/kit";
import { WebSocketServer } from "ws";
import { userRepo } from "$lib/server/repo";
let server: WebSocketServer;
@ -12,9 +13,7 @@ function initServer() {
port: 21563,
path: "/net",
});
server.on("error", (err) => {
console.error("WebSocket server error:", err);
});
server.on("error", (err) => {});
server.on("connection", (socket) => {
socket.on("message", (data) => {
socket.send(data);
@ -25,6 +24,10 @@ function initServer() {
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();

View file

@ -0,0 +1,28 @@
import { browser } from "$app/environment";
import { readable } from "svelte/store";
const reducedMotionQuery = "(prefers-reduced-motion: reduce)";
const getInitialMotionPreference = () =>
browser ? window.matchMedia(reducedMotionQuery).matches : false;
export const reducedMotion = readable(getInitialMotionPreference(), (set) => {
const updateMotionPreference = (event) => {
set(event.matches);
};
let queryList = browser ? window.matchMedia(reducedMotionQuery) : null;
function initialize() {
queryList?.addEventListener("change", updateMotionPreference);
updateMotionPreference(window.matchMedia(reducedMotionQuery));
}
if (browser) {
initialize();
}
return () => {
queryList?.removeEventListener("change", updateMotionPreference);
};
});

View file

@ -15,8 +15,8 @@ export class SuyuUser extends BaseEntity {
@Column("text")
avatarUrl: string;
@Column("text")
roles: string;
@Column("json")
roles: Role[];
@Column("text", {
select: false,

View file

@ -11,14 +11,6 @@ export function json<T>(body: T): Response {
});
}
export function serializeRoles(roles: Role[]): string {
return roles.join("|");
}
export function deserializeRoles(roles: string): Role[] {
return roles.split("|") as Role[];
}
export async function getJwtData(token: string): Promise<IJwtData> {
return new Promise((resolve, reject) => {
jwt.verify(token, PUBLIC_KEY, { algorithms: ["RS256"] }, (err, data) => {

View file

@ -32,3 +32,17 @@ export async function useAuth(request: Request | string): Promise<SuyuUser | nul
});
return user;
}
export async function useModeratorAuth(request: Request | string): Promise<{
user: SuyuUser;
isModerator: boolean;
} | null> {
const user = await useAuth(request);
if (!user) {
return null;
}
return {
user,
isModerator: user.roles.includes("moderator"),
};
}

View file

@ -22,7 +22,5 @@
<h1 class="text-[42px] leading-[1.41] md:text-[84px] md:leading-[1.1]">
{status}: "{message}"
</h1>
<p style="text-align: center;" class="max-w-[36rem] text-lg leading-relaxed text-[#A6A5A7]">
Sorry about that. Click the suyu logo to go home.
</p>
<a href="/" class="cta-button">Take me back home</a>
</div>

View file

@ -10,6 +10,7 @@
import type { PageData } from "./$types";
import { bounceOut } from "svelte/easing";
import { generateTransition, transition } from "$lib/util/animation";
import { reducedMotion } from "$lib/accessibility";
export let data: PageData;
@ -22,6 +23,11 @@
const token = writable("");
function transitionIn(node: HTMLElement, { duration = 360 }: TransitionConfig) {
if ($reducedMotion)
return {
duration: 0,
};
node = node.querySelector(".content") || node;
const UA = navigator.userAgent;
const ff = UA.indexOf("Firefox") > -1;
if (!dropdownCloseFinished) {
@ -30,12 +36,12 @@
{
top: "160px",
opacity: "0",
filter: ff ? "" : "blur(20px)",
filter: ff ? "none" : "blur(20px)",
},
{
top: "0",
opacity: "1",
filter: ff ? "" : "blur(0px)",
filter: ff ? "none" : "blur(0px)",
},
],
{
@ -54,12 +60,12 @@
{
top: "-240px",
opacity: "0",
filter: ff ? "" : "blur(20px)",
filter: ff ? "none" : "blur(20px)",
},
{
top: "0",
opacity: "1",
filter: ff ? "" : "blur(0px)",
filter: ff ? "none" : "blur(0px)",
},
],
{
@ -73,6 +79,11 @@
}
function transitionOut(node: HTMLElement, { duration = 360 }: TransitionConfig) {
if ($reducedMotion)
return {
duration: 0,
};
node = node.querySelector(".content") || node;
if (!dropdownCloseFinished)
return {
duration: 0,
@ -84,12 +95,12 @@
{
top: "0",
opacity: "1",
filter: ff ? "" : "blur(0px)",
filter: ff ? "none" : "blur(0px)",
},
{
top: "240px",
opacity: "0",
filter: ff ? "" : "blur(80px)",
filter: ff ? "none" : "blur(80px)",
},
],
{
@ -113,7 +124,7 @@
[key: string]: string;
} = {};
const navItems: NavItem[] = [
$: navItems = [
{
name: "Blog",
href: "/blog",
@ -134,7 +145,11 @@
name: "GitLab",
href: "https://gitlab.com/suyu-emu/",
},
];
{
name: $token ? "Account" : "Sign up",
href: $token ? "/account" : "/signup",
},
] as NavItem[];
$: {
if (browser) {
@ -250,9 +265,9 @@
>
<DiscordSolid />
</a>
<!-- <a href={$token ? "/account" : "/signup"} class="button-sm"
<a href={$token ? "/account" : "/signup"} class="button-sm"
>{$token ? "Account" : "Sign up"}</a
> -->
>
</div>
<div class="relative mr-4 hidden flex-row gap-4 max-[625px]:flex">
<button
@ -283,7 +298,7 @@
<div
style="transition: 180ms ease;"
aria-hidden={!dropdownOpenFinished && !dropdownOpen}
class={`fixed left-0 z-10 h-screen w-full bg-black p-9 pt-[120px] ${dropdownOpen ? "pointer-events-auto visible opacity-100" : "pointer-events-none opacity-0"} ${!dropdownOpen && dropdownCloseFinished ? "invisible" : ""}`}
class={`fixed left-0 z-[99999] h-screen w-full bg-black 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>
@ -344,7 +359,6 @@
<div
in:transitionIn={{ duration: 500 }}
out:transitionOut={{ duration: 500 }}
style="transition: 360ms {transition}; transition-property: opacity, transform;"
aria-hidden={dropdownOpenFinished && dropdownOpen}
tabindex={dropdownOpen ? 0 : -1}
class={`absolute left-[50%] z-50 mx-auto flex w-screen max-w-[1300px] translate-x-[-50%] flex-col px-8 pb-12 pt-[120px] ${dropdownOpen ? "pointer-events-none translate-y-[25vh] opacity-0" : ""} ${dropdownOpenFinished && dropdownOpen ? "invisible" : ""}`}

View file

@ -0,0 +1,201 @@
<script lang="ts">
import { browser } from "$app/environment";
import { transition } from "$lib/util/animation";
import { onMount } from "svelte";
import type { PageData } from "./$types";
import { page } from "$app/stores";
import { writable } from "svelte/store";
import { afterNavigate, beforeNavigate } from "$app/navigation";
import { v4 } from "uuid";
import { reducedMotion } from "$lib/accessibility";
interface NavItem {
name: string;
href: string;
}
let indicator: HTMLDivElement;
let navBar: HTMLDivElement;
let selected = 0;
export let data: PageData;
const navItems: NavItem[] = [
{
name: "Online Services",
href: "/account",
},
{
name: "Lobbies",
href: "/account/lobbies",
},
{
name: "Settings",
href: "/account/settings",
},
];
function navClick(e: MouseEvent | HTMLAnchorElement) {
const navBars = document.querySelectorAll<HTMLDivElement>(".navbar");
if (navBars.length !== 1)
navBars.forEach((bar) => {
if (!bar.classList.contains($page.url.pathname)) {
bar.style.opacity = "0";
}
});
else {
navBar.style.zIndex = "1";
}
const target = e.target as HTMLAnchorElement;
const index = parseInt(target.dataset.index!);
selected = index;
const bounds = target.getBoundingClientRect();
const navBounds = navBar.getBoundingClientRect();
const pillBounds = indicator.getBoundingClientRect();
indicator.style.transform = `translateX(${bounds.left - navBounds.left}px)`;
indicator.style.width = `${bounds.width}px`;
if ((selected !== 0 && selected !== navItems.length - 1) || $reducedMotion) return;
indicator.offsetHeight;
const transformFactor = bounds.left - pillBounds.left;
navBar.animate(
[
{
transform: "translateX(0px)",
easing: "ease-out",
},
{
transform: `translateX(${transformFactor / 100}px)`,
offset: 0.1,
easing: "ease-out",
},
{
transform: `translateX(${-transformFactor / 200}px)`,
offset: 0.8,
easing: "ease-in",
},
{
transform: "translateX(0px)",
easing: "ease-in",
},
],
{
duration: 500,
delay: 170,
},
);
}
afterNavigate(({ from }) => {
if (from) {
if (!from.url.pathname.startsWith("/account")) {
console.log("!");
navBar.style.opacity = "0";
navBar.animate(
[
{
opacity: "0",
filter: "blur(20px)",
marginTop: "-96px",
},
{
opacity: "1",
filter: "blur(0px)",
marginTop: "0px",
},
],
$reducedMotion
? {
duration: 0,
}
: {
duration: 500,
easing: transition,
delay: 60,
},
).onfinish = () => {
navBar.style.opacity = "1";
};
selected = 0;
navClick({ target: document.querySelector(".navitem")! } as unknown as MouseEvent);
return;
}
}
const prevIndex = from ? navItems.findIndex((i) => from.url.pathname === i.href) : 0;
const items = Array.from(document.querySelectorAll(".navitem")) as HTMLAnchorElement[];
const oldItem = items.find((i) => i.dataset.index === prevIndex.toString());
if (!oldItem) return;
const oldItemBounds = oldItem.getBoundingClientRect();
const navBounds = navBar.getBoundingClientRect();
const oldTransition = indicator.style.transition;
indicator.style.transition = "0s";
indicator.style.transform = `translateX(${oldItemBounds.left - navBounds.left}px)`;
indicator.style.width = `${oldItemBounds.width}px`;
indicator.offsetHeight;
indicator.style.transition = oldTransition;
const item = items.find((i) => new URL(i.href).pathname === data.url);
navClick({ target: item } as unknown as MouseEvent);
});
beforeNavigate(({ to }) => {
if (!to) return;
if (!to.url.pathname.startsWith("/account")) {
if (navBar.style.opacity === "0") return;
navBar.animate(
[
{
opacity: "1",
filter: "blur(0px)",
transform: "translateY(0px)",
},
{
opacity: "0",
filter: "blur(20px)",
transform: "translateY(-50px)",
},
],
$reducedMotion
? {
duration: 0,
}
: {
duration: 360,
easing: transition,
},
).onfinish = () => {
navBar.style.opacity = "0";
};
}
});
</script>
{#key data.url}
<div
class={`navbar ${data.url} relative z-50 mb-4 flex w-max gap-1 overflow-hidden rounded-full bg-black p-1`}
bind:this={navBar}
>
<div
bind:this={indicator}
style="transition: 360ms {transition}"
class="pointer-events-none absolute left-0 top-[4px] z-10 h-[calc(100%-8px)] translate-x-0 transform rounded-full bg-gradient-to-b from-slate-50 to-[#a9a9a9] mix-blend-difference motion-reduce:!transition-none"
></div>
{#each navItems as item, i}
<a
href={item.href}
data-index={i}
on:click={navClick}
class={`navitem flex flex-grow basis-[0] items-center justify-center whitespace-nowrap rounded-full px-4 py-2 text-sm font-bold ${
selected === i ? " text-[#a9a9a9] opacity-100" : "opacity-50"
}`}
>
{item.name}
</a>
{/each}
</div>
{/key}
<div class="relative">
<div class="content absolute w-full">
<slot />
</div>
</div>

View file

@ -0,0 +1,13 @@
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");
const rooms = RoomManager.getRooms().map((r) => r.toJSON());
return {
user: { ...user },
rooms,
token: apiKey,
};
}

View file

@ -0,0 +1,80 @@
<script lang="ts">
import { browser } from "$app/environment";
import { goto } from "$app/navigation";
import { getContext } from "svelte";
import type { PageData } from "./$types";
import type { Writable } from "svelte/store";
const token = getContext<Writable<string>>("token");
let copyText = "Copy token";
export let data: PageData;
$: b64Token = btoa(data.token || "");
$: {
if (Object.keys(data.user).length === 0 && browser) {
$token = "";
document.cookie =
"token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; samesite=strict";
console.log("no user");
goto("/signup");
}
}
function copyToken() {
navigator.clipboard.writeText(b64Token);
copyText = "Copied!";
setTimeout(() => {
copyText = "Copy token";
}, 2000);
}
</script>
<div class="relative h-[calc(100vh-200px)] flex-col gap-6 overflow-hidden">
<div
class="relative flex w-full flex-col gap-6 overflow-hidden rounded-[2.25rem] bg-black p-8 md:p-12"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="512"
height="525"
viewBox="0 0 512 525"
fill="none"
style="animation-duration: 300s; transform-origin: 50% 50%; animation-iteration-count: infinite; animation-timing-function: linear; animation-name: spin; animation-delay: 0s; animation-direction: normal; animation-fill-mode: none; animation-play-state: running;"
class="pointer-events-none absolute -bottom-[18rem] right-0 z-0 animate-spin opacity-20"
>
<path
d="M511.5 262.12C511.5 353.613 465.547 434.182 396.019 480.947C408.179 457.937 415.083 431.597 415.083 403.617C415.083 313.723 343.816 240.744 255.992 240.744C191.257 240.744 138.692 186.941 138.692 120.622C138.692 54.3027 191.257 0.5 255.992 0.5C397.026 0.5 511.5 117.695 511.5 262.12ZM255.992 53.5225C243.745 53.5225 233.816 63.7047 233.816 76.2224C233.816 88.7388 243.745 98.9223 255.992 98.9223C268.257 98.9223 278.173 88.7387 278.173 76.2224C278.173 63.7048 268.257 53.5225 255.992 53.5225ZM299.355 97.9223C287.104 97.9223 277.173 108.104 277.173 120.622C277.173 133.139 287.104 143.322 299.355 143.322C311.62 143.322 321.536 133.139 321.536 120.622C321.536 108.104 311.62 97.9223 299.355 97.9223ZM212.635 97.9223C200.382 97.9223 190.455 108.104 190.455 120.622C190.455 133.139 200.382 143.322 212.635 143.322C224.889 143.322 234.816 133.139 234.816 120.622C234.816 108.104 224.888 97.9223 212.635 97.9223ZM255.992 142.322C243.745 142.322 233.816 152.505 233.816 165.021C233.816 177.539 243.745 187.721 255.992 187.721C268.257 187.721 278.173 177.538 278.173 165.021C278.173 152.505 268.257 142.322 255.992 142.322Z"
stroke="white"
/>
<path
d="M0.5 262.119C0.5 170.626 46.444 90.0553 115.976 43.2909C103.82 66.3019 96.9172 92.6424 96.9172 120.622C96.9172 210.516 168.174 283.495 255.992 283.495C320.735 283.495 373.305 337.298 373.305 403.617C373.305 469.934 320.735 523.739 255.992 523.739C114.974 523.739 0.5 406.544 0.5 262.119ZM255.992 336.517C243.744 336.517 233.816 346.7 233.816 359.217C233.816 371.735 243.745 381.917 255.992 381.917C268.256 381.917 278.173 371.735 278.173 359.217C278.173 346.701 268.256 336.517 255.992 336.517ZM299.355 380.917C287.104 380.917 277.173 391.099 277.173 403.617C277.173 416.135 287.104 426.317 299.355 426.317C311.619 426.317 321.536 416.135 321.536 403.617C321.536 391.099 311.619 380.917 299.355 380.917ZM255.992 425.317C243.745 425.317 233.816 435.499 233.816 448.016C233.816 460.533 243.744 470.717 255.992 470.717C268.256 470.717 278.173 460.533 278.173 448.016C278.173 435.499 268.256 425.317 255.992 425.317ZM212.634 380.917C200.382 380.917 190.454 391.099 190.454 403.617C190.454 416.135 200.382 426.317 212.634 426.317C224.888 426.317 234.816 416.135 234.816 403.617C234.816 391.099 224.888 380.917 212.634 380.917Z"
stroke="white"
/>
</svg>
<h1 class="text-[36px] leading-[1.41] md:text-[60px] md:leading-[1.1]">
suyu Online Services
</h1>
<p class="text-wrap text-lg leading-relaxed text-[#A6A5A7]">
Your token should be kept private. If you believe it has been compromised, please
contact us immediately.
</p>
<div class="flex gap-4">
<div
class="input !w-fit max-w-full select-all overflow-hidden text-ellipsis whitespace-pre"
>
<p
style="transition: 180ms ease; transition-property: filter;"
class="w-fit blur hover:blur-none"
>
{b64Token}
</p>
</div>
<button class="button-sm" on:click={copyToken}>{copyText}</button>
</div>
<div class="flex gap-4">
<a href="/account/friends" class="button-sm">Manage Friends</a>
<a href="/account/rooms" class="button-sm">Rooms</a>
</div>
</div>
</div>

View file

@ -0,0 +1,7 @@
<div class="relative h-[calc(100vh-200px)] flex-col gap-6 overflow-hidden">
<div
class="relative flex w-full flex-col gap-6 overflow-hidden rounded-[2.25rem] bg-black p-8 md:p-12"
>
LOBBIES YEHAHH
</div>
</div>

View file

@ -0,0 +1,7 @@
<div class="relative h-[calc(100vh-200px)] flex-col gap-6 overflow-hidden">
<div
class="relative flex w-full flex-col gap-6 overflow-hidden rounded-[2.25rem] bg-black p-8 md:p-12"
>
SETTTTTINGGGASSS WOOOOOOOOOOOO
</div>
</div>

View file

@ -0,0 +1,122 @@
// TODO: refactor into external utils (ie Suyu.createAccount() or something???)
import { userRepo } from "$lib/server/repo";
import type { SuyuUser } from "$lib/server/schema";
import { json } from "$lib/server/util";
import { useAuth } from "$lib/util/api";
import type {
CreateAccountRequest,
CreateAccountResponse,
DeleteAccountResponse,
GetUserResponse,
} from "$types/api";
import crypto from "crypto";
import { promisify } from "util";
import { verify } from "hcaptcha";
import { PUBLIC_SITE_KEY } from "$env/static/public";
import { HCAPTCHA_KEY } from "$env/static/private";
import validator from "validator";
const randomBytes = promisify(crypto.randomBytes);
async function genKey(username: string) {
const random = (await randomBytes(80)).toString("hex");
let apiKey = `${username}:${random}`;
let b64ApiKey = Buffer.from(apiKey).toString("base64");
if (b64ApiKey.length > 80) {
b64ApiKey = b64ApiKey.slice(0, 80);
}
// decode b64ApiKey
apiKey = Buffer.from(b64ApiKey, "base64").toString("utf-8");
return apiKey;
}
export async function POST({ request, getClientAddress }) {
const body: CreateAccountRequest = await request.json();
if (!body.username || !body.email || !body.captchaToken) {
return json<CreateAccountResponse>({
success: false,
error: "missing fields",
});
}
if (body.username.length < 3 || body.username.length > 24) {
return json<CreateAccountResponse>({
success: false,
error: "invalid username",
});
}
if (!validator.isEmail(body.email)) {
return json<CreateAccountResponse>({
success: false,
error: "invalid email",
});
}
const res = await verify(HCAPTCHA_KEY, body.captchaToken, getClientAddress(), PUBLIC_SITE_KEY);
if (!res.success) {
return json<CreateAccountResponse>({
success: false,
error: "missing fields!",
});
}
// check if user exists
const user = await userRepo.findOne({
where: [
{
username: body.username,
},
{
email: body.email,
},
],
});
if (user) {
return json<CreateAccountResponse>({
success: false,
error: "user already exists",
});
}
// the api key can only be 80 characters total, including the username and colon
const key = await genKey(body.username);
const createdUser: SuyuUser = userRepo.create({
username: body.username,
avatarUrl: `https://avatars.githubusercontent.com/u/${Math.floor(Math.random() * 100000000)}?v=4`,
displayName: body.username,
roles: ["user"],
apiKey: key,
email: body.email,
});
await userRepo.save(createdUser);
return json<CreateAccountResponse>({
success: true,
token: createdUser.apiKey,
user: createdUser,
});
}
export async function GET({ request }) {
const user = await useAuth(request);
if (!user) {
return json<GetUserResponse>({
success: false,
error: "unauthorized",
});
}
return json<GetUserResponse>({
success: true,
user,
});
}
export async function DELETE({ request }) {
const user = await useAuth(request);
if (!user) {
return json<DeleteAccountResponse>({
success: false,
error: "unauthorized",
});
}
await userRepo.remove(user);
return json<DeleteAccountResponse>({
success: true,
});
}

View file

@ -1,8 +1,10 @@
import path from "path";
import fs from "fs/promises";
import { error } from "@sveltejs/kit";
import { useModeratorAuth } from "$lib/util/api/index.js";
import type { SuyuUser } from "$lib/server/schema/index.js";
export async function load({ params }) {
export async function load({ request }) {
const basePath = "static/blog";
const files = await fs.readdir(basePath);
// get all file contents in an array
@ -31,7 +33,12 @@ export async function load({ params }) {
};
}),
);
const user = await useModeratorAuth(request);
return {
posts,
userInfo: JSON.parse(JSON.stringify(user)) as {
user: SuyuUser | null;
isModerator: boolean;
},
};
}

View file

@ -4,6 +4,8 @@
import type { PageData } from "./$types";
import { transition } from "$lib/util/animation";
import SvelteMarkdown from "svelte-markdown";
import { goto } from "$app/navigation";
import { reducedMotion } from "$lib/accessibility";
let cardsContainer: HTMLDivElement;
@ -31,12 +33,17 @@
filter: "blur(0px)",
},
],
{
duration: 700,
easing: transition,
delay: x * 40,
fill: "forwards",
},
reducedMotion
? {
duration: 0,
fill: "forwards",
}
: {
duration: 700,
easing: transition,
delay: x * 40,
fill: "forwards",
},
);
});
}
@ -45,42 +52,21 @@
transitionIn();
});
function doTransition(e: MouseEvent) {
const card = (e.target as HTMLDivElement).closest(".card");
if (!card) return;
// get bounds of card
const bounds = card.getBoundingClientRect();
// how much does the card need to scale to become 100vw, 100vh
const scaleX = window.innerWidth / bounds.width;
const scaleY = window.innerHeight / bounds.height;
// how much does the card need to move to become centered
const translateX = window.innerWidth / 2 - bounds.x - bounds.width / 2;
const translateY = window.innerHeight / 2 - bounds.y - bounds.height / 2;
// animate the card to become fullscreen
card.animate(
[
{
transform: "translate(0px, 0px) scale(1)",
opacity: "1",
},
{
transform: `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`,
opacity: "0",
backgroundColor: "transparent",
},
],
{
duration: 850,
easing: "cubic-bezier(0.19, 1, 0.22, 1)",
fill: "forwards",
},
);
}
export let data: PageData;
</script>
<h1 class="mb-8 text-[40px] leading-[1.41] md:text-[60px] md:leading-[1.1]">Blog Posts</h1>
{#if data.userInfo.isModerator}
<!-- <p class="mb-8 text-[24px] leading-[1.41] md:text-[36px] md:leading-[1.1]">
<a href="/blog/new">Create a new post</a>
</p> -->
<!-- wh, wha r -->
<div class="mb-4 ml-1 flex gap-4">
<a href="/blog/new" class="cta-button">Create a new post</a>
<a href="/blog/edit" class="cta-button">Edit a post</a>
</div>
{/if}
<div class="grid max-w-full grid-cols-1 gap-8 lg:grid-cols-2" bind:this={cardsContainer}>
{#each data.posts as post}
<a href={`/blog/${post.slug}`}>

View file

@ -1,84 +1,21 @@
<script lang="ts">
import CodeRenderer from "$components/CodeRenderer.svelte";
import type { PageData } from "./$types";
import SvelteMarkdown from "svelte-markdown";
export let data: PageData;
</script>
<p class="author">{data.props.author} presents...</p>
<div class="md">
<SvelteMarkdown source={data.props.contents} />
<div class="page">
<article class="prose prose-invert mx-auto px-4 lg:prose-xl">
<header>
<address class="mb-4 text-lg text-gray-400">{data.props.author} presents...</address>
</header>
<SvelteMarkdown
source={data.props.contents}
renderers={{
code: CodeRenderer,
}}
/>
</article>
</div>
<style>
.author {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.5);
margin-bottom: -2rem;
}
.md {
margin-top: 8px;
}
.md :global(pre > code) {
background: black;
display: block;
padding: 24px 32px;
border-radius: 2.25rem;
}
.md :global(h1),
.md :global(h2),
.md :global(h3),
.md :global(h4),
.md :global(h5),
.md :global(h6) {
margin-bottom: 1rem;
margin-top: 2rem;
color: white;
}
.md :global(h1) {
font-size: 3rem;
line-height: 1.41;
}
.md :global(h2) {
font-size: 2.5rem;
line-height: 1.5;
}
.md :global(h3) {
font-size: 2rem;
line-height: 1.5;
}
.md :global(h4) {
font-size: 1.5rem;
line-height: 1.5;
}
.md :global(h5) {
font-size: 1.25rem;
line-height: 1.5;
}
.md :global(h6) {
font-size: 1rem;
line-height: 1.5;
}
.md :global(li) {
margin-bottom: 0.5rem;
margin-top: 1rem;
list-style: circle;
margin-left: 2rem;
padding-left: 1rem;
}
.md :global(*) {
font-size: 1.25rem;
color: rgba(255, 255, 255, 0.7);
}
</style>

View file

@ -0,0 +1,44 @@
<script lang="ts">
import SvelteMarkdown from "svelte-markdown";
import CodeRenderer from "$components/CodeRenderer.svelte";
let content = "";
const handleOnSubmit = (event: Event) => {
event.preventDefault();
console.log(content);
};
</script>
<div class="flex h-[calc(100vh-196px)] min-h-[32rem] w-full flex-row gap-8">
<div class="flex h-full w-1/2 flex-1 flex-col gap-4">
<form on:submit={handleOnSubmit} class="contents">
<div class="flex flex-row items-center justify-between">
<h1>Write</h1>
<button type="submit" class="cta-button">Publish</button>
</div>
<textarea
id="write"
bind:value={content}
class="h-full w-full resize-none rounded-2xl !border-none bg-black p-4 font-[Consolas] !outline-none ring ring-[#ffffff11] focus:ring-[#ffffff44]"
></textarea>
</form>
</div>
<div class="flex h-full w-1/2 flex-1 flex-col gap-4">
<div class="flex flex-row items-center justify-between">
<h1>Preview</h1>
<div class="h-12"></div>
</div>
<div
class="prose prose-invert h-full w-full overflow-auto rounded-2xl bg-zinc-950 p-4 ring ring-[#ffffff11] focus:ring-[#ffffff44]"
>
<SvelteMarkdown
renderers={{
code: CodeRenderer,
}}
source={content}
/>
</div>
</div>
</div>

View file

@ -0,0 +1,13 @@
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");
const rooms = RoomManager.getRooms().map((r) => r.toJSON());
return {
user: { ...user },
rooms,
token: apiKey,
};
}

View file

@ -0,0 +1,84 @@
<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 } from "svelte";
const token = getContext<Writable<string>>("token");
if ($token) goto("/account");
let usernameInput = "";
let emailInput = "";
let captchaToken = "";
$: disabled = !usernameInput || !emailInput || !captchaToken;
export let data: PageData;
if (Object.keys(data.user).length !== 0 && browser) goto("/account");
async function signUp() {
const res = await SuyuAPI.users.createAccount({
username: usernameInput,
email: emailInput,
captchaToken,
});
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");
}
async function captchaComplete(event: CustomEvent<any>) {
captchaToken = event.detail.token;
}
</script>
<div
class="align-center relative flex h-[calc(100vh-200px)] flex-col items-center justify-center gap-6 overflow-hidden"
>
<div class="flex h-fit w-full max-w-[500px] flex-col rounded-[2.25rem] bg-black p-10">
<h1 class="text-[60px] md:leading-[1.1]">Sign up</h1>
<div class="mt-4 flex flex-col gap-4">
<p>
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>
<ul class="[&>*]: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>
<input
bind:value={emailInput}
maxlength="128"
class="input"
type="text"
placeholder="Recovery Email"
/>
<input
bind:value={usernameInput}
maxlength="24"
class="input"
type="text"
placeholder="Username"
/>
<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>
</div>
</div>
</div>

View file

@ -1,14 +1,15 @@
/** @type {import('tailwindcss').Config}*/
const config = {
content: [
"./src/**/*.{html,js,svelte,ts}",
"./node_modules/flowbite-svelte-icons/**/*.{html,js,svelte,ts}",],
"./src/**/*.{html,js,svelte,ts}",
"./node_modules/flowbite-svelte-icons/**/*.{html,js,svelte,ts}",
],
theme: {
extend: {},
},
plugins: [],
plugins: [require("@tailwindcss/typography")],
};
module.exports = config;