aboutsummaryrefslogtreecommitdiff
path: root/src/routes
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2022-12-15 20:19:42 -0600
committerGitHub <noreply@github.com>2022-12-15 20:19:42 -0600
commited5eedab8f9fc90dadf5c442cf559572d1b35f0c (patch)
tree01a763fd11810e9970f14f7dae180e95b279de9a /src/routes
parent89bf3d31e36ad3bdfd45461ee6fb69a4c791f848 (diff)
parent103689520f51991a1e9a4ca5829fe2f46d1a32c2 (diff)
downloadskyblock-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')
-rw-r--r--src/routes/+error.svelte (renamed from src/routes/__error.svelte)22
-rw-r--r--src/routes/+layout.svelte (renamed from src/routes/__layout.svelte)0
-rw-r--r--src/routes/+page.server.ts7
-rw-r--r--src/routes/+page.svelte (renamed from src/routes/index.svelte)16
-rw-r--r--src/routes/[player=username]/+page.ts6
-rw-r--r--src/routes/[player].ts14
-rw-r--r--src/routes/auctionprices/+page.svelte (renamed from src/routes/auctionprices.svelte)27
-rw-r--r--src/routes/auctionprices/+page.ts13
-rw-r--r--src/routes/chat/+page.svelte (renamed from src/routes/chat.svelte)0
-rw-r--r--src/routes/election/+page.svelte (renamed from src/routes/election.svelte)30
-rw-r--r--src/routes/election/+page.ts8
-rw-r--r--src/routes/items/+page.svelte (renamed from src/routes/items.svelte)15
-rw-r--r--src/routes/items/+page.ts6
-rw-r--r--src/routes/leaderboard/+page.ts6
-rw-r--r--src/routes/leaderboard/[name].ts12
-rw-r--r--src/routes/leaderboard/[name]/+server.ts7
-rw-r--r--src/routes/leaderboard/index.ts11
-rw-r--r--src/routes/leaderboards/+page.svelte (renamed from src/routes/leaderboards/index.svelte)15
-rw-r--r--src/routes/leaderboards/+page.ts6
-rw-r--r--src/routes/leaderboards/[name]/+page.svelte (renamed from src/routes/leaderboards/[name].svelte)19
-rw-r--r--src/routes/leaderboards/[name]/+page.ts13
-rw-r--r--src/routes/loggedin/+server.ts (renamed from src/routes/loggedin.ts)25
-rw-r--r--src/routes/login.ts36
-rw-r--r--src/routes/login/+server.ts18
-rw-r--r--src/routes/logout/+server.ts (renamed from src/routes/logout.ts)15
-rw-r--r--src/routes/player/+page.server.ts12
-rw-r--r--src/routes/player/[player]/+page.svelte (renamed from src/routes/player/[player]/index.svelte)37
-rw-r--r--src/routes/player/[player]/+page.ts19
-rw-r--r--src/routes/player/[player]/[profile]/+page.svelte (renamed from src/routes/player/[player]/[profile].svelte)97
-rw-r--r--src/routes/player/[player]/[profile]/+page.ts40
-rw-r--r--src/routes/player/[player]/[profile]/sections/AccessoryBagUpgrades.svelte100
-rw-r--r--src/routes/player/[player]/[profile]/sections/Achievements.svelte87
-rw-r--r--src/routes/player/[player]/[profile]/sections/Armor.svelte14
-rw-r--r--src/routes/player/[player]/[profile]/sections/Auctions.svelte109
-rw-r--r--src/routes/player/[player]/[profile]/sections/Bank.svelte89
-rw-r--r--src/routes/player/[player]/[profile]/sections/Claimed.svelte33
-rw-r--r--src/routes/player/[player]/[profile]/sections/Collections.svelte62
-rw-r--r--src/routes/player/[player]/[profile]/sections/Coop.svelte79
-rw-r--r--src/routes/player/[player]/[profile]/sections/Essence.svelte27
-rw-r--r--src/routes/player/[player]/[profile]/sections/FarmingContests.svelte62
-rw-r--r--src/routes/player/[player]/[profile]/sections/Harp.svelte72
-rw-r--r--src/routes/player/[player]/[profile]/sections/Infobox.svelte83
-rw-r--r--src/routes/player/[player]/[profile]/sections/Inventories.svelte105
-rw-r--r--src/routes/player/[player]/[profile]/sections/Leaderboards.svelte46
-rw-r--r--src/routes/player/[player]/[profile]/sections/Minions.svelte40
-rw-r--r--src/routes/player/[player]/[profile]/sections/Pets.svelte79
-rw-r--r--src/routes/player/[player]/[profile]/sections/Skills.svelte87
-rw-r--r--src/routes/player/[player]/[profile]/sections/Slayers.svelte74
-rw-r--r--src/routes/player/[player]/[profile]/sections/StatList.svelte48
-rw-r--r--src/routes/player/[player]/[profile]/sections/Zones.svelte42
-rw-r--r--src/routes/player/index.ts14
-rw-r--r--src/routes/profile/+page.server.ts7
-rw-r--r--src/routes/profile/+page.svelte (renamed from src/routes/profile/index.svelte)71
-rw-r--r--src/routes/profile/+page.ts44
-rw-r--r--src/routes/profile/update/+server.ts (renamed from src/routes/profile/update.ts)69
-rw-r--r--src/routes/verify.ts75
-rw-r--r--src/routes/verify/+page.server.ts75
-rw-r--r--src/routes/verify/+page.svelte (renamed from src/routes/verify.svelte)25
-rw-r--r--src/routes/verify/+page.ts13
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&current=${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&current=${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