live member count, remove navbar items
This commit is contained in:
parent
a9bb121354
commit
f3c20e8e2b
20 changed files with 6870 additions and 103 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -10,4 +10,5 @@ vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
ssl
|
ssl
|
||||||
*.sqlite
|
*.sqlite
|
||||||
src/lib/server/secrets/secrets.json
|
src/lib/server/secrets/secrets.json
|
||||||
|
src/assets/blurhash.json
|
6593
package-lock.json
generated
6593
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -40,10 +40,13 @@
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^5.0.3",
|
"vite": "^5.0.3",
|
||||||
"vite-plugin-image-optimizer": "^1.1.7"
|
"vite-imagetools": "^6.2.9",
|
||||||
|
"vite-plugin-image-optimizer": "^1.1.7",
|
||||||
|
"vite-plugin-webp-generator": "^0.0.5"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@benzara/svelte-animated-counter": "^0.0.3",
|
||||||
"@sveltejs/adapter-static": "^3.0.1",
|
"@sveltejs/adapter-static": "^3.0.1",
|
||||||
"@sveltejs/enhanced-img": "^0.1.8",
|
"@sveltejs/enhanced-img": "^0.1.8",
|
||||||
"better-sqlite3": "^9.4.3",
|
"better-sqlite3": "^9.4.3",
|
||||||
|
@ -55,10 +58,11 @@
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"sequelize": "^6.37.1",
|
"sequelize": "^6.37.1",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"svelte-hcaptcha": "^0.1.1",
|
"svelte-countup": "^0.2.6",
|
||||||
"typeorm": "^0.3.20",
|
"typeorm": "^0.3.20",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"verify-hcaptcha": "^1.0.0",
|
"verify-hcaptcha": "^1.0.0",
|
||||||
|
"vite-plugin-blurhash": "^0.2.0",
|
||||||
"vite-plugin-vsharp": "^1.7.3",
|
"vite-plugin-vsharp": "^1.7.3",
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Roc Grotesk";
|
font-family: "Roc Grotesk";
|
||||||
src: url("RocGroteskWideMedium.ttf") format("truetype");
|
src: url(./assets/fonts/RocGroteskWideMedium.ttf) format("truetype");
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
|
@ -64,7 +64,7 @@ h3 {
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
border: 2px solid #46424d;
|
border: 2px solid #46424d;
|
||||||
@apply flex w-fit shrink-0 select-none flex-row items-center justify-center gap-4 rounded-xl py-3 pl-7 pr-5 font-bold transition;
|
@apply flex w-fit shrink-0 select-none flex-row items-center justify-center gap-4 rounded-xl px-6 py-3 font-bold transition;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
|
|
Binary file not shown.
87
src/components/AnimatedCounter.svelte
Normal file
87
src/components/AnimatedCounter.svelte
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
/**
|
||||||
|
* list of values to animate
|
||||||
|
*/
|
||||||
|
export let values = Array.from({ length: 100 }, (_, i) => new String(i).padStart(3, "0"));
|
||||||
|
/**
|
||||||
|
* counter interval between each step in milliseconds, defaults to `1000`
|
||||||
|
*/
|
||||||
|
export let interval = 1000;
|
||||||
|
/**
|
||||||
|
* whether to start the counter immediately or wait for the `interval` to pass, defaults to `false`
|
||||||
|
*/
|
||||||
|
export let startImmediately = false;
|
||||||
|
/**
|
||||||
|
* counter direction, can be `up` or `down` defaults to `down`
|
||||||
|
*/
|
||||||
|
export let direction = "down";
|
||||||
|
/**
|
||||||
|
* whether to loop the counter animation after reaching the end of `values` array , defaults to `true`
|
||||||
|
*/
|
||||||
|
export let loop = true;
|
||||||
|
/**
|
||||||
|
* easing function to use, defaults to `cubic-bezier(1, 0, 0, 1)`
|
||||||
|
*/
|
||||||
|
export let ease = "cubic-bezier(1, 0, 0, 1)";
|
||||||
|
/**
|
||||||
|
* optional initial value to start the counter from
|
||||||
|
*/
|
||||||
|
export let initialValue = undefined;
|
||||||
|
$: contentValues = values.join("\n");
|
||||||
|
$: intervalInMs = `${interval}ms`;
|
||||||
|
let index = direction === "up" ? 0 : values.length - 1;
|
||||||
|
let lastValue = initialValue ?? values[index];
|
||||||
|
onMount(() => {
|
||||||
|
// timer function
|
||||||
|
const start = () => {
|
||||||
|
index = values.indexOf(lastValue) + (direction === "up" ? 1 : -1);
|
||||||
|
// terminate if we looped through all values && loop is false
|
||||||
|
if (!loop && (index === values.length - 1 || index === 0)) {
|
||||||
|
clearInterval(timer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ensure index is in range
|
||||||
|
if (loop && index === values.length) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
if (loop && index === -1) {
|
||||||
|
index = values.length - 1;
|
||||||
|
}
|
||||||
|
lastValue = values[index];
|
||||||
|
};
|
||||||
|
if (startImmediately) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
let timer = setInterval(start, interval);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class="sliding-text {$$props.class}">
|
||||||
|
<span style="--index: {index}; --interval: {intervalInMs}; --ease:{ease}">
|
||||||
|
<span>{contentValues}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.sliding-text {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
line-height: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
.sliding-text > span {
|
||||||
|
height: 1em;
|
||||||
|
display: inline-block;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
.sliding-text > span > span {
|
||||||
|
text-align: center;
|
||||||
|
transition: all var(--interval) var(--ease);
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
white-space: pre;
|
||||||
|
top: calc(var(--index) * -1em);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1 +0,0 @@
|
||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
|
@ -20,7 +20,7 @@ export async function useAuth(request: Request | string): Promise<SuyuUser | nul
|
||||||
}) as IJwtData;
|
}) as IJwtData;
|
||||||
const user = await userRepo.findOne({
|
const user = await userRepo.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: decoded.id,
|
apiKey: decoded.apiKey,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return user;
|
return user;
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
export function generateBgSvg(size: number, color: string) {
|
import hashes from "$assets/blurhash.json";
|
||||||
const svg = `<svg
|
|
||||||
class="outlined-logo"
|
export function getBlurHash(name: string): string {
|
||||||
width="${size}"
|
for (const key in hashes) {
|
||||||
height="${size}"
|
let modifiedKey = key;
|
||||||
viewBox="0 0 96 96"
|
modifiedKey = key.split("\\").at(-1)!;
|
||||||
fill="none"
|
const lastCapitalIndex = modifiedKey.search(/([^ \n])([A-Z][^A-Z]*$)/gm);
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
modifiedKey = modifiedKey.slice(0, lastCapitalIndex + 1);
|
||||||
>
|
if (modifiedKey === name) {
|
||||||
<path
|
return hashes[key];
|
||||||
fill-rule="evenodd"
|
}
|
||||||
clip-rule="evenodd"
|
}
|
||||||
d="M47.9985 2C36.6131 2 27.3713 11.2417 27.3713 22.6287C27.3713 34.0154 36.6131 43.2574 47.9985 43.2574C64.6221 43.2574 78.1148 56.7485 78.1148 73.3723C78.1148 77.301 77.3607 81.0561 75.9891 84.4997C86.9391 76.0868 94 62.8624 94 48.0005C94 22.6085 73.392 2 47.9985 2ZM25.3713 22.6287C25.3713 10.1372 35.5085 0 47.9985 0C74.4965 0 96 21.5039 96 48.0005C96 64.9212 87.228 79.8046 73.9948 88.3518L72.5787 87.0251C74.8311 82.9826 76.1148 78.3278 76.1148 73.3723C76.1148 57.8532 63.5176 45.2574 47.9985 45.2574C35.5085 45.2574 25.3713 35.1199 25.3713 22.6287ZM20.0107 11.5006C9.05987 19.9133 2 33.1381 2 48.0005C2 73.3926 22.6081 94.001 47.9985 94.001C59.3855 94.001 68.6282 84.759 68.6282 73.3723C68.6282 61.9857 59.3855 52.7436 47.9985 52.7436C31.3761 52.7436 17.8852 39.2524 17.8852 22.6287C17.8852 18.6998 18.6393 14.9443 20.0107 11.5006ZM0 48.0005C0 31.0799 8.77019 16.1962 22.0042 7.64919L23.4203 8.97585C21.1686 13.0184 19.8852 17.6732 19.8852 22.6287C19.8852 38.1479 32.4807 50.7436 47.9985 50.7436C60.4899 50.7436 70.6282 60.881 70.6282 73.3723C70.6282 85.8636 60.4899 96.001 47.9985 96.001C21.5034 96.001 0 74.4971 0 48.0005ZM43.0189 14.6674C43.0189 11.9182 45.2503 9.68675 47.9985 9.68675C50.7501 9.68675 52.9791 11.9184 52.9791 14.6674C52.9791 17.4161 50.7502 19.648 47.9985 19.648C45.2503 19.648 43.0189 17.4163 43.0189 14.6674ZM47.9985 11.6867C46.3552 11.6867 45.0189 13.0225 45.0189 14.6674C45.0189 16.3121 46.3552 17.648 47.9985 17.648C49.6448 17.648 50.9791 16.3123 50.9791 14.6674C50.9791 13.0223 49.6449 11.6867 47.9985 11.6867ZM35.0581 22.6287C35.0581 19.8794 37.2893 17.648 40.0384 17.648C42.7877 17.648 45.0189 19.8794 45.0189 22.6287C45.0189 25.3777 42.7877 27.6094 40.0384 27.6094C37.2892 27.6094 35.0581 25.3777 35.0581 22.6287ZM40.0384 19.648C38.3939 19.648 37.0581 20.9839 37.0581 22.6287C37.0581 24.2733 38.394 25.6094 40.0384 25.6094C41.683 25.6094 43.0189 24.2733 43.0189 22.6287C43.0189 20.9839 41.6831 19.648 40.0384 19.648ZM50.9791 22.6287C50.9791 19.8793 53.2109 17.648 55.9596 17.648C58.7113 17.648 60.9402 19.8797 60.9402 22.6287C60.9402 25.3774 58.7113 27.6094 55.9596 27.6094C53.2109 27.6094 50.9791 25.3778 50.9791 22.6287ZM55.9596 19.648C54.3153 19.648 52.9791 20.984 52.9791 22.6287C52.9791 24.2732 54.3154 25.6094 55.9596 25.6094C57.606 25.6094 58.9402 24.2736 58.9402 22.6287C58.9402 20.9836 57.606 19.648 55.9596 19.648ZM43.0189 30.5897C43.0189 27.8407 45.2504 25.6094 47.9985 25.6094C50.7501 25.6094 52.9791 27.841 52.9791 30.5897C52.9791 33.3387 50.7501 35.5704 47.9985 35.5704C45.2503 35.5704 43.0189 33.3389 43.0189 30.5897ZM47.9985 27.6094C46.3551 27.6094 45.0189 28.9451 45.0189 30.5897C45.0189 32.2347 46.3552 33.5704 47.9985 33.5704C49.6449 33.5704 50.9791 32.2349 50.9791 30.5897C50.9791 28.9449 49.6449 27.6094 47.9985 27.6094ZM43.0189 65.411C43.0189 62.6621 45.2503 60.4304 47.9985 60.4304C50.7502 60.4304 52.979 62.6623 52.979 65.411C52.979 68.16 50.7501 70.3917 47.9985 70.3917C45.2503 70.3917 43.0189 68.1602 43.0189 65.411ZM47.9985 62.4304C46.3552 62.4304 45.0189 63.7663 45.0189 65.411C45.0189 67.056 46.3551 68.3917 47.9985 68.3917C49.6449 68.3917 50.979 67.0562 50.979 65.411C50.979 63.7661 49.6448 62.4304 47.9985 62.4304ZM35.058 73.3723C35.058 70.623 37.2892 68.3917 40.0384 68.3917C42.7877 68.3917 45.0189 70.623 45.0189 73.3723C45.0189 76.1217 42.7877 78.353 40.0384 78.353C37.2892 78.353 35.058 76.1216 35.058 73.3723ZM40.0384 70.3917C38.3939 70.3917 37.058 71.7275 37.058 73.3723C37.058 75.0172 38.3939 76.353 40.0384 76.353C41.6831 76.353 43.0189 75.0171 43.0189 73.3723C43.0189 71.7275 41.6831 70.3917 40.0384 70.3917ZM50.979 73.3723C50.979 70.6229 53.2109 68.3917 55.9596 68.3917C58.7113 68.3917 60.9402 70.6233 60.9402 73.3723C60.9402 76.1213 58.7113 78.353 55.9596 78.353C53.2109 78.353 50.979 76.1217 50.979 73.3723ZM55.9596 70.3917C54.3153 70.3917 52.979 71.7276 52.979 73.3723C52.979 75.017 54.3153 76.353 55.9596 76.353C57.606 76.353 58.9402 75.0175 58.9402 73.3723C58.9402 71.7272 57.606 70.3917 55.9596 70.3917ZM43.0189 81.3336C43.0189 78.5844 45.2503 76.353 47.9985 76.353C50.7501 76.353 52.979 78.5846 52.979 81.3336C52.979 84.0823 50.7502 86.3143 47.9985 86.3143C45.2503 86.3143 43.0189 84.0825 43.0189 81.3336ZM47.9985 78.353C46.3551 78.353 45.0189 79.6887 45.0189 81.3336C45.0189 82.9783 46.3552 84.3143 47.9985 84.3143C49.6448 84.3143 50.979 82.9785 50.979 81.3336C50.979 79.6885 49.6449 78.353 47.9985 78.353Z"
|
return "";
|
||||||
fill="${color}"
|
|
||||||
/>
|
|
||||||
</svg>`;
|
|
||||||
return svg;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,17 @@
|
||||||
import { CodeBranchOutline, DiscordSolid, DownloadOutline } from "flowbite-svelte-icons";
|
import { CodeBranchOutline, DiscordSolid, DownloadOutline } from "flowbite-svelte-icons";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import ModalManager from "$components/ModalRoot.svelte";
|
import ModalManager from "$components/ModalRoot.svelte";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { setContext } from "svelte";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
|
||||||
|
const token = writable("");
|
||||||
|
|
||||||
let scrolled = false;
|
let scrolled = false;
|
||||||
let cookies: {
|
let cookies: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
cookies = Object.fromEntries(
|
cookies = Object.fromEntries(
|
||||||
document.cookie.split("; ").map((c) => {
|
document.cookie.split("; ").map((c) => {
|
||||||
|
@ -17,7 +23,11 @@
|
||||||
return [key, value];
|
return [key, value];
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
if (cookies.token) {
|
||||||
|
$token = cookies.token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
setContext("token", token);
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
scrolled = window.scrollY > 0;
|
scrolled = window.scrollY > 0;
|
||||||
|
@ -63,9 +73,9 @@
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-row items-center justify-center gap-2 text-sm font-medium text-[#A6A5A7]"
|
class="flex w-full flex-row items-center justify-center gap-2 text-sm font-medium text-[#A6A5A7]"
|
||||||
>
|
>
|
||||||
<a href="/" class="px-5 py-3 transition hover:text-white">Blog</a>
|
<!-- <a href="/" class="px-5 py-3 transition hover:text-white">Blog</a>
|
||||||
<a href="/" class="px-5 py-3 transition hover:text-white">Docs</a>
|
<a href="/" class="px-5 py-3 transition hover:text-white">Docs</a>
|
||||||
<a href="/" class="px-5 py-3 transition hover:text-white">FAQ</a>
|
<a href="/" class="px-5 py-3 transition hover:text-white">FAQ</a> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-row items-center justify-end gap-4 text-[#A6A5A7]">
|
<div class="flex w-full flex-row items-center justify-end gap-4 text-[#A6A5A7]">
|
||||||
<a
|
<a
|
||||||
|
@ -84,8 +94,8 @@
|
||||||
>
|
>
|
||||||
<DiscordSolid />
|
<DiscordSolid />
|
||||||
</a>
|
</a>
|
||||||
<a href={cookies.token ? "/account" : "/signup"} class="button-sm"
|
<a href={$token ? "/account" : "/signup"} class="button-sm"
|
||||||
>{cookies.token ? "Account" : "Sign up"}</a
|
>{$token ? "Account" : "Sign up"}</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
38
src/routes/+page.server.ts
Normal file
38
src/routes/+page.server.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { building } from "$app/environment";
|
||||||
|
import { DISCORD_USER_TOKEN } from "$env/static/private";
|
||||||
|
|
||||||
|
let memberCount = 0;
|
||||||
|
let roleMembers: { [key: string]: number } = {
|
||||||
|
"1214817156420862012": 50,
|
||||||
|
};
|
||||||
|
async function setMemberCount() {
|
||||||
|
console.log("Fetching member count");
|
||||||
|
const promises = [
|
||||||
|
fetch("https://discord.com/api/v9/invites/suyu?with_counts=true&with_expiration=true"),
|
||||||
|
DISCORD_USER_TOKEN
|
||||||
|
? fetch("https://discord.com/api/v9/guilds/1214371687114477618/roles/member-counts", {
|
||||||
|
headers: {
|
||||||
|
Authorization: DISCORD_USER_TOKEN,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: Promise.resolve({ json: () => roleMembers }),
|
||||||
|
];
|
||||||
|
const [res, roles] = await Promise.all(promises);
|
||||||
|
const jsonPromises = [res.json(), roles.json()];
|
||||||
|
const [resJson, rolesJson] = await Promise.all(jsonPromises);
|
||||||
|
memberCount = resJson.approximate_member_count;
|
||||||
|
if (DISCORD_USER_TOKEN) roleMembers = rolesJson;
|
||||||
|
console.log("Member count:", memberCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!building) {
|
||||||
|
await setMemberCount();
|
||||||
|
setInterval(setMemberCount, 1000 * 60 * 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function load(opts) {
|
||||||
|
return {
|
||||||
|
memberCount,
|
||||||
|
roleMembers,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,18 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// import { XCircleOutline } from "flowbite-svelte-icons";
|
|
||||||
// import { Dialog } from "radix-svelte";
|
|
||||||
// import type { ResolvedProps } from "radix-svelte/internal/helpers";
|
|
||||||
|
|
||||||
import embedImage from "$assets/branding/suyu__Embed-Image.png";
|
import embedImage from "$assets/branding/suyu__Embed-Image.png";
|
||||||
import suyuWindow from "$assets/mockups/suyuwindow.png";
|
|
||||||
import { ModalManager } from "$lib/util/modal";
|
import { ModalManager } from "$lib/util/modal";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
import suyuWindow from "$assets/mockups/suyuwindow.png";
|
||||||
|
|
||||||
// let rootOpen: boolean;
|
export let data: PageData;
|
||||||
// let rootModal: boolean = true;
|
$: memberCount = parseFloat(data.memberCount.toPrecision(2));
|
||||||
// let portalContainer: HTMLElement | string;
|
$: contributors = parseFloat(data.roleMembers["1214817156420862012"].toPrecision(2));
|
||||||
// let contentOpenAutoFocus: boolean = true;
|
|
||||||
// let contentCloseAutoFocus: boolean = true;
|
|
||||||
|
|
||||||
let metadata = {
|
let metadata = {
|
||||||
url: "https://suyu.dev",
|
url: "https://suyu.dev",
|
||||||
title: "suyu - Open-source, non-profit Switch emulator",
|
title: "suyu - Open-source, non-profit Switch emulator",
|
||||||
|
@ -153,15 +147,51 @@
|
||||||
>
|
>
|
||||||
<h1 class="text-[48px] leading-[0.9]">By the numbers</h1>
|
<h1 class="text-[48px] leading-[0.9]">By the numbers</h1>
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
<h2 class="text-[40px] leading-[1.1]">30+</h2>
|
<h2 class="flex items-center gap-1 text-[40px] leading-[1.1]">
|
||||||
|
<!-- <AnimatedCounter
|
||||||
|
values={Array.from({ length: contributors + 1 }, (_, i) => i.toString())}
|
||||||
|
startImmediately={false}
|
||||||
|
direction="up"
|
||||||
|
loop={false}
|
||||||
|
ease="cubic-bezier(0.25, 0.1, 0.25, 1)"
|
||||||
|
initialValue={(contributors - 1).toString()}
|
||||||
|
/>+ -->
|
||||||
|
{contributors}+
|
||||||
|
</h2>
|
||||||
<div class="text-[#A6A5A7]">dedicated contributors</div>
|
<div class="text-[#A6A5A7]">dedicated contributors</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
<h2 class="text-[40px] leading-[1.1]">4000+</h2>
|
<h2 class="flex items-center gap-1 text-[40px] leading-[1.1]">
|
||||||
|
<!-- <AnimatedCounter
|
||||||
|
values={// array from 0 - 4000 with steps of 100
|
||||||
|
Array.from({ length: 41 }, (_, i) => (i * 100).toString())}
|
||||||
|
startImmediately={false}
|
||||||
|
direction="up"
|
||||||
|
loop={false}
|
||||||
|
ease="cubic-bezier(0.25, 0.1, 0.25, 1)"
|
||||||
|
initialValue={"3900"}
|
||||||
|
/>+ -->
|
||||||
|
4000+
|
||||||
|
</h2>
|
||||||
<div class="text-[#A6A5A7]">supported games</div>
|
<div class="text-[#A6A5A7]">supported games</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
<h2 class="text-[40px] leading-[1.1]">14000+</h2>
|
<h2 class="flex items-center gap-1 text-[40px] leading-[1.1]">
|
||||||
|
<!-- <AnimatedCounter
|
||||||
|
values={Array.from(
|
||||||
|
{
|
||||||
|
length: memberCount / 100 + 1,
|
||||||
|
},
|
||||||
|
(_, i) => (i * 100).toString(),
|
||||||
|
)}
|
||||||
|
startImmediately={false}
|
||||||
|
direction="up"
|
||||||
|
loop={false}
|
||||||
|
ease="cubic-bezier(0.25, 0.1, 0.25, 1)"
|
||||||
|
initialValue={(memberCount - 100).toString()}
|
||||||
|
/>+ -->
|
||||||
|
{memberCount}+
|
||||||
|
</h2>
|
||||||
<div class="text-[#A6A5A7]">members on Discord</div>
|
<div class="text-[#A6A5A7]">members on Discord</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -251,4 +281,4 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-spacer-element class="min-h-[400px]"></div>
|
<div data-spacer-element class="min-h-[180px]"></div>
|
||||||
|
|
|
@ -1,42 +1,81 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { browser } from "$app/environment";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import HCaptcha from "$components/HCaptcha.svelte";
|
import { getContext } from "svelte";
|
||||||
import { PUBLIC_SITE_KEY } from "$env/static/public";
|
|
||||||
import { SuyuAPI } from "$lib/client/api";
|
|
||||||
import type { PageData } from "./$types";
|
import type { PageData } from "./$types";
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
import { ModalManager } from "$lib/util/modal";
|
||||||
|
|
||||||
let usernameInput = "";
|
const token = getContext<Writable<string>>("token");
|
||||||
let emailInput = "";
|
|
||||||
let captchaToken = "";
|
let copyText = "Copy token";
|
||||||
$: disabled = !usernameInput || !emailInput || !captchaToken;
|
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
$: b64Token = btoa(data.token || "");
|
||||||
$: {
|
$: {
|
||||||
if (Object.keys(data.user).length === 0) goto("/signup");
|
if (Object.keys(data.user).length === 0 && browser) {
|
||||||
}
|
$token = "";
|
||||||
async function signUp() {
|
document.cookie =
|
||||||
const res = await SuyuAPI.users.createAccount({
|
"token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; samesite=strict";
|
||||||
username: usernameInput,
|
console.log("no user");
|
||||||
email: emailInput,
|
goto("/signup");
|
||||||
captchaToken,
|
|
||||||
});
|
|
||||||
if (!res.success) {
|
|
||||||
// TODO: modal
|
|
||||||
alert(res.error);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// set "token" cookie
|
|
||||||
document.cookie = `token=${res.token}; path=/; max-age=31536000; samesite=strict`;
|
|
||||||
goto("/account");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function captchaComplete(event: CustomEvent<any>) {
|
function copyToken() {
|
||||||
captchaToken = event.detail.token;
|
navigator.clipboard.writeText(b64Token);
|
||||||
|
copyText = "Copied!";
|
||||||
|
setTimeout(() => {
|
||||||
|
copyText = "Copy token";
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="relative h-[calc(100vh-200px)] flex-col gap-6 overflow-hidden">
|
||||||
class="align-center relative flex h-[calc(100vh-200px)] flex-col items-center justify-center 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"
|
||||||
a
|
>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,9 @@ import jwt from "jsonwebtoken";
|
||||||
export async function POST({ request }) {
|
export async function POST({ request }) {
|
||||||
const userKey = `${request.headers.get("x-username")}:${request.headers.get("x-token")}`;
|
const userKey = `${request.headers.get("x-username")}:${request.headers.get("x-token")}`;
|
||||||
const user = await useAuth(userKey);
|
const user = await useAuth(userKey);
|
||||||
const token = jwt.sign({ ...user }, Buffer.from(PRIVATE_KEY), { algorithm: "RS256" });
|
const token = jwt.sign({ ...user, apiKey: userKey }, Buffer.from(PRIVATE_KEY), {
|
||||||
|
algorithm: "RS256",
|
||||||
|
});
|
||||||
return new Response(token, {
|
return new Response(token, {
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "text/html",
|
"content-type": "text/html",
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { userRepo } from "$lib/server/repo/index.js";
|
||||||
import { SuyuUser } from "$lib/server/schema";
|
import { SuyuUser } from "$lib/server/schema";
|
||||||
import { PUBLIC_KEY } from "$lib/server/secrets/secrets.json";
|
import { PUBLIC_KEY } from "$lib/server/secrets/secrets.json";
|
||||||
import { json } from "$lib/server/util";
|
import { json } from "$lib/server/util";
|
||||||
|
import { useAuth } from "$lib/util/api/index.js";
|
||||||
import type { IJwtData } from "$types/auth.js";
|
import type { IJwtData } from "$types/auth.js";
|
||||||
import type { IRoom, LobbyResponse } from "$types/rooms";
|
import type { IRoom, LobbyResponse } from "$types/rooms";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
|
@ -37,11 +38,11 @@ export async function POST({ request, getClientAddress }) {
|
||||||
return new Response(null, { status: 400 });
|
return new Response(null, { status: 400 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const token = request.headers.get("authorization")?.replace("Bearer ", "");
|
const token = request.headers.get("authorization");
|
||||||
if (!token) return new Response(null, { status: 401 });
|
if (!token) return new Response(null, { status: 401 });
|
||||||
// TODO: jwt utils which type and validate automatically
|
// TODO: jwt utils which type and validate automatically
|
||||||
const data = jwt.verify(token, Buffer.from(PUBLIC_KEY), { algorithms: ["RS256"] }) as IJwtData;
|
const user = await useAuth(token);
|
||||||
const user = await userRepo.findOne({ where: { id: data.id } });
|
console.log(user);
|
||||||
if (!user) return new Response(null, { status: 401 });
|
if (!user) return new Response(null, { status: 401 });
|
||||||
const room = RoomManager.createRoom({
|
const room = RoomManager.createRoom({
|
||||||
name: body.name,
|
name: body.name,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { useAuth } from "$lib/util/api/index.js";
|
||||||
|
|
||||||
export async function GET({ request }) {
|
export async function GET({ request }) {
|
||||||
const user = await useAuth(request.headers.get("authorization") || "");
|
const user = await useAuth(request.headers.get("authorization") || "");
|
||||||
|
console.log(user);
|
||||||
if (!user) return new Response(null, { status: 401 });
|
if (!user) return new Response(null, { status: 401 });
|
||||||
return json({
|
return json({
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
|
|
@ -5,6 +5,11 @@
|
||||||
import { PUBLIC_SITE_KEY } from "$env/static/public";
|
import { PUBLIC_SITE_KEY } from "$env/static/public";
|
||||||
import { SuyuAPI } from "$lib/client/api";
|
import { SuyuAPI } from "$lib/client/api";
|
||||||
import type { PageData } from "./$types";
|
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 usernameInput = "";
|
||||||
let emailInput = "";
|
let emailInput = "";
|
||||||
|
@ -28,6 +33,7 @@
|
||||||
}
|
}
|
||||||
// set "token" cookie
|
// set "token" cookie
|
||||||
document.cookie = `token=${res.token}; path=/; max-age=31536000; samesite=strict`;
|
document.cookie = `token=${res.token}; path=/; max-age=31536000; samesite=strict`;
|
||||||
|
$token = res.token;
|
||||||
goto("/account");
|
goto("/account");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +63,9 @@
|
||||||
</p>
|
</p>
|
||||||
<input bind:value={emailInput} class="input" type="text" placeholder="Recovery Email" />
|
<input bind:value={emailInput} class="input" type="text" placeholder="Recovery Email" />
|
||||||
<input bind:value={usernameInput} class="input" type="text" placeholder="Username" />
|
<input bind:value={usernameInput} class="input" type="text" placeholder="Username" />
|
||||||
<HCaptcha on:success={captchaComplete} theme="dark" sitekey={PUBLIC_SITE_KEY} />
|
<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} on:click={signUp} class="cta-button mt-2">Sign up</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
1
src/types/auth.d.ts
vendored
1
src/types/auth.d.ts
vendored
|
@ -5,4 +5,5 @@ export interface IJwtData {
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
roles: string;
|
roles: string;
|
||||||
iat: number;
|
iat: number;
|
||||||
|
apiKey: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import { sveltekit } from "@sveltejs/kit/vite";
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
|
import { imagetools } from "vite-imagetools";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [ViteImageOptimizer(), sveltekit()],
|
plugins: [
|
||||||
|
imagetools({
|
||||||
|
defaultDirectives: new URLSearchParams({
|
||||||
|
format: "webp",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
sveltekit(),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue