aboutsummaryrefslogtreecommitdiff
path: root/src/routes/auctionprices/+page.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'src/routes/auctionprices/+page.svelte')
-rw-r--r--src/routes/auctionprices/+page.svelte198
1 files changed, 198 insertions, 0 deletions
diff --git a/src/routes/auctionprices/+page.svelte b/src/routes/auctionprices/+page.svelte
new file mode 100644
index 0000000..844af04
--- /dev/null
+++ b/src/routes/auctionprices/+page.svelte
@@ -0,0 +1,198 @@
+<script lang="ts">
+ import Header from '$lib/Header.svelte'
+ import Head from '$lib/Head.svelte'
+ import { cleanId, removeFormattingCode, toTitleCase, type PreviewedAuctionData } from '$lib/utils'
+ import type { ItemAuctionsSchema } from '$lib/APITypes'
+ import AuctionPriceScatterplot from '$lib/AuctionPriceScatterplot.svelte'
+ import AuctionPreviewTooltip from '$lib/AuctionPreviewTooltip.svelte'
+ 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: PageData
+ let auctionPrices: ItemAuctionsSchema[] = data.prices
+ let auctionItems: Record<string, { display: { name: string }; vanillaId?: string }> = data.items
+
+ let currentlyPreviewedAuction: PreviewedAuctionData | null = null
+
+ let query: string = ''
+
+ $: queryNormalized = query.toLowerCase()
+
+ let allMatchingItemIds: string[]
+ $: {
+ pageNumber = 0
+ allMatchingItemIds = Object.entries(auctionItems)
+ .filter(a => removeFormattingCode(a[1]?.display.name.toLowerCase()).includes(queryNormalized))
+ .map(a => a[0])
+ }
+ $: {
+ if (browser) fetchAndSetItems(allMatchingItemIds.slice(0, 100))
+ }
+
+ async function fetchAndSetItems(itemIds: string[]) {
+ const localQuery = query
+ const localData = await fetchItems(query.length > 0 ? itemIds : null)
+ // if the query hasn't changed, update the data
+ if (query === localQuery) data = localData
+ }
+ async function fetchItems(itemIds: null | string[]): Promise<ItemAuctionsSchema[]> {
+ let url = `auctionprices`
+ if (itemIds !== null) {
+ if (itemIds.length === 0) return []
+ url += `?items=${itemIds.join(',')}`
+ }
+ return await fetchApi(url, fetch).then(r => r.json())
+ }
+
+ let pageHeight = 0
+ $: {
+ pageHeight = 0
+ }
+
+ // 0 indexed
+ let pageNumber = 0
+ let loadingPage = false
+
+ async function checkScroll() {
+ if (loadingPage) return
+
+ let pageHeightTemp = window.scrollY + window.innerHeight
+ if (pageHeightTemp <= pageHeight) return
+ pageHeight = pageHeightTemp
+ if (pageHeight >= document.body.scrollHeight - 1000) {
+ loadingPage = true
+ pageNumber++
+ const itemIds = allMatchingItemIds.slice(pageNumber * 100, (pageNumber + 1) * 100)
+ if (itemIds.length > 0) {
+ const shownIds = data.map(d => d.id)
+ const items = (await fetchItems(itemIds)).filter(i => !shownIds.includes(i.id))
+ if (items.length > 0) data = [...data, ...items]
+ }
+ loadingPage = false
+ }
+ }
+
+ $: {
+ if (browser && !currentlyPreviewedAuction) {
+ for (const el of document.getElementsByClassName('selected-auction'))
+ el.classList.remove('selected-auction')
+ }
+ }
+</script>
+
+<Head
+ title="SkyBlock Auction Prices"
+ description="View how many coins every SkyBlock item costs in the auction house."
+/>
+<Header />
+
+<svelte:window on:scroll={checkScroll} />
+
+<AuctionPreviewTooltip bind:preview={currentlyPreviewedAuction} />
+
+<main>
+ <h1>SkyBlock Auction Prices</h1>
+ <div class="filter-items-settings">
+ <input type="text" id="filter-items-tier" placeholder="Search..." bind:value={query} />
+ </div>
+ <div class="item-list">
+ {#each data as item (item.id)}
+ {@const binAuctions = item.auctions.filter(i => i.bin)}
+ {@const normalAuctions = item.auctions.filter(i => !i.bin)}
+ {@const itemData = auctionItems[item.sbId]}
+ <div class="item-container">
+ {#if itemData && itemData.vanillaId}
+ <div class="item-slot-container">
+ <Item item={{ ...itemData, id: item.id }} pack={furfskyReborn} headSize={50} isslot />
+ </div>
+ {/if}
+ <h2>
+ {removeFormattingCode(
+ auctionItems[item.id]?.display.name ?? toTitleCase(cleanId(item.id.toLowerCase()))
+ )}
+ </h2>
+ <div class="auctions-info-text">
+ {#if binAuctions.length > 0}
+ <p>
+ Cheapest recent BIN: <b>
+ {binAuctions.reduce((a, b) => (a.coins < b.coins ? a : b)).coins.toLocaleString()} coins
+ </b>
+ </p>
+ {/if}
+ {#if normalAuctions.length > 0}
+ <p>
+ Cheapest recent auction: <b>
+ {normalAuctions
+ .reduce((a, b) => (a.coins < b.coins ? a : b))
+ .coins.toLocaleString()} coins
+ </b>
+ </p>
+ {/if}
+ {#if item.auctions.length >= 2}
+ <p>
+ Median:
+ <b>
+ {[...item.auctions]
+ .sort((a, b) => a.coins - b.coins)
+ [Math.floor(item.auctions.length / 2)].coins.toLocaleString()} coins
+ </b>
+ </p>
+ <p>
+ Volume:
+ <b>
+ {parseFloat(
+ (
+ (24 * 60 * 60) /
+ ((Date.now() / 1000 - item.auctions[0].ts) / item.auctions.length)
+ ).toPrecision(2)
+ ).toLocaleString()}/day
+ </b>
+ </p>
+ {/if}
+ </div>
+ <div class="item-scatterplot">
+ <AuctionPriceScatterplot {item} bind:currentlyPreviewedAuction />
+ </div>
+ </div>
+ {/each}
+ {#if data.length === 0}
+ No results
+ {/if}
+ </div>
+</main>
+
+<style>
+ p {
+ margin: 0;
+ }
+ .item-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
+ grid-gap: 1em;
+ margin-top: 1em;
+ }
+ .item-container {
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ background: rgba(0, 0, 0, 0.1);
+ padding: 0.75em;
+ border-radius: 1em;
+ }
+
+ .item-scatterplot {
+ margin-top: 1em;
+ }
+
+ .auctions-info-text {
+ color: var(--theme-darker-text);
+ }
+ .auctions-info-text b {
+ color: var(--theme-main-text);
+ }
+
+ .item-slot-container {
+ float: right;
+ }
+</style>