Skip to content

Commit 23b7038

Browse files
committed
feat: make profile data a reactive class
1 parent 7d1ac9b commit 23b7038

24 files changed

+251
-224
lines changed

src/context/createContext.svelte.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import type { IsMobile } from "$lib/hooks/is-mobile.svelte";
33
import type { ModelsMiscOutput, ModelsResourcePackConfig, ModelsSkillsOutput, ModelsStatsOutput } from "$lib/shared/api/orval-generated";
44
import { createContext } from "svelte";
55

6+
export class ProfileContext {
7+
#current: ModelsStatsOutput | null = $state(null);
8+
9+
get current() {
10+
return this.#current;
11+
}
12+
13+
set current(value: ModelsStatsOutput | null) {
14+
this.#current = value;
15+
}
16+
}
17+
618
export class PacksContext {
719
#packs: ModelsResourcePackConfig[] = $state([]);
820

@@ -39,7 +51,7 @@ export class SkillsContext {
3951
}
4052
}
4153

42-
export const [getProfileContext, setProfileContext] = createContext<ModelsStatsOutput>();
54+
export const [getProfileContext, setProfileContext] = createContext<ProfileContext>();
4355
export const [getSkillsContext, setSkillsContext] = createContext<SkillsContext>();
4456
export const [getMiscContext, setMiscContext] = createContext<MiscContext>();
4557
export const [getMobileContext, setMobileContext] = createContext<IsMobile>();

