diff options
author | mat <27899617+mat-1@users.noreply.github.com> | 2022-12-15 20:19:42 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-15 20:19:42 -0600 |
commit | ed5eedab8f9fc90dadf5c442cf559572d1b35f0c (patch) | |
tree | 01a763fd11810e9970f14f7dae180e95b279de9a /src/routes | |
parent | 89bf3d31e36ad3bdfd45461ee6fb69a4c791f848 (diff) | |
parent | 103689520f51991a1e9a4ca5829fe2f46d1a32c2 (diff) | |
download | skyblock-stats-ed5eedab8f9fc90dadf5c442cf559572d1b35f0c.tar.gz skyblock-stats-ed5eedab8f9fc90dadf5c442cf559572d1b35f0c.tar.bz2 skyblock-stats-ed5eedab8f9fc90dadf5c442cf559572d1b35f0c.zip |
Merge pull request #6 from skyblockstats/sveltekit-v1
Sveltekit v1
Diffstat (limited to 'src/routes')
59 files changed, 1737 insertions, 546 deletions
diff --git a/src/routes/__error.svelte b/src/routes/+error.svelte index 3057f19..83b4407 100644 --- a/src/routes/__error.svelte +++ b/src/routes/+error.svelte @@ -1,22 +1,12 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - - export const load: Load = async ({ error, status }) => { - return { - props: { - error, - status, - }, - } - } -</script> - <script lang="ts"> import Header from '$lib/Header.svelte' import Head from '$lib/Head.svelte' - export let status: number - export let error: Error + import { page } from '$app/stores' + + // sveltekit bug: types of error and status say they can be null but they can't + let error = $page.error! + let status = $page.status! </script> <Head title={error.message} /> @@ -26,7 +16,7 @@ <div> <h1>{status}</h1> <p>{error.message}</p> - <a href="/" sveltekit:prefetch>Home</a> + <a href="/" data-sveltekit-preload-data="hover">Home</a> </div> </main> diff --git a/src/routes/__layout.svelte b/src/routes/+layout.svelte index 74e29b9..74e29b9 100644 --- a/src/routes/__layout.svelte +++ b/src/routes/+layout.svelte diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..4596c1b --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,7 @@ +import type { ServerLoad } from '@sveltejs/kit' + +export const load = (({ locals }) => { + return { + loggedIn: locals.sid !== undefined, + } +}) satisfies ServerLoad diff --git a/src/routes/index.svelte b/src/routes/+page.svelte index 851f70e..3c518fb 100644 --- a/src/routes/index.svelte +++ b/src/routes/+page.svelte @@ -1,15 +1,3 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - - export const load: Load = async ({ params, fetch, session }) => { - return { - props: { - loggedIn: session.sid !== undefined, - }, - } - } -</script> - <script lang="ts"> import Username from '$lib/minecraft/Username.svelte' import SearchUser from '$lib/SearchUser.svelte' @@ -17,10 +5,12 @@ import Head from '$lib/Head.svelte' import Emoji from '$lib/Emoji.svelte' import LoginButton from '$lib/LoginButton.svelte' + import type { PageData } from './$types' export const hydrate = false - export let loggedIn: boolean + export let data: PageData + export let loggedIn: boolean = data.loggedIn </script> <svelte:head> diff --git a/src/routes/[player=username]/+page.ts b/src/routes/[player=username]/+page.ts new file mode 100644 index 0000000..289cd48 --- /dev/null +++ b/src/routes/[player=username]/+page.ts @@ -0,0 +1,6 @@ +import { redirect } from '@sveltejs/kit' +import type { PageLoad } from './$types' + +export const load = (({ params }) => { + throw redirect(303, `/player/${params.player}`) +}) satisfies PageLoad
\ No newline at end of file diff --git a/src/routes/[player].ts b/src/routes/[player].ts deleted file mode 100644 index 3f79368..0000000 --- a/src/routes/[player].ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ParamMatcher, RequestHandler } from '@sveltejs/kit' - -export const match: ParamMatcher = (param) => { - return /^\w{2,16}$/.test(param) -} - -export const get: RequestHandler = async ({ params }) => { - return { - status: 303, - headers: { - location: `/player/${params.player}` - } - } -}
\ No newline at end of file diff --git a/src/routes/auctionprices.svelte b/src/routes/auctionprices/+page.svelte index ac75c86..844af04 100644 --- a/src/routes/auctionprices.svelte +++ b/src/routes/auctionprices/+page.svelte @@ -1,21 +1,3 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - import { fetchApi } from '$lib/api' - - export const load: Load = async ({ params, fetch }) => { - const auctionItemsPromise = fetchApi(`auctionitems`, fetch).then(r => r.json()) - const data = await fetchApi(`auctionprices`, fetch).then(r => r.json()) - const auctionItems = await auctionItemsPromise - - return { - props: { - data, - auctionItems, - }, - } - } -</script> - <script lang="ts"> import Header from '$lib/Header.svelte' import Head from '$lib/Head.svelte' @@ -23,12 +5,15 @@ import type { ItemAuctionsSchema } from '$lib/APITypes' import AuctionPriceScatterplot from '$lib/AuctionPriceScatterplot.svelte' import AuctionPreviewTooltip from '$lib/AuctionPreviewTooltip.svelte' - import { browser } from '$app/env' + import { browser } from '$app/environment' import Item from '$lib/minecraft/Item.svelte' import furfskyReborn from 'skyblock-assets/matchers/furfsky_reborn.json' + import type { PageData } from './$types' + import { fetchApi } from '$lib/api' - export let data: ItemAuctionsSchema[] - export let auctionItems: Record<string, { display: { name: string }; vanillaId?: string }> + export let data: PageData + let auctionPrices: ItemAuctionsSchema[] = data.prices + let auctionItems: Record<string, { display: { name: string }; vanillaId?: string }> = data.items let currentlyPreviewedAuction: PreviewedAuctionData | null = null diff --git a/src/routes/auctionprices/+page.ts b/src/routes/auctionprices/+page.ts new file mode 100644 index 0000000..31af8e3 --- /dev/null +++ b/src/routes/auctionprices/+page.ts @@ -0,0 +1,13 @@ +import type { PageLoad } from './$types' +import { fetchApi } from '$lib/api' + +export const load = (async ({ fetch }) => { + const auctionItemsPromise = fetchApi(`auctionitems`, fetch).then(r => r.json()) + const prices = await fetchApi(`auctionprices`, fetch).then(r => r.json()) + const items = await auctionItemsPromise + + return { + prices, + items, + } +}) satisfies PageLoad diff --git a/src/routes/chat.svelte b/src/routes/chat/+page.svelte index df7aeee..df7aeee 100644 --- a/src/routes/chat.svelte +++ b/src/routes/chat/+page.svelte diff --git a/src/routes/election.svelte b/src/routes/election/+page.svelte index ad869a1..d270425 100644 --- a/src/routes/election.svelte +++ b/src/routes/election/+page.svelte @@ -1,29 +1,12 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - import { fetchApi } from '$lib/api' - - export const load: Load = async ({ params, fetch }) => { - const data = await fetchApi(`election?t=${Math.floor(Date.now() / 1000)}`, fetch).then(r => - r.json() - ) - - return { - props: { - data, - }, - } - } -</script> - <script lang="ts"> import Header from '$lib/Header.svelte' import Head from '$lib/Head.svelte' import { colorCodes, formattingCodeToHtml, millisecondsToTime, skyblockTime } from '$lib/utils' import type { ElectionData } from '$lib/APITypes' import { onDestroy, onMount } from 'svelte' - import MayorSkin from '$lib/MayorSkin.svelte' + import MayorSkin from '../../lib/MayorSkin.svelte' import { invalidate } from '$app/navigation' - import { browser } from '$app/env' + import { browser } from '$app/environment' export let data: ElectionData @@ -73,16 +56,9 @@ <b>Last API update:</b> {millisecondsToTime(currentTime - data.lastUpdated, { smallestUnit: 1, - parts: 1, + parts: 3, })} ago </p> - <p> - <b>Next API update:</b> - {millisecondsToTime(10 * 60 * 1000 - (currentTime - data.lastUpdated), { - smallestUnit: 1, - parts: 1, - })} - </p> </div> {/if} <h1>SkyBlock Mayor Election Status</h1> diff --git a/src/routes/election/+page.ts b/src/routes/election/+page.ts new file mode 100644 index 0000000..e41087d --- /dev/null +++ b/src/routes/election/+page.ts @@ -0,0 +1,8 @@ +import { fetchApi } from '$lib/api' +import type { PageLoad } from './$types' + +export const load = (async ({ fetch }) => { + return await fetchApi(`election?t=${Math.floor(Date.now() / 1000)}`, fetch).then(r => + r.json() + ) +}) satisfies PageLoad diff --git a/src/routes/items.svelte b/src/routes/items/+page.svelte index 82b3ee3..e378bdf 100644 --- a/src/routes/items.svelte +++ b/src/routes/items/+page.svelte @@ -1,18 +1,3 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - import { fetchApi } from '$lib/api' - - export const load: Load = async ({ params, fetch }) => { - const data = await fetchApi(`items`, fetch).then(r => r.json()) - - return { - props: { - data, - }, - } - } -</script> - <script lang="ts"> import Header from '$lib/Header.svelte' import Head from '$lib/Head.svelte' diff --git a/src/routes/items/+page.ts b/src/routes/items/+page.ts new file mode 100644 index 0000000..e5bb249 --- /dev/null +++ b/src/routes/items/+page.ts @@ -0,0 +1,6 @@ +import { fetchApi } from '$lib/api' +import type { PageLoad } from './$types' + +export const load = (async ({ fetch }) => { + return await fetchApi(`items`, fetch).then(r => r.json()) +}) satisfies PageLoad diff --git a/src/routes/leaderboard/+page.ts b/src/routes/leaderboard/+page.ts new file mode 100644 index 0000000..68f69ac --- /dev/null +++ b/src/routes/leaderboard/+page.ts @@ -0,0 +1,6 @@ +import { redirect } from '@sveltejs/kit' +import type { PageLoad } from './$types' + +export const load = (() => { + throw redirect(303, '/leaderboards') +}) satisfies PageLoad diff --git a/src/routes/leaderboard/[name].ts b/src/routes/leaderboard/[name].ts deleted file mode 100644 index 11f3468..0000000 --- a/src/routes/leaderboard/[name].ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { RequestHandler } from '@sveltejs/kit' - -// The route /leaderboard/<name> was moved to /leaderboards/<name> for -// consistency. -export const get: RequestHandler = ({ params }) => { - return { - status: 303, - headers: { - location: `/leaderboards/${params.name}` - } - } -} diff --git a/src/routes/leaderboard/[name]/+server.ts b/src/routes/leaderboard/[name]/+server.ts new file mode 100644 index 0000000..2a1dd38 --- /dev/null +++ b/src/routes/leaderboard/[name]/+server.ts @@ -0,0 +1,7 @@ +import { redirect, type RequestHandler } from '@sveltejs/kit' + +// The route /leaderboard/<name> was moved to /leaderboards/<name> for +// consistency. +export const GET = (({ params }) => { + throw redirect(303, `/leaderboards/${params.name}`) +}) satisfies RequestHandler diff --git a/src/routes/leaderboard/index.ts b/src/routes/leaderboard/index.ts deleted file mode 100644 index e904469..0000000 --- a/src/routes/leaderboard/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { RequestHandler } from '@sveltejs/kit' - - -export const get: RequestHandler = () => { - return { - status: 303, - headers: { - location: '/leaderboards' - } - } -} diff --git a/src/routes/leaderboards/index.svelte b/src/routes/leaderboards/+page.svelte index 50687c8..1d66ef5 100644 --- a/src/routes/leaderboards/index.svelte +++ b/src/routes/leaderboards/+page.svelte @@ -1,18 +1,3 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - import { fetchApi } from '$lib/api' - - export const load: Load = async ({ fetch }) => { - const data = await fetchApi(`leaderboards`, fetch).then(r => r.json()) - - return { - props: { - data, - }, - } - } -</script> - <script lang="ts"> import Header from '$lib/Header.svelte' import Head from '$lib/Head.svelte' diff --git a/src/routes/leaderboards/+page.ts b/src/routes/leaderboards/+page.ts new file mode 100644 index 0000000..f28ac79 --- /dev/null +++ b/src/routes/leaderboards/+page.ts @@ -0,0 +1,6 @@ +import type { PageLoad } from './$types' +import { fetchApi } from '$lib/api' + +export const load = (async ({ fetch }) => { + return await fetchApi(`leaderboards`, fetch).then(r => r.json()) +}) satisfies PageLoad diff --git a/src/routes/leaderboards/[name].svelte b/src/routes/leaderboards/[name]/+page.svelte index 75000a9..cfb7311 100644 --- a/src/routes/leaderboards/[name].svelte +++ b/src/routes/leaderboards/[name]/+page.svelte @@ -1,22 +1,3 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - import { fetchApi } from '$lib/api' - - export const load: Load = async ({ params, fetch }) => { - const dataText = await fetchApi(`leaderboards/${params.name}`, fetch).then(r => r.text()) - - const data = JSON.parse(dataText) - - if (data.list.length === 0) return { status: 404, error: 'Unknown leaderboard' } - - return { - props: { - data, - }, - } as any - } -</script> - <script lang="ts"> import Header from '$lib/Header.svelte' import Head from '$lib/Head.svelte' diff --git a/src/routes/leaderboards/[name]/+page.ts b/src/routes/leaderboards/[name]/+page.ts new file mode 100644 index 0000000..735be61 --- /dev/null +++ b/src/routes/leaderboards/[name]/+page.ts @@ -0,0 +1,13 @@ +import type { PageLoad } from './$types' +import { fetchApi } from '$lib/api' +import { error } from '@sveltejs/kit' + +export const load = (async ({ params, fetch }) => { + const dataText = await fetchApi(`leaderboards/${params.name}`, fetch).then(r => r.text()) + + const data = JSON.parse(dataText) + + if (data.list.length === 0) throw error(404, 'Unknown leaderboard') + + return data +}) satisfies PageLoad diff --git a/src/routes/loggedin.ts b/src/routes/loggedin/+server.ts index 9ae28ef..4772cf6 100644 --- a/src/routes/loggedin.ts +++ b/src/routes/loggedin/+server.ts @@ -1,7 +1,7 @@ import { fetchApi } from '$lib/api' -import type { RequestHandler } from '@sveltejs/kit' +import { redirect, type RequestHandler, } from '@sveltejs/kit' -export const get: RequestHandler = async ({ url }) => { +export const GET = (async ({ url, cookies }) => { const code = url.searchParams.get('code') const redirectUri = `${url.protocol}//${url.host}/loggedin` const response = await fetchApi(`accounts/createsession`, fetch, { @@ -20,18 +20,11 @@ export const get: RequestHandler = async ({ url }) => { }) if (response.ok) { - return { - status: 303, - headers: { - location: '/verify', - 'Set-Cookie': `sid=${response.session_id}; Max-Age=31536000000; Path=/; HttpOnly` - } - } + cookies.set('sid', response.session_id, { + maxAge: 31536000000, + httpOnly: true, + }) + throw redirect(303, '/verify') } - return { - status: 303, - headers: { - location: '/login', - } - } -} + throw redirect(303, '/login') +}) satisfies RequestHandler diff --git a/src/routes/login.ts b/src/routes/login.ts deleted file mode 100644 index 1cd9b23..0000000 --- a/src/routes/login.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { RequestHandler } from '@sveltejs/kit' -import env from '$lib/env' - - -export const get: RequestHandler = async ({ request, platform }) => { - const host = request.headers.get('host') - - const clientId = env(platform).DISCORD_CLIENT_ID - - if (!clientId) - return { - status: 500, - headers: { - 'content-type': 'text/plain', - }, - body: 'DISCORD_CLIENT_ID is not set as an environment variable. Please contact the owner of the website if this was expected to work.', - } - - if (!host) - return { - status: 400, - headers: { - 'content-type': 'text/plain', - }, - body: 'Host header is required.', - } - - const protocol = request.url.startsWith('https') ? 'https' : 'http' - - return { - status: 303, - headers: { - location: `https://discord.com/oauth2/authorize?client_id=${clientId}&redirect_uri=${protocol}://${host}%2Floggedin&response_type=code&scope=identify` - } - } -} diff --git a/src/routes/login/+server.ts b/src/routes/login/+server.ts new file mode 100644 index 0000000..684f11e --- /dev/null +++ b/src/routes/login/+server.ts @@ -0,0 +1,18 @@ +import env from '$lib/env' +import { error, redirect, type RequestHandler } from '@sveltejs/kit' + +export const GET = (async ({ request, platform }) => { + const host = request.headers.get('host') + + const clientId = env(platform).DISCORD_CLIENT_ID + + if (!clientId) + throw error(500, 'DISCORD_CLIENT_ID is not set as an environment variable. Please contact the owner of the website if this was expected to work.') + + if (!host) + throw error(400, 'Host header is required.') + + const protocol = request.url.startsWith('https') ? 'https' : 'http' + + throw redirect(303, `https://discord.com/oauth2/authorize?client_id=${clientId}&redirect_uri=${protocol}://${host}%2Floggedin&response_type=code&scope=identify`) +}) satisfies RequestHandler diff --git a/src/routes/logout.ts b/src/routes/logout/+server.ts index 25be86c..2e0f40d 100644 --- a/src/routes/logout.ts +++ b/src/routes/logout/+server.ts @@ -1,7 +1,7 @@ import { fetchApi } from '$lib/api' -import type { RequestHandler } from '@sveltejs/kit' +import { redirect, type RequestHandler } from '@sveltejs/kit' -export const get: RequestHandler = async ({ locals, url }) => { +export const GET = (async ({ url, cookies, locals }) => { // if the sid is wrong, nothing to do if (url.searchParams.has('sid') && url.searchParams.get('sid') === locals.sid) { await fetchApi(`accounts/session`, fetch, { @@ -17,12 +17,7 @@ export const get: RequestHandler = async ({ locals, url }) => { throw new Error(res.statusText) }) } - return { - status: 303, - headers: { - location: '/', - 'Set-Cookie': 'sid=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/;' - } - } -} + cookies.delete('sid') + throw redirect(303, '/') +}) as RequestHandler diff --git a/src/routes/player/+page.server.ts b/src/routes/player/+page.server.ts new file mode 100644 index 0000000..eafae71 --- /dev/null +++ b/src/routes/player/+page.server.ts @@ -0,0 +1,12 @@ +import { redirect } from '@sveltejs/kit' +import type { Actions } from './$types' + +export const actions: Actions = { + default: async ({ request }) => { + const form = await request.formData() + + const player = form.get('user-search') + + throw redirect(303, `/player/${player}`) + } +}
\ No newline at end of file diff --git a/src/routes/player/[player]/index.svelte b/src/routes/player/[player]/+page.svelte index 5a51816..6711864 100644 --- a/src/routes/player/[player]/index.svelte +++ b/src/routes/player/[player]/+page.svelte @@ -1,34 +1,3 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - import { fetchApi } from '$lib/api' - - export const load: Load = async ({ params, fetch }) => { - const player: string = params.player - - const data = await fetchApi(`player/${player}?customization=true`, fetch).then(r => r.json()) - - if (!data.player) { - return { - status: 404, - error: 'Unknown player', - } - } - - if (data.player.username !== player) { - return { - redirect: `/player/${data.player.username}`, - status: 302, - } as any - } - - return { - props: { - data, - }, - } - } -</script> - <script lang="ts"> import type { CleanPlayer, CleanProfile, CleanUser } from '$lib/APITypes' import BackgroundImage from '$lib/BackgroundImage.svelte' @@ -40,9 +9,9 @@ import { MODE_EMOJIS, DEFAULT_MODE_EMOJI } from '$lib/profile' import Tooltip from '$lib/Tooltip.svelte' import { cleanId } from '$lib/utils' - import { navigating } from '$app/stores' + import type { PageData } from './$types' - export let data: CleanUser & { player: CleanPlayer } + export let data: PageData let activeProfile: CleanProfile | null = null let activeProfileLastSave: number = 0 @@ -97,7 +66,7 @@ <a class="profile-name" href="/player/{data.player?.username}/{profile.name}" - sveltekit:prefetch + data-sveltekit-preload-data="hover" > {profile.name} </a> diff --git a/src/routes/player/[player]/+page.ts b/src/routes/player/[player]/+page.ts new file mode 100644 index 0000000..023c3ff --- /dev/null +++ b/src/routes/player/[player]/+page.ts @@ -0,0 +1,19 @@ +import type { PageLoad } from './$types' +import { fetchApi } from '$lib/api' +import { error, redirect } from '@sveltejs/kit' + +export const load = (async ({ params, fetch }) => { + const player: string = params.player! + + const data = await fetchApi(`player/${player}?customization=true`, fetch).then(r => r.json()) + + if (!data.player) { + throw error(404, 'Unknown player') + } + + if (data.player.username !== player) { + throw redirect(302, `/player/${data.player.username}`) + } + + return data +}) satisfies PageLoad diff --git a/src/routes/player/[player]/[profile].svelte b/src/routes/player/[player]/[profile]/+page.svelte index e686164..8a3adbb 100644 --- a/src/routes/player/[player]/[profile].svelte +++ b/src/routes/player/[player]/[profile]/+page.svelte @@ -1,92 +1,41 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - import { loadPack } from '$lib/packs' - import { fetchApi } from '$lib/api' - - export const load: Load = async ({ params, fetch }) => { - const player: string = params.player - const profile: string = params.profile - const data: CleanMemberProfile = await fetchApi( - `player/${player}/${profile}?customization=true`, - fetch - ).then(async r => { - const text = await r.text() - try { - return JSON.parse(text) - } catch (e) { - throw new Error(`Invalid JSON: ${text}`) - } - }) - - if (!data.member) { - return { - status: 404, - error: 'Unknown profile', - } - } - - if (data.member.username !== player) { - return { - redirect: `/player/${data.member.username}/${data.profile.name}`, - status: 302, - } as any - } - if (!data.member.left && data.profile.name !== profile) { - return { - redirect: `/player/${data.member.username}/${data.profile.name}`, - status: 302, - } as any - } - - const packName = params.pack ?? data?.customization?.pack - - let pack = await loadPack(packName) - - return { - props: { - data, - pack, - }, - } - } -</script> - <script lang="ts"> import { inventoryIconMap, skyblockItemToUrl } from '$lib/minecraft/inventory' - import FarmingContests from '$lib/sections/FarmingContests.svelte' - import Leaderboards from '$lib/sections/Leaderboards.svelte' - import Inventories from '$lib/sections/Inventories.svelte' - import Collections from '$lib/sections/Collections.svelte' + import FarmingContests from './sections/FarmingContests.svelte' + import Leaderboards from './sections/Leaderboards.svelte' + import Achievements from './sections/Achievements.svelte' + import Inventories from './sections/Inventories.svelte' + import Collections from './sections/Collections.svelte' import { chooseDefaultBackground } from '$lib/backgrounds' import BackgroundImage from '$lib/BackgroundImage.svelte' import type { CleanMemberProfile } from '$lib/APITypes' import Username from '$lib/minecraft/Username.svelte' - import StatList from '$lib/sections/StatList.svelte' - import Infobox from '$lib/sections/Infobox.svelte' - import Minions from '$lib/sections/Minions.svelte' - import Slayers from '$lib/sections/Slayers.svelte' + import StatList from './sections/StatList.svelte' + import Auctions from './sections/Auctions.svelte' + import Infobox from './sections/Infobox.svelte' + import Minions from './sections/Minions.svelte' + import Essence from './sections/Essence.svelte' + import Slayers from './sections/Slayers.svelte' import type { MatcherFile } from 'skyblock-assets' - import Claimed from '$lib/sections/Claimed.svelte' + import Claimed from './sections/Claimed.svelte' import Collapsible from '$lib/Collapsible.svelte' - import Skills from '$lib/sections/Skills.svelte' + import Skills from './sections/Skills.svelte' import { generateInfobox } from '$lib/profile' - import Zones from '$lib/sections/Zones.svelte' - import Armor from '$lib/sections/Armor.svelte' - import Harp from '$lib/sections/Harp.svelte' - import Pets from '$lib/sections/Pets.svelte' - import Coop from '$lib/sections/Coop.svelte' - import Bank from '$lib/sections/Bank.svelte' + import Zones from './sections/Zones.svelte' + import Armor from './sections/Armor.svelte' + import Harp from './sections/Harp.svelte' + import Pets from './sections/Pets.svelte' + import Coop from './sections/Coop.svelte' + import Bank from './sections/Bank.svelte' + import type { PageData } from './$types' import Header from '$lib/Header.svelte' import Emoji from '$lib/Emoji.svelte' import { cleanId } from '$lib/utils' import Head from '$lib/Head.svelte' import Toc from '$lib/Toc.svelte' - import Achievements from '$lib/sections/Achievements.svelte' - import Essence from '$lib/sections/Essence.svelte' - import Auctions from '$lib/sections/Auctions.svelte' - export let data: CleanMemberProfile - export let pack: MatcherFile + export let data: PageData & CleanMemberProfile + + export let pack: MatcherFile = data.pack let categories: string[] = [] function setCategories() { diff --git a/src/routes/player/[player]/[profile]/+page.ts b/src/routes/player/[player]/[profile]/+page.ts new file mode 100644 index 0000000..34a4e2a --- /dev/null +++ b/src/routes/player/[player]/[profile]/+page.ts @@ -0,0 +1,40 @@ +import type { CleanMemberProfile } from '$lib/APITypes' +import type { PageLoad } from './$types' +import { loadPack } from '$lib/packs' +import { fetchApi } from '$lib/api' +import { error, redirect } from '@sveltejs/kit' + +export const load = (async ({ params, fetch, url }) => { + const player: string = params.player + const profile: string = params.profile + const data: CleanMemberProfile = await fetchApi( + `player/${player}/${profile}?customization=true`, + fetch + ).then(async r => { + const text = await r.text() + try { + return JSON.parse(text) + } catch (e) { + throw new Error(`Invalid JSON: ${text}`) + } + }) + + if (!data.member) { + throw error(404, 'Unknown profile') + } + + if (data.member.username !== player) { + throw redirect(302, `/player/${data.member.username}/${data.profile.name}`) + } + if (!data.member.left && data.profile.name !== profile) { + throw redirect(302, `/player/${data.member.username}/${data.profile.name}`) + } + + const packName = url.searchParams.get('pack') ?? data?.customization?.pack + const pack = await loadPack(packName) + + return { + ...data, + pack, + } +}) satisfies PageLoad diff --git a/src/routes/player/[player]/[profile]/sections/AccessoryBagUpgrades.svelte b/src/routes/player/[player]/[profile]/sections/AccessoryBagUpgrades.svelte new file mode 100644 index 0000000..74530d1 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/AccessoryBagUpgrades.svelte @@ -0,0 +1,100 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import Emoji from '$lib/Emoji.svelte' + import ListItemWithIcon from '$lib/ListItemWithIcon.svelte' + import { cleanId, skyblockTime } from '$lib/utils' + + export let data: CleanMemberProfile + + $: bagData = data.member.accessoryBagUpgrades +</script> + +<span class="accessory-bag-upgrades"> + <h3>Upgrades</h3> + <div class="accessory-bag-info-text"> + <p>Purchased: <b>{bagData.upgrades.purchased}</b></p> + <p>Coins spent: <b>{bagData.upgrades.coinsSpent.toLocaleString()}</b></p> + <p>Extra slots: <b>{bagData.upgrades.extraSlots}</b></p> + </div> + + <h3>Powers</h3> + {#if bagData.powers.selected} + <p class="accessory-bag-info-text"> + Selected: <b>{cleanId(bagData.powers.selected)}</b> + </p> + {/if} + <ul> + {#each bagData.powers.list as power} + <li> + {#if bagData.powers.selected === power} + <b>{cleanId(power)}</b> + {:else} + {cleanId(power)} + {/if} + </li> + {/each} + </ul> + + <div class="tuning-templates"> + {#each bagData.tuningTemplates as template, template_index} + <div class="tuning-template"> + <h3>Template #{template_index + 1}</h3> + <div class="accessory-bag-info-text"> + {#each Object.entries(template) as [statName, statValue]} + <p>{cleanId(statName)}: <b>{statValue}</b></p> + {/each} + </div> + </div> + {/each} + </div> +</span> + +<style> + p, + ul { + margin: 0; + } + ul { + padding-left: 1.5em; + } + h3 { + margin: 0.5em 0 0 0; + } + + .accessory-bag-info-text { + color: var(--theme-darker-text); + } + .accessory-bag-info-text b { + color: var(--theme-main-text); + } + + .tuning-templates { + display: flex; + flex-wrap: wrap; + max-width: 40rem; + column-gap: 0.5rem; + row-gap: 0.5rem; + margin-top: 1rem; + } + .tuning-template { + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.1); + padding: 0.75em; + border-radius: 1em; + } + .tuning-template h3 { + margin: 0; + } + + .accessory-bag-upgrades { + /* width: 5rem; */ + } + @media only screen and (min-width: 1160px) { + .accessory-bag-upgrades { + display: inline-grid; + position: relative; + top: -1.5em; + left: 0.5em; + } + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Achievements.svelte b/src/routes/player/[player]/[profile]/sections/Achievements.svelte new file mode 100644 index 0000000..92ba468 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Achievements.svelte @@ -0,0 +1,87 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import Tooltip from '$lib/Tooltip.svelte' + export let data: CleanMemberProfile +</script> + +{#if data.member.achievements} + <h3> + Tiered + <span class="achievement-count"> + ({data.member.achievements.tiered.filter(a => a.amount).length}/{data.member.achievements + .tiered.length}) + </span> + </h3> + <ul> + {#each data.member.achievements.tiered as achievement} + <li class="achievement"> + <Tooltip> + <span slot="tooltip"> + {achievement.description} + </span> + + <span class:achievement-locked={achievement.amount === 0}> + {achievement.name}: {#if achievement.next} + <b class="achievement-amount">{achievement.amount}</b>/{achievement.next} + {:else} + <span class="achievement-amount achievement-amount-maxed">{achievement.amount}</span> + {/if} + </span> + </Tooltip> + </li> + {/each} + </ul> + + <h3> + Challenge + <span class="achievement-count"> + ({data.member.achievements.challenge.filter(a => a.unlocked).length}/{data.member.achievements + .challenge.length}) + </span> + </h3> + <ul> + {#each data.member.achievements.challenge as achievement} + <li class="achievement"> + <Tooltip> + <span slot="tooltip"> + {achievement.description} + </span> + + {#if achievement.unlocked} + <span> + {achievement.name} + </span> + {:else} + <span class="achievement-locked"> + {achievement.name} + </span> + {/if} + </Tooltip> + </li> + {/each} + </ul> +{/if} + +<style> + ul { + margin: 0; + padding-left: 1em; + } + + .achievement-locked { + opacity: 0.5; + } + + .achievement-count { + color: var(--theme-darker-text); + font-weight: normal; + } + + .achievement-amount { + opacity: 0.9; + } + .achievement-amount-maxed { + color: #0e0; + opacity: 1; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Armor.svelte b/src/routes/player/[player]/[profile]/sections/Armor.svelte new file mode 100644 index 0000000..285a898 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Armor.svelte @@ -0,0 +1,14 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import Inventory from '$lib/minecraft/Inventory.svelte' + import type { MatcherFile } from 'skyblock-assets' + + export let data: CleanMemberProfile + export let pack: MatcherFile +</script> + +{#if data.member.inventories} + <span> + <Inventory items={data.member.inventories.armor} name="armor" groupLimit={1} {pack} /> + </span> +{/if} diff --git a/src/routes/player/[player]/[profile]/sections/Auctions.svelte b/src/routes/player/[player]/[profile]/sections/Auctions.svelte new file mode 100644 index 0000000..50d47b6 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Auctions.svelte @@ -0,0 +1,109 @@ +<!-- + @component + + A list of the player's past auctions, and their auction stats. +--> +<script lang="ts"> + import { cleanId, millisecondsToTime } from '$lib/utils' + import type { CleanMemberProfile, StatItem } from '$lib/APITypes' + import { fetchApi } from '$lib/api' + import type { MatcherFile } from 'skyblock-assets' + import Auction from '$lib/Auction.svelte' + + export let data: CleanMemberProfile + export let stats: StatItem[] + export let pack: MatcherFile + + let onlyThisProfile = true + + let auctions: any[] = [] + let loading = true + + let page = 0 + let totalPages: number | undefined = undefined + + async function updateAuctions() { + loading = true + const thisPage = page + page += 1 + const auctionsResponse = await fetchApi( + `playerauctions/${data.member.uuid}?page=${thisPage}`, + fetch + ).then(r => r.json()) + loading = false + auctions = [...auctions, ...auctionsResponse.auctions] + totalPages = auctionsResponse.pages + } + + updateAuctions() +</script> + +<div class="auction-stats-and-list-container"> + <ul> + {#each stats.sort((a, b) => b.value - a.value) as stat} + <li> + <span class="stat-name">{cleanId(stat.categorizedName)}:</span> + <span class="stat-value"> + {#if stat.unit === 'time'} + {millisecondsToTime(stat.value)} + {:else} + {stat.value.toLocaleString()} + {/if} + </span> + </li> + {/each} + </ul> + + <div class="player-auctions-list-container"> + {#if loading || auctions.length > 0} + <h3>Auctions sold</h3> + {/if} + {#if auctions.length > 0} + <div class="player-auctions-list"> + {#each auctions as auction} + {#if !onlyThisProfile || auction.sellerProfileUuid == data.profile.uuid} + <Auction {auction} {pack} /> + {/if} + {/each} + </div> + {#if !loading && page != totalPages} + <button on:click={updateAuctions}>Show more</button> + {/if} + {/if} + {#if loading} + Loading... + {/if} + </div> +</div> + +<style> + li { + position: relative; + } + ul { + padding-left: 1em; + margin-top: 0.5em; + width: max-content; + } + .auction-stats-and-list-container { + display: grid; + grid-template-columns: 1fr auto; + } + + @media (max-width: 600px) { + .auction-stats-and-list-container { + grid-template-columns: 1fr; + } + } + + .player-auctions-list { + display: flex; + flex-wrap: wrap; + column-gap: 0.5rem; + row-gap: 0.5rem; + } + .player-auctions-list-container { + margin-top: 0.5em; + margin-left: 0.5em; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Bank.svelte b/src/routes/player/[player]/[profile]/sections/Bank.svelte new file mode 100644 index 0000000..9bd2b8f --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Bank.svelte @@ -0,0 +1,89 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import Tooltip from '$lib/Tooltip.svelte' + import ConditionalLink from '$lib/ConditionalLink.svelte' + import { + colorCodeCharacter, + formattingCodeToHtml, + millisecondsToTime, + removeFormattingCode, + } from '$lib/utils' + + export let data: CleanMemberProfile +</script> + +{#if data.profile.bank} + <div class="bank-main-current-balance"> + <p> + Current bank balance: + <span class="bank-main-current-balance-value"> + <b>{data.profile.bank.balance?.toLocaleString()}</b> coins + </span> + </p> + <p> + Purse: + <span class="bank-main-current-balance-value"> + <b>{data.member.purse.toLocaleString()}</b> coins + </span> + </p> + </div> + {#each data.profile.bank.history as transaction} + <div> + <span class="transaction-player"> + <ConditionalLink + href="/player/{removeFormattingCode(transaction.name)}" + isWrapped={transaction.name.startsWith(colorCodeCharacter)} + > + {@html formattingCodeToHtml(transaction.name)} + </ConditionalLink> + </span> + <Tooltip> + <span slot="tooltip"> + New balance: <b>{transaction.total.toLocaleString()}</b> + </span> + <span + class:difference-positive={transaction.change > 0} + class:difference-negative={transaction.change < 0} + > + {transaction.change > 0 + ? '+' + transaction.change.toLocaleString() + : transaction.change.toLocaleString()} + </span> + </Tooltip> + + <span class="transaction-timeago"> + {millisecondsToTime(Date.now() - transaction.timestamp)} ago + </span> + </div> + {/each} +{/if} + +<style> + .difference-positive { + color: #0f0; + } + .difference-negative { + color: red; + } + + .transaction-timeago { + color: var(--theme-darker-text); + } + + .transaction-player { + font-family: Minecraft, 'Atkinson Hyperlegible', sans-serif; + font-size: 0.8em; + } + + .bank-main-current-balance { + margin: 0.5em 0; + color: var(--theme-darker-text); + } + + .bank-main-current-balance-value { + color: var(--theme-main-text); + } + p { + margin: 0; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Claimed.svelte b/src/routes/player/[player]/[profile]/sections/Claimed.svelte new file mode 100644 index 0000000..8f44796 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Claimed.svelte @@ -0,0 +1,33 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import { cleanId, millisecondsToTime, toTitleCase } from '$lib/utils' + + export let data: CleanMemberProfile +</script> + +{#if data.member.claimed && data.member.claimed.length > 0} + <ul> + {#each data.member.claimed as claimed} + <li> + <b class="claimed-item-name">{toTitleCase(cleanId(claimed.id))}</b> + <span class="claimed-item-timestamp"> + {millisecondsToTime(Date.now() - claimed.timestamp)} ago + </span> + </li> + {/each} + </ul> +{/if} + +<style> + p { + margin: 0; + } + + ul { + margin: 0.5em 0; + } + + .claimed-item-timestamp { + color: var(--theme-darker-text); + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Collections.svelte b/src/routes/player/[player]/[profile]/sections/Collections.svelte new file mode 100644 index 0000000..4baa660 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Collections.svelte @@ -0,0 +1,62 @@ +<script lang="ts"> + import type { CleanMemberProfile, Collection } from '$lib/APITypes' + import { skyblockItemToUrl } from '$lib/minecraft/inventory' + import ListItemWithIcon from '$lib/ListItemWithIcon.svelte' + import type { MatcherFile } from 'skyblock-assets' + import Tooltip from '$lib/Tooltip.svelte' + import { cleanId } from '$lib/utils' + + export let data: CleanMemberProfile + export let pack: MatcherFile + + const categories: Record<string, Collection[]> = {} + if (data.member.collections) + for (const collection of data.member.collections) { + if (!categories[collection.category]) categories[collection.category] = [] + categories[collection.category].push(collection) + } +</script> + +{#if data.member.collections} + {#each Object.keys(categories).sort() as categoryName} + {@const collections = categories[categoryName]} + <h3>{cleanId(categoryName)}</h3> + <ul> + {#each collections as collection} + <ListItemWithIcon + src={skyblockItemToUrl(collection.name, pack)} + alt={cleanId(collection.name)} + > + <Tooltip> + <span slot="tooltip"> + Amount: {collection.amount.toLocaleString()} + </span> + {cleanId(collection.name)} + <span class="coll-level">{collection.level}</span> + </Tooltip> + </ListItemWithIcon> + {/each} + </ul> + {/each} +{/if} + +<style> + ul { + margin: 0; + display: flex; + flex-wrap: wrap; + width: fit-content; + /* this ensures there's at most 2 lines */ + max-width: 30em; + } + + ul > :global(li) { + width: 12em; + height: 1.5em; + text-overflow: ellipsis; + } + + h3 { + margin: 0.5em 0 0.5em 0.5em; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Coop.svelte b/src/routes/player/[player]/[profile]/sections/Coop.svelte new file mode 100644 index 0000000..ff858f1 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Coop.svelte @@ -0,0 +1,79 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import Username from '$lib/minecraft/Username.svelte' + import { millisecondsToTime } from '$lib/utils' + export let data: CleanMemberProfile + + $: isProfileCreator = data.member.coopInvitation?.invitedBy?.uuid == data.member.uuid +</script> + +{#if data.member.coopInvitation} + <div class="info-text primary-info-text"> + {#if isProfileCreator} + <p><b class="info-text-value">Created co-op</b></p> + {:else} + <p> + Invited by {#if data.member.coopInvitation.invitedBy} + <Username player={data.member.coopInvitation.invitedBy} prefix /> + {:else} + <b>Unknown player</b> + {/if} + </p> + {/if} + <p> + {isProfileCreator ? 'Began creation' : 'Invited'}: + <span class="info-text-value coop-invited-timeago"> + <b>{millisecondsToTime(Date.now() - data.member.coopInvitation.invitedTimestamp)}</b> ago + </span> + </p> + {#if data.member.coopInvitation.acceptedTimestamp} + <p> + {isProfileCreator ? 'Finished creation' : 'Accepted invite'}: + <span class="info-text-value coop-accepted-invite-after"> + after <b> + {millisecondsToTime( + data.member.coopInvitation.acceptedTimestamp - + data.member.coopInvitation.invitedTimestamp + )} + </b> + </span> + </p> + {/if} + </div> + <h3>Members</h3> + {#each data.profile.members.filter(m => !m.left) as player} + <span class="member"> + <Username {player} headType="2d" hyperlinkToProfile /> + </span> + {/each} + {#if data.profile.members.filter(m => m.left).length > 0} + <h3 class="former-members-title">Former members</h3> + {#each data.profile.members.filter(m => m.left) as player} + <span class="member"> + <Username {player} headType="2d" hyperlinkToProfile={data.profile.uuid} /> + </span> + {/each} + {/if} +{/if} + +<style> + p { + margin: 0; + } + .primary-info-text { + margin: 0.5em 0; + } + .info-text { + color: var(--theme-darker-text); + } + .info-text .info-text-value { + color: var(--theme-main-text); + } + + .member { + display: block; + } + .former-members-title { + margin-top: 0.5rem; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Essence.svelte b/src/routes/player/[player]/[profile]/sections/Essence.svelte new file mode 100644 index 0000000..244dbcd --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Essence.svelte @@ -0,0 +1,27 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import { cleanId, toTitleCase } from '$lib/utils' + + export let data: CleanMemberProfile +</script> + +{#if data.member.essence.types.length > 0} + <ul> + {#each data.member.essence.types as essenceType} + <li> + {toTitleCase(cleanId(essenceType.id))}: + <b class="essence-type-amount">{essenceType.amount.toLocaleString()}</b> + </li> + {/each} + </ul> +{/if} + +<style> + p { + margin: 0; + } + + ul { + margin: 0.5em 0; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/FarmingContests.svelte b/src/routes/player/[player]/[profile]/sections/FarmingContests.svelte new file mode 100644 index 0000000..8eef53d --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/FarmingContests.svelte @@ -0,0 +1,62 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import Emoji from '$lib/Emoji.svelte' + import ListItemWithIcon from '$lib/ListItemWithIcon.svelte' + import { skyblockItemToUrl } from '$lib/minecraft/inventory' + import { skyblockTime } from '$lib/utils' + + export let data: CleanMemberProfile + + let cachedItemUrls: Record<string, string> = {} + function cachedSkyblockItemToUrl(item: string) { + if (!cachedItemUrls[item]) cachedItemUrls[item] = skyblockItemToUrl(item) + return cachedItemUrls[item] + } +</script> + +<div class="info-text primary-info-text"> + <p>Talked to Jacob: <Emoji value={data.member.farmingContests.talkedToJacob ? '✅' : '❌'} /></p> +</div> +<div class="farming-contests-list"> + {#each data.member.farmingContests.list as farmingContest} + <div class="farming-contest"> + <p class="farming-contest-date"> + {new Date( + skyblockTime(farmingContest.year, farmingContest.month, farmingContest.day) + ).toUTCString()} + </p> + <ul> + {#each farmingContest.crops as crop} + <ListItemWithIcon src={cachedSkyblockItemToUrl(crop.item)}> + <b>{crop.amount.toLocaleString()}</b> collected + {#if crop.position} + <span class="farming-contest-item-placement"> + (#{crop.position}/{crop.participants}) + </span> + {/if} + </ListItemWithIcon> + {/each} + </ul> + </div> + {/each} +</div> + +<style> + p, + ul { + margin: 0; + } + + .primary-info-text { + margin: 0.5em 0; + } + + .info-text { + color: var(--theme-darker-text); + } + + .farming-contest-item-placement, + .farming-contest-date { + color: var(--theme-darker-text); + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Harp.svelte b/src/routes/player/[player]/[profile]/sections/Harp.svelte new file mode 100644 index 0000000..6a5c9cc --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Harp.svelte @@ -0,0 +1,72 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import Emoji from '$lib/Emoji.svelte' + import { cleanId, millisecondsToTime, toTitleCase } from '$lib/utils' + + export let data: CleanMemberProfile +</script> + +<div class="info-text primary-info-text"> + <p>Claimed Melody's hair: <Emoji value={data.member.harp.claimedMelodysHair ? '✅' : '❌'} /></p> + {#if data.member.harp.selected} + <p> + Selected song: + <b class="info-text-value">{toTitleCase(cleanId(data.member.harp.selected.id))}</b> + <span class="harp-selection-timeago"> + {millisecondsToTime(Date.now() - data.member.harp.selected.timestamp)} ago + </span> + </p> + {/if} +</div> +<div class="harp-songs-list"> + {#each data.member.harp.songs as song} + <div class="harp-song" class:selected-harp-song={song.id === data.member.harp.selected?.id}> + <h3>{toTitleCase(cleanId(song.id))}</h3> + <div class="info-text"> + {#if song.completions} + <p>Completions: <b class="info-text-value">{song.completions}</b></p> + {/if} + {#if song.perfectCompletions} + <p>Perfect completions: <b class="info-text-value">{song.perfectCompletions}</b></p> + {:else} + <p>Progress: <b class="info-text-value">{Math.floor(song.progress * 100)}%</b></p> + {/if} + </div> + </div> + {/each} +</div> + +<style> + p { + margin: 0; + } + + .primary-info-text { + margin: 0.5em 0; + } + + .info-text { + color: var(--theme-darker-text); + } + .info-text .info-text-value { + color: var(--theme-main-text); + } + + .harp-songs-list { + display: flex; + flex-wrap: wrap; + max-width: 40rem; + column-gap: 0.5rem; + row-gap: 0.5rem; + } + .harp-song { + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.1); + padding: 0.75em; + border-radius: 1em; + width: 12em; + } + .selected-harp-song { + border: 1px solid rgba(255, 255, 255, 0.2); + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Infobox.svelte b/src/routes/player/[player]/[profile]/sections/Infobox.svelte new file mode 100644 index 0000000..f8d2889 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Infobox.svelte @@ -0,0 +1,83 @@ +<script lang="ts"> + import { generateInfobox } from '$lib/profile' + import Username from '$lib/minecraft/Username.svelte' + import Emoji from '$lib/Emoji.svelte' + + export let data +</script> + +<div id="infobox-container"> + <div id="infobox"> + <h2> + <Username player={data.member} prefix /> ({data.member.left + ? 'Removed' + : data.member.profileName}) + </h2> + {#each generateInfobox(data) as item} + <!-- hack so fairy souls is clickable to get to the leaderboards --> + {#if item.includes('Fairy souls')} + <a href="/leaderboards/fairy_souls" class="fairy-souls-leaderboard" + ><p><Emoji value={item} /></p></a + > + {:else} + <p><Emoji value={item} /></p> + {/if} + {/each} + </div> + <div id="infobox-extra"> + <p>Player UUID:</p> + <b><code>{data.member.uuid}</code></b> + <p>Profile UUID:</p> + <b><code>{data.profile.uuid}</code></b> + </div> +</div> + +<style> + #infobox-container { + float: right; + max-width: 95%; + width: 20em; + } + #infobox { + background-color: rgba(20, 20, 20, 0.4); + padding: 1em; + margin-top: 2em; + border-radius: 0.5em; + box-shadow: 0 0 1em #000; + } + #infobox-extra { + opacity: 0.5; + margin-top: 0.5rem; + } + #infobox-extra p { + margin: 0; + } + p { + margin: 0 0 0.25em 0; + } + .fairy-souls-leaderboard { + color: inherit; + text-decoration: none; + } + @media only screen and (max-width: 600px) { + #infobox { + position: relative; + right: -2em; + margin-top: 0; + } + } + @media only screen and (max-width: 550px) { + #infobox { + position: unset; + box-shadow: none; + float: none; + border: 1px solid var(--theme-lighter-background); + } + } + @media only screen and (max-width: 460px) { + #infobox-container { + max-width: 100%; + float: left; + } + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Inventories.svelte b/src/routes/player/[player]/[profile]/sections/Inventories.svelte new file mode 100644 index 0000000..1dd7d28 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Inventories.svelte @@ -0,0 +1,105 @@ +<script lang="ts"> + import { inventoryIconMap, skyblockItemToUrl, type Item } from '$lib/minecraft/inventory' + import Inventory from '$lib/minecraft/Inventory.svelte' + import type { MatcherFile } from 'skyblock-assets' + import { cleanId } from '$lib/utils' + import AccessoryBagUpgrades from './AccessoryBagUpgrades.svelte' + import type { CleanMemberProfile } from '$lib/APITypes' + + export let data: CleanMemberProfile + export let pack: MatcherFile + + let displayingInventories: string[] = [] + for (const inventoryName in data.member.inventories) + if (inventoryName !== 'armor') displayingInventories.push(inventoryName) + + let selectedInventoryName: string = displayingInventories[0] +</script> + +{#if displayingInventories.length > 1} + <div id="inventory-tabs"> + {#each displayingInventories as inventoryName} + <button + class="inventory-tab" + class:inventory-tab-active={inventoryName === selectedInventoryName} + on:click={() => (selectedInventoryName = inventoryName)} + > + {#if inventoryName in inventoryIconMap} + <img + class="inventory-tab-icon" + loading="lazy" + src={skyblockItemToUrl(inventoryIconMap[inventoryName], pack, 50)} + alt={cleanId(inventoryName)} + /> + {/if} + <span class="inventory-tab-name">{cleanId(inventoryName)}</span> + </button> + {/each} + </div> +{/if} +{#if data.member.inventories} + {#each displayingInventories as inventoryName} + {#if inventoryName === selectedInventoryName} + <span id={inventoryName} class="inventory-content"> + <Inventory items={data.member.inventories[inventoryName]} {pack} name={inventoryName} /> + </span> + {#if inventoryName == 'accessory_bag'} + <AccessoryBagUpgrades {data} /> + {/if} + {/if} + {/each} +{/if} + +<style> + #inventory-tabs { + margin-bottom: 1em; + overflow: hidden; + border-radius: 1em; + max-width: 40em; + /* box-shadow: 0 0 1em #000; */ + } + .inventory-tab { + /* background-color: var(--theme-lighter-background); */ + background-color: rgba(20, 20, 20, 0.4); + color: var(--theme-main-text); + border: none; + border-radius: 0; + padding: 0 0.5em; + cursor: pointer; + transition-duration: 200ms; + height: 2.5em; + vertical-align: middle; + } + .inventory-tab-icon { + height: 1.5em; + width: 1.5em; + vertical-align: text-bottom; + position: relative; + top: 0.1em; + image-rendering: crisp-edges; + image-rendering: pixelated; + } + .inventory-tab-name { + vertical-align: text-top; + } + .inventory-tab:hover, + .inventory-tab-active { + background-color: rgba(40, 40, 40, 0.9); + } + + .inventory-content { + display: inline-grid; + } + + @media only screen and (max-width: 480px) { + .inventory-content :global(.item) { + /* there's no good way to override the existing 32px size without !important :( */ + font-size: 24px !important; + } + } + @media only screen and (max-width: 350px) { + .inventory-content :global(.item) { + font-size: 16px !important; + } + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Leaderboards.svelte b/src/routes/player/[player]/[profile]/sections/Leaderboards.svelte new file mode 100644 index 0000000..817f59a --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Leaderboards.svelte @@ -0,0 +1,46 @@ +<script lang="ts"> + import { fetchApi } from '$lib/api' + + import type { CleanMemberProfile } from '$lib/APITypes' + import Emoji from '$lib/Emoji.svelte' + import { cleanId, formatNumberFromUnit } from '$lib/utils' + + export let data: CleanMemberProfile +</script> + +{#await fetchApi(`player/${data.member.uuid}/${data.profile.uuid}/leaderboards`, fetch).then( r => r.json() )} + Loading... +{:then leaderboards} + {#if leaderboards.length > 0} + <ul> + {#each leaderboards as leaderboard} + <li class="leaderboard-item"> + <a href="/leaderboard/{leaderboard.name}" class="leaderboard-item-anchor"> + {leaderboard.positionIndex + 1}) <b>{cleanId(leaderboard.name)}</b>: {formatNumberFromUnit( + leaderboard.value, + leaderboard.unit ?? null + )} + </a> + </li> + {/each} + </ul> + {:else} + <p>This player isn't in any leaderboards. <Emoji value="😦" /></p> + {/if} +{/await} + +<style> + .leaderboard-item-anchor { + color: inherit; + } + .leaderboard-item { + list-style-type: none; + } + ul { + padding-left: 0; + margin-top: 0.5em; + } + p { + margin: 0.5rem 0; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Minions.svelte b/src/routes/player/[player]/[profile]/sections/Minions.svelte new file mode 100644 index 0000000..856d8f0 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Minions.svelte @@ -0,0 +1,40 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import { cleanId, toRomanNumerals } from '$lib/utils' + + export let data: CleanMemberProfile +</script> + +<p class="unique-minions-text"> + Unique minions: + <span class="minions-fraction"> + <b>{data.profile.minionCount}</b>/{data.profile.maxUniqueMinions} + </span> +</p> +<table> + {#each data.profile.minions as minion} + <tr> + <th>{cleanId(minion.name)}</th> + {#each minion.levels as unlocked, i} + <td class="minion-table-item" class:unlocked> + {toRomanNumerals(i + 1)} + </td> + {/each} + </tr> + {/each} +</table> + +<style> + .unique-minions-text { + color: var(--theme-darker-text); + } + .minions-fraction { + color: var(--theme-main-text); + } + .minion-table-item:not(.unlocked) { + opacity: 0.2; + } + .minion-table-item.unlocked { + color: #3e3; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Pets.svelte b/src/routes/player/[player]/[profile]/sections/Pets.svelte new file mode 100644 index 0000000..7f660c4 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Pets.svelte @@ -0,0 +1,79 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import { cleanId, toRomanNumerals, toTitleCase } from '$lib/utils' + + export let data: CleanMemberProfile + + // we convert it to a set to remove duplicates + const petsAcquiredCount = new Set(data.member.pets.list.map(p => p.id)).size + const totalPetsCount = data.member.pets.missingIds.length + petsAcquiredCount +</script> + +{#if data.member.zones} + <p class="zones-visited-text"> + Pets acquired: + <span class="zones-visited-number"> + <b>{petsAcquiredCount}</b>/{totalPetsCount} + </span> + </p> + <div class="pets-list"> + {#each data.member.pets.list as pet} + <div class="individual-pet-data"> + <h3>{cleanId(pet.id.toLowerCase())}</h3> + <p>Level: <b>{pet.level.toLocaleString()}</b></p> + <p>Tier: <b>{toTitleCase(pet.tier)}</b></p> + {#if pet.item} + <p>Item: <b>{pet.item.display.name}</b></p> + {/if} + </div> + {/each} + </div> + {#if data.member.pets.missingIds.length > 0} + <h3 class="missing-pets-title">Missing</h3> + <ul> + {#each data.member.pets.missingIds as petId} + <li class="missing-pet">{toTitleCase(cleanId(petId.toLowerCase()))}</li> + {/each} + </ul> + {/if} +{/if} + +<style> + p { + margin: 0; + } + .zones-visited-text { + color: var(--theme-darker-text); + margin: 0.5em 0; + } + .zones-visited-number { + color: var(--theme-main-text); + } + .missing-pet { + opacity: 0.5; + } + .individual-pet-data { + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.1); + padding: 0.75em; + border-radius: 1em; + width: 9rem; + } + + .pets-list { + display: flex; + flex-wrap: wrap; + max-width: 40rem; + column-gap: 0.5rem; + row-gap: 0.5rem; + } + + .missing-pets-title { + margin-top: 1rem; + } + + ul { + padding-left: 1em; + margin: 0; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Skills.svelte b/src/routes/player/[player]/[profile]/sections/Skills.svelte new file mode 100644 index 0000000..8fc5aaf --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Skills.svelte @@ -0,0 +1,87 @@ +<script lang="ts"> + import Emoji from '$lib/Emoji.svelte' + + import Tooltip from '$lib/Tooltip.svelte' + import { cleanId, formatNumber } from '$lib/utils' + + const skillImages = { + runecrafting: '/skill-icons/runecrafting.webp', + alchemy: '/skill-icons/herblore.webp', + combat: '/skill-icons/attack.webp', + enchanting: '/skill-icons/magic.webp', + foraging: '/skill-icons/woodcutting.webp', + mining: '/skill-icons/mining.webp', + taming: '/skill-icons/hunter.webp', + farming: '/skill-icons/farming.webp', + fishing: '/skill-icons/fishing.webp', + carpentry: '/skill-icons/construction.webp', + social: '/skill-icons/agility.webp', + } + + export let data +</script> + +{#if !data.member.skills.apiEnabled} + <p class="skills-api-warning"> + <Emoji value="⚠" /> Skills API is disabled for this profile, so the values shown may be inaccurate. + </p> +{/if} +<ul> + {#each data.member.skills.list as skill} + <li + class="list-item-with-icon" + style="background: url({skillImages[skill.id]}) 0 0/1em no-repeat" + > + <Tooltip> + <span slot="tooltip"> + {#if skill.levelXpRequired !== null} + {Math.round((skill.levelXp / skill.levelXpRequired) * 100)}% to next level, {Math.round( + skill.levelXp + ).toLocaleString()}/{formatNumber(skill.levelXpRequired, 3)} xp + {:else} + {Math.round(skill.levelXp).toLocaleString()} extra xp + {/if} + </span> + <span> + {cleanId(skill.id)} + <span class="skill-level" class:skill-maxed={skill.level === skill.maxLevel}> + {skill.level} + </span> + </span> + </Tooltip> + </li> + {/each} +</ul> + +<style> + .skill-level { + opacity: 0.9; + } + .skill-maxed { + color: #0e0; + opacity: 1; + } + .list-item-with-icon { + list-style: none; + padding-left: 1.2em; + position: relative; + right: 1.2em; + image-rendering: crisp-edges; + image-rendering: pixelated; + } + + .skills-api-warning { + margin-top: 0; + } + + ul { + margin-top: 0; + display: flex; + flex-wrap: wrap; + max-width: 30em; + } + ul > li { + width: 10em; + margin: 0.25em 0.25em 0 0; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Slayers.svelte b/src/routes/player/[player]/[profile]/sections/Slayers.svelte new file mode 100644 index 0000000..40fa67d --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Slayers.svelte @@ -0,0 +1,74 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import { cleanId, toRomanNumerals } from '$lib/utils' + + export let data: CleanMemberProfile +</script> + +{#if data.member.slayers} + <div class="slayer-info-text total-slayer-info-text"> + <p>Xp: <b>{data.member.slayers.xp}</b></p> + <p>Kills: <b>{data.member.slayers.kills}</b></p> + </div> + <div class="slayers-list"> + {#each data.member.slayers.bosses as slayer} + <div class="individual-slayer-data"> + <h3>{slayer.name ? cleanId(slayer.name) : cleanId(slayer.rawName)}</h3> + <div class="slayer-info-text"> + <p>Xp: <b>{slayer.xp.toLocaleString()}</b></p> + <p>Level: <b>{slayer.level}</b></p> + </div> + <table> + <tr> + {#each slayer.tiers as tier} + <th>Tier {toRomanNumerals(tier.tier)}</th> + {/each} + </tr> + <tr> + {#each slayer.tiers as tier} + <td class="slayer-tier-kills"> + {tier.kills.toLocaleString()} + </td> + {/each} + </tr> + </table> + </div> + {/each} + </div> +{/if} + +<style> + p { + margin: 0; + } + + .total-slayer-info-text { + margin: 0.5em 0; + } + + .slayer-tier-kills { + text-align: center; + } + + .slayer-info-text { + color: var(--theme-darker-text); + } + .slayer-info-text b { + color: var(--theme-main-text); + } + + .individual-slayer-data { + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(0, 0, 0, 0.1); + padding: 0.75em; + border-radius: 1em; + } + + .slayers-list { + display: flex; + flex-wrap: wrap; + max-width: 40rem; + column-gap: 0.5rem; + row-gap: 0.5rem; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/StatList.svelte b/src/routes/player/[player]/[profile]/sections/StatList.svelte new file mode 100644 index 0000000..38604d7 --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/StatList.svelte @@ -0,0 +1,48 @@ +<!-- + @component + + A sorted list of a user's stats, with the total sometimes being at the top. +--> +<script lang="ts"> + import { cleanId, millisecondsToTime } from '$lib/utils' + import type { StatItem } from '$lib/APITypes' + + export let stats: StatItem[] +</script> + +<ul> + {#each stats.sort((a, b) => b.value - a.value) as stat} + <li class:total-stat={stat.categorizedName === 'total'}> + <span class="stat-name">{cleanId(stat.categorizedName)}:</span> + <span class="stat-value"> + {#if stat.unit === 'time'} + {millisecondsToTime(stat.value)} + {:else} + {stat.value.toLocaleString()} + {/if} + </span> + </li> + {/each} +</ul> + +<style> + .total-stat .stat-name { + color: var(--theme-darker-text); + } + .total-stat .stat-value { + font-weight: bold; + } + + .total-stat { + list-style-type: none; + padding: 0 0 0.5em 0; + right: 1em; + } + li { + position: relative; + } + ul { + margin-top: 0.5em; + padding-left: 1em; + } +</style> diff --git a/src/routes/player/[player]/[profile]/sections/Zones.svelte b/src/routes/player/[player]/[profile]/sections/Zones.svelte new file mode 100644 index 0000000..f7e993b --- /dev/null +++ b/src/routes/player/[player]/[profile]/sections/Zones.svelte @@ -0,0 +1,42 @@ +<script lang="ts"> + import type { CleanMemberProfile } from '$lib/APITypes' + import { cleanId } from '$lib/utils' + + export let data: CleanMemberProfile + + let zonesVisitedCount = data.member.zones ? data.member.zones?.filter(z => z.visited).length : 0 +</script> + +{#if data.member.zones} + <p class="zones-visited-text"> + Zones visited: + <span class="zones-visited-number"> + <b>{zonesVisitedCount}</b>/{data.member.zones.length} + </span> + </p> + <ul> + {#each data.member.zones.filter(z => z.visited) as zone} + <li>{cleanId(zone.name)}</li> + {/each} + {#each data.member.zones.filter(z => !z.visited) as zone} + <li class="unvisited-zone">{cleanId(zone.name)}</li> + {/each} + </ul> +{/if} + +<style> + .zones-visited-text { + color: var(--theme-darker-text); + margin: 0.5em 0; + } + .zones-visited-number { + color: var(--theme-main-text); + } + .unvisited-zone { + opacity: 0.5; + } + ul { + padding-left: 1em; + margin: 0; + } +</style> diff --git a/src/routes/player/index.ts b/src/routes/player/index.ts deleted file mode 100644 index 4644499..0000000 --- a/src/routes/player/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { RequestHandler } from '@sveltejs/kit' - -export const post: RequestHandler = async ({ request }) => { - const form = await request.formData() - - const player = form.get('user-search') - - return { - status: 303, - headers: { - location: `/player/${player}` - } - } -}
\ No newline at end of file diff --git a/src/routes/profile/+page.server.ts b/src/routes/profile/+page.server.ts new file mode 100644 index 0000000..60c24c1 --- /dev/null +++ b/src/routes/profile/+page.server.ts @@ -0,0 +1,7 @@ +import type { ServerLoad } from '@sveltejs/kit' + +export const load = (({ locals }) => { + return { + sid: locals.sid + } +}) satisfies ServerLoad diff --git a/src/routes/profile/index.svelte b/src/routes/profile/+page.svelte index 4da4a8f..244d1a2 100644 --- a/src/routes/profile/index.svelte +++ b/src/routes/profile/+page.svelte @@ -1,65 +1,21 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - import { fetchApi } from '$lib/api' - import type { AccountCustomization, AccountSchema, CleanUser, SessionSchema } from '$lib/APITypes' - import Head from '$lib/Head.svelte' - import Header from '$lib/Header.svelte' - import donators from '../../_donators.json' - import admins from '../../_admins.json' - - export const load: Load = async ({ fetch, session }) => { - const sessionResponse: { session: SessionSchema | null; account: AccountSchema | null } | null = - await fetchApi(`accounts/session`, fetch, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - uuid: session.sid, - }), - }).then(r => r.json()) - - const playerResponse = sessionResponse?.account - ? await fetchApi(`player/${sessionResponse.account.minecraftUuid}`, fetch).then(r => r.json()) - : null - - // redirect to /login if the user is not logged in - if ( - !sessionResponse || - !sessionResponse.account || - !sessionResponse.session || - !playerResponse.player - ) { - return { redirect: '/login', status: 303 } - } - - const isDonator = - donators.find(d => d?.uuid === sessionResponse.account?.minecraftUuid) !== undefined - const isAdmin = admins.find(a => a === sessionResponse.account?.minecraftUuid) !== undefined - - return { - props: { - session: sessionResponse.session, - account: sessionResponse.account, - player: playerResponse, - isDonator: isDonator || isAdmin, - }, - } - } -</script> - <script lang="ts"> import Emoji from '$lib/Emoji.svelte' - import { browser } from '$app/env' + import { browser } from '$app/environment' import Tooltip from '$lib/Tooltip.svelte' import { onDestroy, onMount } from 'svelte' import backgroundNames from '../../_backgrounds.json' + import type { AccountCustomization, AccountSchema, CleanUser, SessionSchema } from '$lib/APITypes' + import type { PageData } from './$types' + import Head from '$lib/Head.svelte' + import Header from '$lib/Header.svelte' + + export let data: PageData - export let session: SessionSchema - export let account: AccountSchema - export let player: CleanUser + export let session: SessionSchema = data.session + export let account: AccountSchema = data.account + export let player: CleanUser = data.player - export let isDonator: boolean + export let isDonator: boolean = data.isDonator let pack: AccountCustomization['pack'] = account?.customization?.pack ?? 'furfsky_reborn' let blurBackground: AccountCustomization['blurBackground'] = @@ -172,6 +128,11 @@ style="background-image: url(/backgrounds-small/{thisBackgroundName})" title={thisBackgroundName} on:click={() => (backgroundName = thisBackgroundName)} + on:keypress={e => { + if (e.key === 'Enter') { + backgroundName = thisBackgroundName + } + }} /> {/each} </div> diff --git a/src/routes/profile/+page.ts b/src/routes/profile/+page.ts new file mode 100644 index 0000000..cdd2a93 --- /dev/null +++ b/src/routes/profile/+page.ts @@ -0,0 +1,44 @@ +import { fetchApi } from '$lib/api' +import type { AccountSchema, SessionSchema } from '$lib/APITypes' +import donators from '../../_donators.json' +import admins from '../../_admins.json' +import type { PageLoad } from './$types' +import { redirect } from '@sveltejs/kit' + +export const load = (async ({ fetch, data }) => { + const sessionResponse: { session: SessionSchema | null; account: AccountSchema | null } | null = + await fetchApi(`accounts/session`, fetch, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + uuid: data.sid, + }), + }).then(r => r.json()) + + const playerResponse = sessionResponse?.account + ? await fetchApi(`player/${sessionResponse.account.minecraftUuid}`, fetch).then(r => r.json()) + : null + + // redirect to /login if the user is not logged in + if ( + !sessionResponse || + !sessionResponse.account || + !sessionResponse.session || + !playerResponse.player + ) { + throw redirect(303, '/login') + } + + const isDonator = + donators.find(d => d?.uuid === sessionResponse.account?.minecraftUuid) !== undefined + const isAdmin = admins.find(a => a === sessionResponse.account?.minecraftUuid) !== undefined + + return { + session: sessionResponse.session, + account: sessionResponse.account, + player: playerResponse, + isDonator: isDonator || isAdmin, + } +}) as PageLoad diff --git a/src/routes/profile/update.ts b/src/routes/profile/update/+server.ts index 168cba8..7190fb8 100644 --- a/src/routes/profile/update.ts +++ b/src/routes/profile/update/+server.ts @@ -1,11 +1,11 @@ import { fetchApi } from '$lib/api' import type { AccountSchema, SessionSchema } from '$lib/APITypes' -import type { RequestHandler } from '@sveltejs/kit' -import backgroundFileNames from '../../_backgrounds.json' -import donators from '../../_donators.json' -import admins from '../../_admins.json' -import type { JSONValue } from '@sveltejs/kit/types/internal' +import backgroundFileNames from '../../../_backgrounds.json' +import donators from '../../../_donators.json' +import admins from '../../../_admins.json' import env from '$lib/env' +import type { PageServerLoad } from '../$types' +import { error, json } from '@sveltejs/kit' const emojiRegex = /^(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])$/ @@ -15,19 +15,13 @@ function isValidEmoji(emoji: string) { } -export const patch: RequestHandler = async ({ request, locals, platform }) => { +export const PATCH = (async ({ request, locals, platform }) => { if (locals.sid === undefined) { - return { - body: { ok: false, error: 'You are not logged in.' }, - status: 401, - } + throw error(401, 'You are not logged in.') } const key = env(platform).SKYBLOCK_STATS_API_KEY if (!key) { - return { - body: { ok: false, error: 'The SKYBLOCK_STATS_API_KEY environment variable is not set.' }, - status: 500, - } + throw error(500, 'The SKYBLOCK_STATS_API_KEY environment variable is not set.') } const data = await request.json() @@ -41,10 +35,7 @@ export const patch: RequestHandler = async ({ request, locals, platform }) => { }), }).then(r => r.json()) if (!sessionResponse.session || !sessionResponse.account?.minecraftUuid) - return { - body: { ok: false, error: 'Invalid session.' }, - status: 401, - } + throw error(401, 'Invalid session.') const backgroundName = data.backgroundName const pack = data.pack @@ -55,49 +46,28 @@ export const patch: RequestHandler = async ({ request, locals, platform }) => { const isAdmin = admins.includes(sessionResponse.account?.minecraftUuid) if (typeof backgroundName !== 'undefined' && typeof backgroundName !== 'string') { - return { - body: { ok: false, error: 'Invalid background.' }, - status: 400, - } + throw error(400, 'Invalid background.') } if (typeof pack !== 'string') { - return { - body: { ok: false, error: 'Invalid pack.' }, - status: 400, - } + throw error(400, 'Invalid pack.') } if (typeof blurBackground !== 'boolean') { - return { - body: { ok: false, error: 'Invalid blurBackground.' }, - status: 400, - } + throw error(400, 'Invalid blurBackground.') } if (typeof emoji !== 'undefined' && typeof emoji !== 'string') { - return { - body: { ok: false, error: 'Invalid emoji.' }, - status: 400, - } + throw error(400, 'Invalid emoji.') } // prevent people from putting non-existent backgrounds if (backgroundName && !backgroundFileNames.includes(backgroundName)) - return { - body: { ok: false, error: 'Invalid background.' }, - status: 400, - } + throw error(400, 'Invalid background.') const backgroundUrl = backgroundName ? `/backgrounds/${backgroundName}` : undefined if (emoji) { if (!isDonator && !isAdmin) - return { - body: { ok: false, error: 'You are not allowed to use emojis.' }, - status: 401, - } + throw error(401, 'You are not allowed to use emojis.') if (!isValidEmoji(emoji)) - return { - body: { ok: false, error: 'Invalid emoji.' }, - status: 400, - } + throw error(400, 'Invalid emoji.') } const updatedAccount: AccountSchema = { @@ -118,9 +88,8 @@ export const patch: RequestHandler = async ({ request, locals, platform }) => { }, body: JSON.stringify(updatedAccount), }).then(r => r.json()) + console.log(response) - return { - body: { ok: true } as JSONValue, - } -}
\ No newline at end of file + return json({ ok: true }) +}) satisfies PageServerLoad
\ No newline at end of file diff --git a/src/routes/verify.ts b/src/routes/verify.ts deleted file mode 100644 index 3321164..0000000 --- a/src/routes/verify.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { fetchApi } from '$lib/api' -import type { AccountSchema, CleanUser, SessionSchema } from '$lib/APITypes' -import type { RequestHandler } from '@sveltejs/kit' -import env from '$lib/env' - - -function redirect(status: number, location: string) { - return { - status, - headers: { - location, - }, - } -} - -export const post: RequestHandler = async ({ request, locals, platform }) => { - const key = env(platform).SKYBLOCK_STATS_API_KEY - if (!key) { - return redirect(303, `/verify?error=NO_KEY`) - } - if (locals.sid === undefined) { - return redirect(303, '/login') - } - - const form = await request.formData() - - // username or uuid - const playerIdentifier = form.get('ign') - if (!playerIdentifier) { - return redirect(303, `/verify?error=NO_IGN`) - } - - const playerResponse: CleanUser = await fetchApi(`player/${playerIdentifier}`, fetch).then(res => res.json()) - const sessionResponse: { session: SessionSchema | null, account: AccountSchema | null } = await fetchApi(`accounts/session`, fetch, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - uuid: locals.sid, - }), - }).then(r => r.json()) - - if (!sessionResponse.session) - return redirect(303, '/login') - - const hypixelDiscordName = playerResponse.player?.socials.discord - - if (!hypixelDiscordName) - return redirect(303, `/verify?error=NOT_LINKED`) - - const discordUser = sessionResponse.session.discord_user - const actualDiscordName = discordUser.name - // some people link themselves as <id>#<discrim> instead of <name>#<discrim> - const actualDiscordIdDiscrim = `${discordUser.id}#${discordUser.name.split('#')[1]}` - - if (!(hypixelDiscordName === actualDiscordName || hypixelDiscordName === actualDiscordIdDiscrim)) - return redirect(303, `/verify?error=WRONG_NAME¤t=${encodeURIComponent(hypixelDiscordName)}&correct=${encodeURIComponent(actualDiscordName)}`) - - const updatedAccount: AccountSchema = { - discordId: sessionResponse.session.discord_user.id, - minecraftUuid: playerResponse.player?.uuid - } - - await fetchApi(`accounts/update`, fetch, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - key: key - }, - body: JSON.stringify(updatedAccount), - }).then(r => r.json()) - - return redirect(303, '/profile') -}
\ No newline at end of file diff --git a/src/routes/verify/+page.server.ts b/src/routes/verify/+page.server.ts new file mode 100644 index 0000000..bd3ea7f --- /dev/null +++ b/src/routes/verify/+page.server.ts @@ -0,0 +1,75 @@ +import { fetchApi } from '$lib/api' +import type { AccountSchema, CleanUser, SessionSchema } from '$lib/APITypes' +import { redirect, type RequestHandler, type ServerLoad } from '@sveltejs/kit' +import env from '$lib/env' +import type { Actions } from './$types' + + +export const actions: Actions = { + default: async ({ platform, locals, request }) => { + const key = env(platform).SKYBLOCK_STATS_API_KEY + if (!key) { + throw redirect(303, `/verify?error=NO_KEY`) + } + if (locals.sid === undefined) { + throw redirect(303, '/login') + } + + const form = await request.formData() + + // username or uuid + const playerIdentifier = form.get('ign') + if (!playerIdentifier) { + throw redirect(303, `/verify?error=NO_IGN`) + } + + const playerResponse: CleanUser = await fetchApi(`player/${playerIdentifier}`, fetch).then(res => res.json()) + const sessionResponse: { session: SessionSchema | null, account: AccountSchema | null } = await fetchApi(`accounts/session`, fetch, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + uuid: locals.sid, + }), + }).then(r => r.json()) + + if (!sessionResponse.session) + throw redirect(303, '/login') + + const hypixelDiscordName = playerResponse.player?.socials.discord + + if (!hypixelDiscordName) + throw redirect(303, `/verify?error=NOT_LINKED`) + + const discordUser = sessionResponse.session.discord_user + const actualDiscordName = discordUser.name + // some people link themselves as <id>#<discrim> instead of <name>#<discrim> + const actualDiscordIdDiscrim = `${discordUser.id}#${discordUser.name.split('#')[1]}` + + if (!(hypixelDiscordName === actualDiscordName || hypixelDiscordName === actualDiscordIdDiscrim)) + throw redirect(303, `/verify?error=WRONG_NAME¤t=${encodeURIComponent(hypixelDiscordName)}&correct=${encodeURIComponent(actualDiscordName)}`) + + const updatedAccount: AccountSchema = { + discordId: sessionResponse.session.discord_user.id, + minecraftUuid: playerResponse.player?.uuid + } + + await fetchApi(`accounts/update`, fetch, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + key: key + }, + body: JSON.stringify(updatedAccount), + }).then(r => r.json()) + + throw redirect(303, '/profile') + } +} + +export const load = (({ locals }) => { + return { + sid: locals.sid + } +}) satisfies ServerLoad diff --git a/src/routes/verify.svelte b/src/routes/verify/+page.svelte index 20de403..4edc50d 100644 --- a/src/routes/verify.svelte +++ b/src/routes/verify/+page.svelte @@ -1,27 +1,14 @@ -<script lang="ts" context="module"> - import type { Load } from '@sveltejs/kit' - export const load: Load = async ({ session, url }) => { - if (session.sid === undefined) { - return { redirect: '/login', status: 303 } - } - return { - props: { - errorCode: url.searchParams.get('error'), - current: url.searchParams.get('current'), - correct: url.searchParams.get('correct'), - }, - } - } -</script> - <script lang="ts"> import Emoji from '$lib/Emoji.svelte' import Head from '$lib/Head.svelte' import Header from '$lib/Header.svelte' + import type { PageData } from './$types' + + export let data: PageData - export let errorCode: string | null - export let current: string | null - export let correct: string | null + export let errorCode: string | null = data.errorCode + export let current: string | null = data.current + export let correct: string | null = data.correct const errorCodes = { NO_IGN: 'Please enter a valid Minecraft username.', diff --git a/src/routes/verify/+page.ts b/src/routes/verify/+page.ts new file mode 100644 index 0000000..8e5f698 --- /dev/null +++ b/src/routes/verify/+page.ts @@ -0,0 +1,13 @@ +import { redirect } from '@sveltejs/kit' +import type { PageLoad } from './$types' + +export const load = (async ({ data, url }) => { + if (data.sid === undefined) { + throw redirect(303, '/login') + } + return { + errorCode: url.searchParams.get('error'), + current: url.searchParams.get('current'), + correct: url.searchParams.get('correct'), + } +}) satisfies PageLoad |