diff options
-rw-r--r-- | src/lib/APITypes.d.ts | 25 | ||||
-rw-r--r-- | src/lib/MayorSkin.svelte | 41 | ||||
-rw-r--r-- | src/lib/utils.ts | 46 | ||||
-rw-r--r-- | src/routes/election.svelte | 273 | ||||
-rw-r--r-- | src/routes/index.svelte | 8 | ||||
-rw-r--r-- | static/fonts/atkinson-hyperlegible/latin-bold.woff2 | bin | 0 -> 16836 bytes | |||
-rw-r--r-- | static/fonts/atkinson-hyperlegible/latin-ext-bold.woff2 | bin | 0 -> 8548 bytes | |||
-rw-r--r-- | static/fonts/atkinson-hyperlegible/latin-ext-italic-bold.woff2 | bin | 0 -> 8976 bytes | |||
-rw-r--r-- | static/fonts/atkinson-hyperlegible/latin-ext-italic.woff2 | bin | 0 -> 8796 bytes | |||
-rw-r--r-- | static/fonts/atkinson-hyperlegible/latin-ext.woff2 | bin | 0 -> 8424 bytes | |||
-rw-r--r-- | static/fonts/atkinson-hyperlegible/latin-italic-bold.woff2 | bin | 0 -> 17808 bytes | |||
-rw-r--r-- | static/fonts/atkinson-hyperlegible/latin-italic.woff2 | bin | 0 -> 17544 bytes | |||
-rw-r--r-- | static/fonts/atkinson-hyperlegible/latin.woff2 | bin | 0 -> 16464 bytes |
13 files changed, 379 insertions, 14 deletions
diff --git a/src/lib/APITypes.d.ts b/src/lib/APITypes.d.ts index 119e5b6..3a54cea 100644 --- a/src/lib/APITypes.d.ts +++ b/src/lib/APITypes.d.ts @@ -138,3 +138,28 @@ export interface Collection { level: number category: CollectionCategory } + +export interface MayorPerk { + name: string + description: string +} + +export interface Candidate { + name: string + perks: MayorPerk[] + votes: number + color: string | null +} + +export interface ElectionData { + last_updated: number + previous: { + year: number + winner: string + candidates: Candidate[] + } + current: { + year: number + candidates: Candidate[] + } | null +}
\ No newline at end of file diff --git a/src/lib/MayorSkin.svelte b/src/lib/MayorSkin.svelte new file mode 100644 index 0000000..c3a90ec --- /dev/null +++ b/src/lib/MayorSkin.svelte @@ -0,0 +1,41 @@ +<script lang="ts"> + import { toTitleCase } from '$lib/utils' + export let name: string + + const skinIds = { + // normal mayors + barry: 'f04c591b164746e848f3d6a451ee87a62dd193e5c45e94ed78e72df119aca426', + paul: '1b59c43d8dbccfd7ec6e6394b6304b70d4ed315add0494ee77c733f41818c73a', + aatrox: 'c1bdf505bb8c0f1f3365a03032de1931663ff71c57e022558de312b8f1b5c445', + foxy: '3485a717fa0f51d7fadc66a5d5e9853905bef914e3b2848a2f128e63d2db87', + cole: '16422de08848952d1cbead66bbbad6f07191bdcc952f3d1036aeb0c22938f39b', + marina: '807fc9bee8d3344e840e4031a37249a4c3c87fc80cf16432cc5c2153d1f9c53d', + diaz: '9cf4737cd444b590545734a6408cbe23c182f4283f167a3e3c09532ccbef17f9', + diana: '83cc1cf672a4b2540be346ead79ac2d9ed19d95b6075bf95be0b6d0da61377be', + + // special mayors + derpy: 'be0f89466528ad5eca5a6506adddd896ff78c4fd21facaa74a8c4a809c89207', + scorpius: '8f26fa0c47536e78e337257d898af8b1ebc87c0894503375234035ff2c7ef8f0', + + // unique mayors + technoblade: '786c039d969d1839155255e38e7b06a626ea9f8baf9cb55e0a77311efe18a3e', + dante: '5af658e00ac0d0ce0686e79f59c067b9577c01ba57ad8c6575db8490c3161772', + faith: '64b39d0756b92b8b7599d1f971580088954e21c5f60c673d0d4f63693fb002b5', + } + + let url: string + $: { + if (name.toLowerCase() === 'derpy') url = '/villager.png' + else url = `https://mc-heads.net/body/${skinIds[name.toLowerCase()]}` + } +</script> + +<img src={url} alt="Mayor {toTitleCase(name)}" /> + +<style> + img { + display: block; + width: 5em; + margin: 0 auto; + } +</style> diff --git a/src/lib/utils.ts b/src/lib/utils.ts index aabd981..d6e8e13 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -81,28 +81,40 @@ function moveToEndOfId(word: string, thing: string) { return thing } -export function millisecondsToTime(totalMilliseconds: number) { +interface MillisecondsToTimeOpts { + parts: number +} + +export function millisecondsToTime(totalMilliseconds: number, opts: MillisecondsToTimeOpts = { parts: 2 }) { const totalSeconds = totalMilliseconds / 1000 const totalMinutes = totalSeconds / 60 const totalHours = totalMinutes / 60 const totalDays = totalHours / 24 const milliseconds = Math.floor(totalMilliseconds) % 1000 + const seconds = Math.floor(totalSeconds) % 60 const minutes = Math.floor(totalMinutes) % 60 const hours = Math.floor(totalHours) % 24 const days = Math.floor(totalDays) + const stringUnits: string[] = [] - if (days > 1) stringUnits.push(`${days} days`) - else if (days == 1) stringUnits.push(`${days} day`) - if (hours > 1) stringUnits.push(`${hours} hours`) - else if (hours == 1) stringUnits.push(`${hours} hour`) - if (minutes > 1) stringUnits.push(`${minutes} minutes`) - else if (minutes == 1) stringUnits.push(`${minutes} minute`) - if (seconds > 1) stringUnits.push(`${seconds} seconds`) - else if (seconds == 1) stringUnits.push(`${seconds} second`) - if (milliseconds > 1) stringUnits.push(`${milliseconds} milliseconds`) - else if (milliseconds == 1) stringUnits.push(`${milliseconds} millisecond`) - return stringUnits.slice(0, 2).join(' and ') + + if (totalDays > 1) stringUnits.push(`${days} days`) + else if (totalDays == 1) stringUnits.push(`${days} day`) + if (totalHours > 1) stringUnits.push(`${hours} hours`) + else if (totalHours == 1) stringUnits.push(`${hours} hour`) + if (totalMinutes > 1) stringUnits.push(`${minutes} minutes`) + else if (totalMinutes == 1) stringUnits.push(`${minutes} minute`) + if (totalSeconds > 1) stringUnits.push(`${seconds} seconds`) + else if (totalSeconds == 1) stringUnits.push(`${seconds} second`) + if (totalMilliseconds > 1) stringUnits.push(`${milliseconds} milliseconds`) + else if (totalMilliseconds == 1) stringUnits.push(`${milliseconds} millisecond`) + // comma separated, "and" before last + + let partsCount = opts.parts + if (stringUnits.length < opts.parts) partsCount = stringUnits.length + if (partsCount === 1) return stringUnits[0] + return stringUnits.slice(0, partsCount - 1).join(', ') + ' and ' + stringUnits[partsCount - 1] } export function cleanId(id: string) { @@ -171,4 +183,14 @@ export function formatNumberFromUnit(n: number, unit: null | 'date' | 'time' | s default: return `${n.toLocaleString()} ${unit}` } +} + +/** Get the milliseconds since epoch for a given SkyBlock date. The year, month, and day are 1 indexed. */ +export function skyblockTime(year: number, month = 1, day = 1) { + const sbEpoch = 1560275700000 + let time = sbEpoch + if (year) time += 446400000 * (year) + if (month) time += 37200000 * (month - 1) + if (day) time += 1200000 * (day - 1) + return time }
\ No newline at end of file diff --git a/src/routes/election.svelte b/src/routes/election.svelte new file mode 100644 index 0000000..69f4812 --- /dev/null +++ b/src/routes/election.svelte @@ -0,0 +1,273 @@ +<script lang="ts" context="module"> + import type { Load } from '@sveltejs/kit' + import { API_URL } from '$lib/api' + + export const load: Load = async ({ params, fetch }) => { + const data = await fetch(`${API_URL}election`).then(r => r.json()) + + return { + props: { + data, + }, + } + } +</script> + +<script lang="ts"> + import Header from '$lib/Header.svelte' + import Head from '$lib/Head.svelte' + import { + cleanId, + 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 { invalidate } from '$app/navigation' + import { browser } from '$app/env' + + export let data: ElectionData + + let destroyed = false + + let currentTime = Date.now() + // update currentTime every animation frame + function updateTime() { + currentTime = Date.now() + if (!destroyed) requestAnimationFrame(updateTime) + } + + $: nextSpecialMayorYear = Math.ceil(((data.current?.year || data.previous.year) + 1) / 8) * 8 + const specialMayors = ['Scorpius', 'Derpy', 'Jerry'] + + // invalidate at the end of every minute + async function autoInvalidate(first: boolean) { + if (browser && !destroyed) { + // don't invalidate the first time the function is called + if (!first) await invalidate('') + + const lastUpdatedAgo = Date.now() - data.last_updated * 1000 + setTimeout(() => autoInvalidate(false), lastUpdatedAgo + 10 * 60 * 1000) + } + } + + autoInvalidate(true) + + onMount(() => { + destroyed = false + updateTime() + }) + + onDestroy(() => { + destroyed = true + }) +</script> + +<Head title="SkyBlock Mayor Election Status" /> +<Header /> + +<main> + <div class="next-mayor-update"> + <p> + <b>Last API update:</b> + {millisecondsToTime(currentTime - data.last_updated * 1000)} + </p> + <p> + <b>Next API update:</b> + {millisecondsToTime(currentTime - data.last_updated * 1000 + 10 * 60 * 1000)} + </p> + </div> + <h1>SkyBlock Mayor Election Status</h1> + <p> + <b>Next election:</b> + {millisecondsToTime(skyblockTime(data.previous.year + 1, 6, 27) - currentTime, { parts: 3 })} + </p> + + {#if data.current} + <h2>Ongoing election <span class="candidate-year">(Year {data.current.year})</span></h2> + <p class="election-ends-in-text"> + <b>Ends in:</b> + {millisecondsToTime(skyblockTime(data.previous.year + 1, 3, 27) - Date.now())} + </p> + <div class="mayor-candidates"> + {#each data.current.candidates.sort((a, b) => a.votes - b.votes) as candidate} + {@const color = candidate.color ? colorCodes[candidate.color] : null} + <div class="mayor"> + <div> + <h3 style={color ? `color: ${color}` : undefined}>{candidate.name}</h3> + <p class="mayor-vote-count"> + <span class="mayor-vote-count-number" style={color ? `color: ${color}` : undefined}> + {candidate.votes.toLocaleString()} + </span> votes + </p> + <MayorSkin name={candidate.name} /> + </div> + <ul class="mayor-perks"> + {#each candidate.perks as perk} + <div class="mayor-perk"> + <h4 style={color ? `color: ${color}` : undefined}>{perk.name}</h4> + <p>{@html formattingCodeToHtml(perk.description)}</p> + </div> + {/each} + </ul> + </div> + {/each} + </div> + {/if} + + <!-- +<h2>Previous election <span class="candidate-year">({{ skyblockYear(data.previous.year) }})</span></h2> +<div class="mayor-candidates"> + {% for candidate in data.previous.candidates|sort(true, false, 'votes') %} + <div class="mayor"> + {% set color = colorCodes[candidate.color] if candidate.color else null %} + <div> + <h3{% if color %} style="color: {{color}}"{% endif %}>{{candidate.name}}</h3> + <p class="mayor-vote-count"><span class="mayor-vote-count-number"{% if color %} style="color: {{color}}"{% endif %}>{{candidate.votes.toLocaleString()}}</span> votes</p> + {% if candidate.name == data.previous.winner %} + <p class="mayor-winner">Winner</p> + {% endif %} + {{ renderMayor(candidate.name) }} + </div> + <ul class="mayor-perks"> + {% for perk in candidate.perks %} + <div class="mayor-perk"> + <h4{% if color %} style="color: {{ color }}"{% endif %}>{{ perk.name }}</h4> + <p>{{ perk.description|formattingCodeToHtml|safe }}</p> + </div> + {% endfor %} + </ul> + </div> + {% endfor %} +</div> --> + + <h2> + Previous election <span class="candidate-year">(Year {data.previous.year})</span> + </h2> + <div class="mayor-candidates"> + {#each data.previous.candidates.sort((a, b) => b.votes - a.votes) as candidate} + {@const color = candidate.color ? colorCodes[candidate.color] : null} + <div class="mayor"> + <div> + <h3 style={color ? `color: ${color}` : undefined}>{candidate.name}</h3> + <p class="mayor-vote-count"> + <span class="mayor-vote-count-number" style={color ? `color: ${color}` : undefined}> + {candidate.votes.toLocaleString()} + </span> votes + </p> + {#if candidate.name == data.previous.winner} + <p class="mayor-winner">Winner</p> + {/if} + <MayorSkin name={candidate.name} /> + </div> + <ul class="mayor-perks"> + {#each candidate.perks as perk} + <div class="mayor-perk"> + <h4 style={color ? `color: ${color}` : undefined}>{perk.name}</h4> + <p>{@html formattingCodeToHtml(perk.description)}</p> + </div> + {/each} + </ul> + </div> + {/each} + </div> + + <!-- {%- set nextSpecialMayorYear = (((data.current.year or data.previous.year) + 1) / 8)|round(0, 'ceil') * 8 -%} + {%- set specialMayors = ['Scorpius', 'Derpy', 'Jerry'] -%} + <h2>Upcoming special mayors</h2> + <ul> + {%- for i in range(3) -%} + <li><b>{{ specialMayors[((nextSpecialMayorYear / 8) + i) % 3] }}</b> <span class="next-special-mayor-time">({{ skyblockYear(nextSpecialMayorYear + 8 * i, 6, 27) }})</span></li> + {%- endfor -%} + </ul> --> + + <h2>Upcoming special mayors</h2> + <ul> + <!-- for i in range(3) --> + {#each Array(3) as _, i} + <li> + <b>{specialMayors[(nextSpecialMayorYear / 8 + i) % 3]}</b> + <span class="next-special-mayor-time"> + ({millisecondsToTime(skyblockTime(nextSpecialMayorYear + 8 * i + 1, 3, 27) - Date.now())}) + </span> + </li> + {/each} + </ul> +</main> + +<style> + .mayor-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-column-gap: 1em; + grid-row-gap: 1em; + margin-top: 1em; + margin-bottom: 1em; + } + .mayor { + display: inline-block; + width: 12em; + margin-right: 1em; + vertical-align: top; + } + .mayor h3 { + filter: brightness(1.5); + text-align: center; + } + .mayor img { + display: block; + width: 5em; + margin: 0 auto; + } + .mayor-vote-count-number { + filter: brightness(1.2); + } + .mayor-vote-count { + text-align: center; + width: 100%; + margin: 0 0 0.5em 0; + } + .mayor-perk h4 { + margin-bottom: 0; + } + .mayor-perk p { + margin: 0; + } + .mayor-candidates { + /* display: ; */ + /* everything next to each other */ + /* grid-template-columns: repeat(5, 1fr); */ + max-width: fit-content; + } + .mayor-perks { + list-style-type: none; + padding: 0; + } + .candidate-year { + font-weight: normal; + color: var(--theme-darker-text); + } + .mayor-winner { + color: var(--theme-yellow); + font-weight: bold; + margin: 0; + text-align: center; + font-size: 1em; + } + .next-special-mayor-time { + color: var(--theme-darker-text); + } + .election-ends-in-text { + margin-top: 0; + } + .next-mayor-update { + float: right; + color: var(--theme-darker-text); + } + .next-mayor-update p { + margin: 0; + } +</style> diff --git a/src/routes/index.svelte b/src/routes/index.svelte index 086c7fb..6f6bae1 100644 --- a/src/routes/index.svelte +++ b/src/routes/index.svelte @@ -30,8 +30,8 @@ <hr style="margin: 25vh 0 2em 0" /> <section> - <h3>Other SkyBlock tools</h3> - <ul> + <h2>Other SkyBlock tools</h2> + <ul id="other-tools-list"> <li><a>Auction prices (coming soon)</a></li> <li><a href="/leaderboards">Leaderboards</a></li> <li><a>Bazaar (coming soon)</a></li> @@ -88,4 +88,8 @@ #donators p { margin: 0; } + + li { + padding: 0.2em 0; + } </style> diff --git a/static/fonts/atkinson-hyperlegible/latin-bold.woff2 b/static/fonts/atkinson-hyperlegible/latin-bold.woff2 Binary files differnew file mode 100644 index 0000000..6d8457a --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/latin-bold.woff2 diff --git a/static/fonts/atkinson-hyperlegible/latin-ext-bold.woff2 b/static/fonts/atkinson-hyperlegible/latin-ext-bold.woff2 Binary files differnew file mode 100644 index 0000000..b139a5a --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/latin-ext-bold.woff2 diff --git a/static/fonts/atkinson-hyperlegible/latin-ext-italic-bold.woff2 b/static/fonts/atkinson-hyperlegible/latin-ext-italic-bold.woff2 Binary files differnew file mode 100644 index 0000000..7490f67 --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/latin-ext-italic-bold.woff2 diff --git a/static/fonts/atkinson-hyperlegible/latin-ext-italic.woff2 b/static/fonts/atkinson-hyperlegible/latin-ext-italic.woff2 Binary files differnew file mode 100644 index 0000000..e579847 --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/latin-ext-italic.woff2 diff --git a/static/fonts/atkinson-hyperlegible/latin-ext.woff2 b/static/fonts/atkinson-hyperlegible/latin-ext.woff2 Binary files differnew file mode 100644 index 0000000..95cc0cf --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/latin-ext.woff2 diff --git a/static/fonts/atkinson-hyperlegible/latin-italic-bold.woff2 b/static/fonts/atkinson-hyperlegible/latin-italic-bold.woff2 Binary files differnew file mode 100644 index 0000000..1ac3456 --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/latin-italic-bold.woff2 diff --git a/static/fonts/atkinson-hyperlegible/latin-italic.woff2 b/static/fonts/atkinson-hyperlegible/latin-italic.woff2 Binary files differnew file mode 100644 index 0000000..f5f51ea --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/latin-italic.woff2 diff --git a/static/fonts/atkinson-hyperlegible/latin.woff2 b/static/fonts/atkinson-hyperlegible/latin.woff2 Binary files differnew file mode 100644 index 0000000..3f1bcc8 --- /dev/null +++ b/static/fonts/atkinson-hyperlegible/latin.woff2 |