src/lib/components/APINotice.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import { fade } from "svelte/transition";
88
import { Drawer } from "vaul-svelte";
99
10-
const profile = $derived(getProfileContext());
10+
const profile = $derived(getProfileContext().current);
1111
12-
const apiSettings = $derived(Object.entries(profile.apiSettings ?? {}).filter(([_, value]) => !value));
12+
const apiSettings = $derived(Object.entries(profile?.apiSettings ?? {}).filter(([_, value]) => !value));
1313
1414
const isHover = getHoverContext();
1515
</script>
@@ -24,7 +24,7 @@
2424
{/if}
2525
<span class="inline-block whitespace-nowrap capitalize">{key.replaceAll("_", " ")}</span>{#if index < apiSettings.length - 1},{/if}
2626
{/each}
27-
{apiSettings.length === 1 ? "is" : "are"} not available for {profile.username} due to limited API access.
27+
{apiSettings.length === 1 ? "is" : "are"} not available for {profile?.username} due to limited API access.
2828
</p>
2929
<p>
3030
{#if isHover.current}

src/lib/components/Navbar.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
import { onDestroy, onMount, tick, type Snippet } from "svelte";
1212
const { children }: { children?: Snippet } = $props();
1313
14-
const profile = $derived(getProfileContext());
14+
const profile = $derived(getProfileContext().current);
1515
16-
const apiSettings = $derived(Object.entries(profile.apiSettings ?? {}).filter(([_, value]) => !value));
16+
const apiSettings = $derived(Object.entries(profile?.apiSettings ?? {}).filter(([_, value]) => !value));
1717
const disabledApiSettings: string[] = $derived(apiSettings.map(([key]) => key));
1818
1919
const filteredSectionOrderPreferences = $derived(
@@ -125,7 +125,7 @@
125125
<ScrollArea.Root type="always" class="navbar group sticky! top-[calc(3rem+env(safe-area-inset-top,0))] z-20 overflow-clip" data-pinned={pinned} bind:ref={navbarElement}>
126126
<ScrollArea.Viewport>
127127
<div class="flex! flex-nowrap items-center gap-2 pb-2 font-semibold whitespace-nowrap text-text/80">
128-
<div class="absolute bottom-1.75 z-1 h-[2px] w-[calc(100%+0.5rem)] bg-icon"></div>
128+
<div class="absolute bottom-1.75 z-1 h-0.5 w-[calc(100%+0.5rem)] bg-icon"></div>
129129
<div class="absolute inset-0 bottom-2 group-data-[pinned=true]:group-data-[mode=dark]/html:bg-[oklch(19.13%_0_0)]/90 group-data-[pinned=true]:group-data-[mode=light]/html:bg-[oklch(95.51%_0_0)]/92"></div>
130130
{#each filteredSectionOrderPreferences as section, index (index)}
131131
<Button.Root class="relative px-2 py-3 after:absolute after:top-full after:left-0 after:h-0 after:w-full after:origin-top after:rounded-full after:bg-icon after:transition-all after:duration-100 after:ease-out hover:after:top-[calc(100%-4px)] hover:after:h-2 data-[active=true]:text-text data-[active=true]:after:top-[calc(100%-4px)] data-[active=true]:after:h-2" data-id={section.name} data-active={$tabValue === section.name} onclick={() => handleSectionClick(section.name)}>

src/lib/components/Skin3D.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import * as skinview3d from "skinview3d";
66
import { onDestroy } from "svelte";
77
8-
const ctx = getProfileContext();
9-
const uuid = $derived(ctx.uuid);
8+
const ctx = $derived(getProfileContext().current);
9+
const uuid = $derived(ctx?.uuid);
1010
1111
let { class: className }: { class: string | undefined } = $props();
1212
let viewer = $state<skinview3d.SkinViewer>();

src/lib/layouts/stats/AdditionalStats.svelte

Lines changed: 67 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
import { format as dateFormat, formatDistanceToNowStrict } from "date-fns";
1515
import { format as numberFormat } from "numerable";
1616
17-
const profile = $derived(getProfileContext());
18-
const profileUUID = $derived(profile.uuid);
19-
const profileId = $derived(profile.profile_id);
17+
const profile = $derived(getProfileContext().current);
18+
const profileUUID = $derived(profile?.uuid);
19+
const profileId = $derived(profile?.profile_id);
2020
2121
const defaultPatternDecimal: string = "0,0.##";
2222
const defaultPattern: string = "0,0";
@@ -27,75 +27,76 @@
2727
</script>
2828

2929
<div class="additional-stats flex flex-col gap-2 @md:flex-row @md:flex-wrap">
30-
{#if profile.joined != null}
31-
<AdditionStat text="Joined" data={formatDistanceToNowStrict(profile.joined, { addSuffix: true, in: tz(Intl.DateTimeFormat().resolvedOptions().timeZone) })} asterisk={true}>
32-
Joined on {dateFormat(profile.joined, "dd MMMM yyyy 'at' HH:mm", { in: tz(Intl.DateTimeFormat().resolvedOptions().timeZone) })}
33-
</AdditionStat>
34-
{/if}
35-
{#if profile.purse != null}
36-
<AdditionStat text="Purse" data={`${formatNumber(profile.purse)} Coins`} />
37-
{/if}
38-
{#if profile.bank != null && profile.personalBank != null}
39-
<AdditionStat text="Bank Account" data={`${formatNumber(profile.bank + profile.personalBank)} Coins`} asterisk={profile.bank && profile.personalBank ? true : false}>
40-
<div>
41-
<h3 class="font-bold text-text/85">
42-
Bank:
43-
<span class="text-text">
44-
{formatNumber(profile.bank)}
45-
</span>
46-
</h3>
47-
{#if profile.personalBank}
48-
<h3 class="font-bold text-text/85">
49-
Personal Bank:
50-
<span class="text-text">
51-
{formatNumber(profile.personalBank)}
52-
</span>
53-
</h3>
54-
{/if}
55-
</div>
56-
</AdditionStat>
57-
{/if}
58-
{#if profile.skills?.averageSkillLevel}
59-
<AdditionStat text="Average Skill Level" data={profile.skills.averageSkillLevel.toFixed(2)} asterisk={true}>
60-
<div class="max-w-xs space-y-2">
30+
{#if profile != null}
31+
{#if profile.joined != null}
32+
<AdditionStat text="Joined" data={formatDistanceToNowStrict(profile.joined, { addSuffix: true, in: tz(Intl.DateTimeFormat().resolvedOptions().timeZone) })} asterisk={true}>
33+
Joined on {dateFormat(profile.joined, "dd MMMM yyyy 'at' HH:mm", { in: tz(Intl.DateTimeFormat().resolvedOptions().timeZone) })}
34+
</AdditionStat>
35+
{/if}
36+
{#if profile.purse != null}
37+
<AdditionStat text="Purse" data={`${formatNumber(profile.purse)} Coins`} />
38+
{/if}
39+
{#if profile.bank != null && profile.personalBank != null}
40+
<AdditionStat text="Bank Account" data={`${formatNumber(profile.bank + profile.personalBank)} Coins`} asterisk={profile.bank && profile.personalBank ? true : false}>
6141
<div>
6242
<h3 class="font-bold text-text/85">
63-
Total Skill XP:
43+
Bank:
6444
<span class="text-text">
65-
{numberFormat(profile.skills.totalSkillXp, defaultPattern)}
45+
{formatNumber(profile.bank)}
6646
</span>
6747
</h3>
68-
<p class="font-medium text-text/80">Total XP gained in all skills except Social and Runecrafting.</p>
48+
{#if profile.personalBank}
49+
<h3 class="font-bold text-text/85">
50+
Personal Bank:
51+
<span class="text-text">
52+
{formatNumber(profile.personalBank)}
53+
</span>
54+
</h3>
55+
{/if}
6956
</div>
70-
{#if profile.skills.averageSkillLevelWithProgress != null}
57+
</AdditionStat>
58+
{/if}
59+
{#if profile.skills?.averageSkillLevel}
60+
<AdditionStat text="Average Skill Level" data={profile.skills.averageSkillLevel.toFixed(2)} asterisk={true}>
61+
<div class="max-w-xs space-y-2">
7162
<div>
7263
<h3 class="font-bold text-text/85">
73-
Average Level:
64+
Total Skill XP:
7465
<span class="text-text">
75-
{profile.skills.averageSkillLevelWithProgress.toFixed(2)}
66+
{numberFormat(profile.skills.totalSkillXp, defaultPattern)}
7667
</span>
7768
</h3>
78-
<p class="font-medium text-text/80">Average skill level over all skills except Social and Runecrafting, includes progress to next level.</p>
69+
<p class="font-medium text-text/80">Total XP gained in all skills except Social and Runecrafting.</p>
70+
</div>
71+
{#if profile.skills.averageSkillLevelWithProgress != null}
72+
<div>
73+
<h3 class="font-bold text-text/85">
74+
Average Level:
75+
<span class="text-text">
76+
{profile.skills.averageSkillLevelWithProgress.toFixed(2)}
77+
</span>
78+
</h3>
79+
<p class="font-medium text-text/80">Average skill level over all skills except Social and Runecrafting, includes progress to next level.</p>
80+
</div>
81+
{/if}
82+
<div>
83+
<h3 class="font-bold text-text/85">
84+
Average Level without progress:
85+
<span class="text-text">
86+
{numberFormat(profile.skills.averageSkillLevel, defaultPatternDecimal)}
87+
</span>
88+
</h3>
89+
<p class="font-medium text-text/80">Average skill level without including partial level progress.</p>
7990
</div>
80-
{/if}
81-
<div>
82-
<h3 class="font-bold text-text/85">
83-
Average Level without progress:
84-
<span class="text-text">
85-
{numberFormat(profile.skills.averageSkillLevel, defaultPatternDecimal)}
86-
</span>
87-
</h3>
88-
<p class="font-medium text-text/80">Average skill level without including partial level progress.</p>
8991
</div>
90-
</div>
91-
</AdditionStat>
92-
{/if}
93-
{#if profile.fairySouls}
94-
<AdditionStat text="Fairy Souls" data={`${profile.fairySouls.found} / ${profile.fairySouls.total}`} maxed={(profile.fairySouls.found ?? 0) >= (profile.fairySouls.total ?? 0)} asterisk={true}>
95-
{calculatePercentage(profile.fairySouls.found ?? 0, profile.fairySouls.total ?? 0)}% of fairy souls found.
96-
</AdditionStat>
92+
</AdditionStat>
93+
{/if}
94+
{#if profile.fairySouls}
95+
<AdditionStat text="Fairy Souls" data={`${profile.fairySouls.found} / ${profile.fairySouls.total}`} maxed={(profile.fairySouls.found ?? 0) >= (profile.fairySouls.total ?? 0)} asterisk={true}>
96+
{calculatePercentage(profile.fairySouls.found ?? 0, profile.fairySouls.total ?? 0)}% of fairy souls found.
97+
</AdditionStat>
98+
{/if}
9799
{/if}
98-
99100
<svelte:boundary>
100101
{#snippet pending()}
101102
<div class="my-0 flex items-center gap-1 font-bold text-text/60">
@@ -132,13 +133,15 @@
132133
<Button.Root onclick={retry} class="text-icon hover:text-icon/80">Retry</Button.Root>
133134
</div>
134135
{/snippet}
135-
{@const networthData = await getNetworth({ uuid: profileUUID!, profileId: profileId! })}
136+
{#if profileUUID != null && profileId != null}
137+
{@const networthData = await getNetworth({ uuid: profileUUID, profileId: profileId })}
136138

137-
{#if networthData.normal}
138-
{@render NetworthSnippet(networthData.normal, "Networth")}
139-
{/if}
140-
{#if networthData.nonCosmetic}
141-
{@render NetworthSnippet(networthData.nonCosmetic, "Non-Cosmetic Networth")}
139+
{#if networthData.normal}
140+
{@render NetworthSnippet(networthData.normal, "Networth")}
141+
{/if}
142+
{#if networthData.nonCosmetic}
143+
{@render NetworthSnippet(networthData.nonCosmetic, "Non-Cosmetic Networth")}
144+
{/if}
142145
{/if}
143146
</svelte:boundary>
144147
</div>

src/lib/layouts/stats/Main.svelte

Lines changed: 55 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { replaceState } from "$app/navigation";
44
import { resolve } from "$app/paths";
55
import { page } from "$app/state";
6-
import { getHoverContext, setProfileContext } from "$ctx";
6+
import { getHoverContext, ProfileContext, setProfileContext } from "$ctx";
77
import Item from "$lib/components/Item.svelte";
88
import ItemContent from "$lib/components/item/item-content.svelte";
99
import Navbar from "$lib/components/Navbar.svelte";
@@ -14,13 +14,13 @@
1414
import Sections from "$lib/sections/Sections.svelte";
1515
import type { ModelsStatsOutput } from "$lib/shared/api/orval-generated";
1616
import { cn, flyAndScale } from "$lib/shared/utils";
17+
import { recentSearches } from "$lib/stores";
1718
import { itemContent, itemContentSpecial, showItem } from "$lib/stores/internal";
1819
import { performanceMode, showGlint } from "$lib/stores/preferences";
19-
import { recentSearches } from "$lib/stores/searches";
2020
import Image from "@lucide/svelte/icons/image";
2121
import { Avatar, Dialog } from "bits-ui";
2222
import { Pane } from "paneforge";
23-
import { tick, untrack } from "svelte";
23+
import { onDestroy, tick } from "svelte";
2424
import { cubicOut } from "svelte/easing";
2525
import { fade } from "svelte/transition";
2626
import { Drawer } from "vaul-svelte";
@@ -39,67 +39,70 @@
3939
let _defaultLeftPanel = $derived(Math.ceil((300 / innerWidth) * 100));
4040
let _defaultRightPanel = $derived(Math.ceil((700 / innerWidth) * 100));
4141
42-
// Initialize the profile context
43-
setProfileContext(ctx);
42+
const abortController = new AbortController();
4443
45-
// Update the profile context when the data changes
46-
$effect(() => {
47-
const abortController = new AbortController();
48-
// setProfileContext(ctx);
44+
// Initialize the profile context
45+
const profileClass = new ProfileContext();
46+
setProfileContext(profileClass);
4947
50-
recentSearches.update((searches) => {
51-
if (!ctx) return searches;
48+
function rewriteURL() {
49+
if (!(ctx as ModelsStatsOutput)) return;
5250
53-
const { username, uuid } = ctx;
54-
if (!username || !uuid) return searches;
51+
const { username, profile_cute_name } = ctx;
52+
if (!username) return;
5553
56-
// Find existing search by username/IGN and update with UUID
57-
const existingIndex = searches.findIndex((search) => search.ign.toLowerCase() === username.toLowerCase());
54+
const current = page.url.pathname;
55+
const wanted = `/stats/${username}/${profile_cute_name || ""}`;
5856
59-
if (existingIndex !== -1) {
60-
// Update existing search with UUID
61-
searches[existingIndex] = {
62-
...searches[existingIndex],
63-
uuid: uuid
64-
};
57+
// Update the URL to match the username and cute name
58+
if (current !== wanted) {
59+
// Only proceed if not aborted
60+
if (!abortController.signal.aborted) {
61+
tick()
62+
.then(() => {
63+
if (!abortController.signal.aborted) {
64+
replaceState(
65+
resolve("/stats/[ign]/[[profile]]", {
66+
ign: username,
67+
profile: profile_cute_name || ""
68+
}),
69+
page.state
70+
);
71+
}
72+
})
73+
.catch(() => {});
6574
}
75+
}
76+
}
6677
67-
return searches;
68-
});
78+
recentSearches.update((searches) => {
79+
if (!ctx) return searches;
6980
70-
untrack(() => {
71-
if (!(ctx as ModelsStatsOutput)) return;
81+
const { username, uuid } = ctx;
82+
if (!username || !uuid) return searches;
7283
73-
const { username, profile_cute_name } = ctx;
74-
if (!username) return;
84+
// Find existing search by username/IGN and update with UUID
85+
const existingIndex = searches.findIndex((search) => search.ign.toLowerCase() === username.toLowerCase());
7586
76-
const current = page.url.pathname;
77-
const wanted = `/stats/${username}/${profile_cute_name || ""}`;
87+
if (existingIndex !== -1) {
88+
// Update existing search with UUID
89+
searches[existingIndex] = {
90+
...searches[existingIndex],
91+
uuid: uuid
92+
};
93+
}
7894
79-
// Update the URL to match the username and cute name
80-
if (current !== wanted) {
81-
// Only proceed if not aborted
82-
if (!abortController.signal.aborted) {
83-
tick()
84-
.then(() => {
85-
if (!abortController.signal.aborted) {
86-
replaceState(
87-
resolve("/stats/[ign]/[[profile]]", {
88-
ign: username,
89-
profile: profile_cute_name || ""
90-
}),
91-
page.state
92-
);
93-
}
94-
})
95-
.catch(() => {});
96-
}
97-
}
98-
});
95+
return searches;
96+
});
97+
98+
// Update the profile context when the data changes
99+
$effect(() => {
100+
profileClass.current = profile;
101+
rewriteURL();
102+
});
99103
100-
return () => {
101-
abortController.abort();
102-
};
104+
onDestroy(() => {
105+
abortController.abort();
103106
});
104107
</script>
105108

0 commit comments

Comments
 (0)