aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <github@matdoes.dev>2022-03-05 16:04:50 -0600
committermat <github@matdoes.dev>2022-03-05 16:04:50 -0600
commitf256a52e6a0a9ba478cde22d96c6292842247c01 (patch)
tree6b45a452cb6f6de45318b2b9839313599edbfd5c
parent478b6ea8a50bbf372921990775d3fd480446f350 (diff)
downloadskyblock-stats-f256a52e6a0a9ba478cde22d96c6292842247c01.tar.gz
skyblock-stats-f256a52e6a0a9ba478cde22d96c6292842247c01.tar.bz2
skyblock-stats-f256a52e6a0a9ba478cde22d96c6292842247c01.zip
mayors
-rw-r--r--src/lib/APITypes.d.ts25
-rw-r--r--src/lib/MayorSkin.svelte41
-rw-r--r--src/lib/utils.ts46
-rw-r--r--src/routes/election.svelte273
-rw-r--r--src/routes/index.svelte8
-rw-r--r--static/fonts/atkinson-hyperlegible/latin-bold.woff2bin0 -> 16836 bytes
-rw-r--r--static/fonts/atkinson-hyperlegible/latin-ext-bold.woff2bin0 -> 8548 bytes
-rw-r--r--static/fonts/atkinson-hyperlegible/latin-ext-italic-bold.woff2bin0 -> 8976 bytes
-rw-r--r--static/fonts/atkinson-hyperlegible/latin-ext-italic.woff2bin0 -> 8796 bytes
-rw-r--r--static/fonts/atkinson-hyperlegible/latin-ext.woff2bin0 -> 8424 bytes
-rw-r--r--static/fonts/atkinson-hyperlegible/latin-italic-bold.woff2bin0 -> 17808 bytes
-rw-r--r--static/fonts/atkinson-hyperlegible/latin-italic.woff2bin0 -> 17544 bytes
-rw-r--r--static/fonts/atkinson-hyperlegible/latin.woff2bin0 -> 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
new file mode 100644
index 0000000..6d8457a
--- /dev/null
+++ b/static/fonts/atkinson-hyperlegible/latin-bold.woff2
Binary files differ
diff --git a/static/fonts/atkinson-hyperlegible/latin-ext-bold.woff2 b/static/fonts/atkinson-hyperlegible/latin-ext-bold.woff2
new file mode 100644
index 0000000..b139a5a
--- /dev/null
+++ b/static/fonts/atkinson-hyperlegible/latin-ext-bold.woff2
Binary files differ
diff --git a/static/fonts/atkinson-hyperlegible/latin-ext-italic-bold.woff2 b/static/fonts/atkinson-hyperlegible/latin-ext-italic-bold.woff2
new file mode 100644
index 0000000..7490f67
--- /dev/null
+++ b/static/fonts/atkinson-hyperlegible/latin-ext-italic-bold.woff2
Binary files differ
diff --git a/static/fonts/atkinson-hyperlegible/latin-ext-italic.woff2 b/static/fonts/atkinson-hyperlegible/latin-ext-italic.woff2
new file mode 100644
index 0000000..e579847
--- /dev/null
+++ b/static/fonts/atkinson-hyperlegible/latin-ext-italic.woff2
Binary files differ
diff --git a/static/fonts/atkinson-hyperlegible/latin-ext.woff2 b/static/fonts/atkinson-hyperlegible/latin-ext.woff2
new file mode 100644
index 0000000..95cc0cf
--- /dev/null
+++ b/static/fonts/atkinson-hyperlegible/latin-ext.woff2
Binary files differ
diff --git a/static/fonts/atkinson-hyperlegible/latin-italic-bold.woff2 b/static/fonts/atkinson-hyperlegible/latin-italic-bold.woff2
new file mode 100644
index 0000000..1ac3456
--- /dev/null
+++ b/static/fonts/atkinson-hyperlegible/latin-italic-bold.woff2
Binary files differ
diff --git a/static/fonts/atkinson-hyperlegible/latin-italic.woff2 b/static/fonts/atkinson-hyperlegible/latin-italic.woff2
new file mode 100644
index 0000000..f5f51ea
--- /dev/null
+++ b/static/fonts/atkinson-hyperlegible/latin-italic.woff2
Binary files differ
diff --git a/static/fonts/atkinson-hyperlegible/latin.woff2 b/static/fonts/atkinson-hyperlegible/latin.woff2
new file mode 100644
index 0000000..3f1bcc8
--- /dev/null
+++ b/static/fonts/atkinson-hyperlegible/latin.woff2
Binary files differ