feat: add github support, remove easter egg and declutter

This commit is contained in:
raymonable
2026-02-16 20:10:19 -05:00
parent 1b5601db45
commit c2ac01d940
25 changed files with 338 additions and 496 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
export let color: string = '179, 198, 255'
export let icon: string
export let href: string | undefined
export let href: string | undefined = undefined
export let isSmall: boolean = false
// Manually positioned icons
@@ -45,6 +45,7 @@
font-size: 1.2rem
display: block
margin-bottom: 0.5rem
color: color-mix(in oklab, rgb(var(--card-color)) 25%, rgba(255, 255, 255, 0.75))
.icon
position: absolute
@@ -0,0 +1,33 @@
<script lang="ts">
import { t } from "../libs/i18n";
const tabs: Record<string, string> = {
[t('home.nav.portal')]: `/home`,
[t('home.nav.link-card')]: `/cards`,
[t('home.nav.game-setup')]: `/setup`,
[t('home.nav.support')]: `/support`
}
</script>
<nav class="tabs">
{#each Object.entries(tabs) as t, i}
<a class="clickable"
href={t[1]}
class:active={
new URL(location.href).pathname === t[1]
}>
{t[0]}
</a>
{/each}
</nav>
<style lang="sass">
@use "../vars"
.tabs
display: flex
gap: 1rem
div
&.active
color: vars.$c-main
</style>
@@ -242,16 +242,6 @@
link.click();
}
function g(v: string) {
if (v != ("\x63\x68\x75\x6E\x69\x74\x68\x6D ").repeat(3).trim()) return;
const t = v.substring(5, 6) + v.substring(1, 2) + "eme";
if (!localStorage.getItem(t)) {
localStorage.setItem(t, v.substring(0, 1) + "\x6E");
} else
localStorage.removeItem(t);
setTimeout(location.reload, 1000); // ?
}
let DDSreader: DDS | undefined;
let USERBOX_PROGRESS = 0;
@@ -316,8 +306,7 @@
<StatusOverlays {error} loading={loading || !!submitting} />
{#if !loading && !error}
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
<h2>{t("userbox.header.general")}</h2>
<div>
<div class="general-options">
<GameSettingFields game="chu3"/>
@@ -477,10 +466,6 @@
input
width: 100%
h2
margin-bottom: 0.5rem
.general-options
display: flex
flex-direction: column
@@ -8,7 +8,7 @@
const rounding = useLocalStorage("rounding", true);
</script>
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
<div class="fields">
<blockquote class="info">
{ts("settings.siteNotice")}
</blockquote>
@@ -4,7 +4,7 @@
import { t } from "../../libs/i18n.js";
import Icon from "@iconify/svelte";
import StatusOverlays from "../StatusOverlays.svelte";
import { GAME } from "../../libs/sdk";
import { GAME, USER } from "../../libs/sdk";
import GameSettingFields from "./GameSettingFields.svelte";
import { download } from "../../libs/ui";
@@ -12,15 +12,15 @@
['name', t('settings.mai2.name')],
]
export let username: string;
let error: string
let submitting = ""
let values = Array(profileFields.length).fill('')
let changed: string[] = []
GAME.userSummary(username, 'mai2').then(({name}) => {
values = [name]
}).catch(e => error = e.message)
USER.me().then(me =>
GAME.userSummary(me.username, 'mai2').then(({name}) => {
values = [name]
}).catch(e => error = e.message));
function submit(field: string, value: string) {
if (submitting) return
@@ -112,14 +112,14 @@
}
try {
musicData = await fetch(`${DATA_HOST}/d/mai2/00/all-music.json`).then(res => res.json())
} catch (e) {
} catch (e: any) {
error = e.message;
submitting = ""
return;
}
try {
data = await GAME.export('mai2');
} catch (e) {
} catch (e: any) {
error = e.message;
submitting = ""
return;
@@ -208,7 +208,7 @@
}
</script>
<div class="fields" out:fade={FADE_OUT} in:fade={FADE_IN}>
<div class="fields">
{#each profileFields as [field, name], i (field)}
<div class="field">
<label for={field}>{name}</label>
@@ -4,6 +4,6 @@
import GameSettingFields from "./GameSettingFields.svelte";
</script>
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
<div>
<GameSettingFields game="ongeki"/>
</div>
@@ -0,0 +1,233 @@
<script lang="ts">
import { slide, fade } from "svelte/transition";
import type { AquaNetUser } from "../../libs/generalTypes";
import { CARD, USER } from "../../libs/sdk";
import StatusOverlays from "../../components/StatusOverlays.svelte";
import Icon from "@iconify/svelte";
import { pfp } from "../../libs/ui";
import { t, ts } from "../../libs/i18n";
import Cropper from "svelte-easy-crop";
let me: AquaNetUser;
let error: string;
let submitting = ""
let loading = false;
const profileFields = [
[ 'displayName', t('settings.profile.name') ],
[ 'username', t('settings.profile.username') ],
[ 'password', t('settings.profile.password') ],
[ 'profileBio', t('settings.profile.bio') ],
] as const
// Fetch user data
const getMe = () => {
loading = true;
USER.me().then((m) => {
if (pfpCropURL != null) {
URL.revokeObjectURL(pfpCropURL);
pfpField.value = "";
pfpCropURL = null;
}; me = m;
loading = false;
}).catch(e => error = e.message)
}
getMe();
let changed: string[] = []
let pfpField: HTMLInputElement
let pfpCropURL: string | null = null;
let pfpCrop = { width: 0, height: 0, x: 0, y: 0 };
function submit(field: string, value: string) {
if (submitting) return
submitting = field
USER.setting(field, value).then(() => {
changed = changed.filter(c => c !== field)
}).catch(e => error = e.message).finally(() => submitting = "")
}
function uploadPfp() {
if (submitting) return
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
const size = Math.round(Math.min(pfpCrop.width, pfpCrop.height, 1024));
canvas.width = size;
canvas.height = size;
let img = document.createElement("img");
img.onload = () => {
ctx?.drawImage(img, pfpCrop.x, pfpCrop.y, pfpCrop.width, pfpCrop.height, 0, 0, size, size);
canvas.toBlob(blob => {
if (!blob) return;
submitting = 'profilePicture'
USER.uploadPfp(blob as File).then(() => {
me.profilePicture = me.username
setTimeout(getMe, 200);
}).catch(e => error = e.message).finally(() => submitting = "")
});
}
img.src = pfpCropURL ?? "";
}
function handlePfpUpload(e: Event & { currentTarget: HTMLInputElement }) {
if (!e.target) return;
let files = e?.currentTarget?.files;
if (!files || files.length <= 0) return;
let file = files[0];
console.log(me.username, me);
switch (file.type) {
case "image/gif":
USER.uploadPfp(file).then(() => {
me.profilePicture = me.username
// reload
setTimeout(getMe, 200);
}).catch(e => error = e.message).finally(() => submitting = "")
break;
case "image/png":
case "image/jpeg":
case "image/webp":
pfpCropURL = URL.createObjectURL(file);
break;
default:
error = t("settings.profile.bad-format");
}
};
function logOut() {
localStorage.removeItem("token");
location.href = "/";
}
const passwordAction = (node: HTMLInputElement, whether: boolean) => {
if (whether) node.type = 'password'
}
</script>
<StatusOverlays {error} loading={!!submitting || loading} />
{#if !submitting && !error && me}
<div>
<div class="fields">
<div class="field">
<label for="profile-upload">{t('settings.profile.picture')}</label>
<div>
{#if me && me.profilePicture}
<div on:click={() => pfpField.click()} on:keydown={e => e.key === 'Enter' && pfpField.click()}
role="button" tabindex="0" class="clickable">
<img use:pfp={me} alt="Profile" />
</div>
{:else}
<button on:click={() => pfpField.click()}>
{t('settings.profile.upload-new')}
</button>
{/if}
</div>
<input id="profile-upload" type="file" accept="image/gif,image/png,image/jpeg,image/webp" hidden bind:this={pfpField}
on:change={handlePfpUpload} />
</div>
{#each profileFields as [field, name], i (field)}
<div class="field">
<label for={field}>{name}</label>
<div>
{#if field == "profileBio"}
<textarea id={field} bind:value={me[field]} on:input={() => changed = [...changed, field]} maxlength=255 placeholder={t('settings.profile.unset')}></textarea>
{:else}
<input id={field} type="text" use:passwordAction={field === 'password'}
bind:value={me[field]} on:input={() => changed = [...changed, field]}
placeholder={field === 'password' ? t('settings.profile.unchanged') : t('settings.profile.unset')}/>
{/if}
{#if changed.includes(field) && me[field]}
<button transition:slide={{axis: 'x'}} on:click={() => submit(field, me[field])}>
{#if submitting === field}
<Icon icon="line-md:loading-twotone-loop" />
{:else}
{t('settings.profile.save')}
{/if}
</button>
{/if}
</div>
</div>
{/each}
<div class="field m-t">
<div class="bool">
<input id="optOutOfLeaderboard" type="checkbox" bind:checked={me.optOutOfLeaderboard}
on:change={() => submit('optOutOfLeaderboard', me.optOutOfLeaderboard.toString())}/>
<label for="optOutOfLeaderboard">
<span class="name">{ts(`settings.fields.optOutOfLeaderboard.name`)}</span>
<span class="desc">{ts(`settings.fields.optOutOfLeaderboard.desc`)}</span>
</label>
</div>
</div>
<div class="field m-t">
<div>
<button on:click={logOut}>{ts(`settings.profile.logout`)}</button>
</div>
</div>
</div>
</div>
{/if}
{#if pfpCropURL != null}
<div class="overlay" transition:fade>
<div>
<div class="cropper-container">
<Cropper maxZoom={1e9} oncropcomplete={(e) => pfpCrop = e.pixels} image={pfpCropURL ?? "assets/imgs/no_profile.png"} aspect={1} cropShape="round"></Cropper>
</div>
<button on:click={uploadPfp}>
{t("settings.profile.save")}
</button>
<button on:click={getMe}>
{t("back")}
</button>
</div>
</div>
{/if}
<style lang="sass">
@use "../../vars"
.fields
display: flex
flex-direction: column
gap: 12px
.bool
display: flex
align-items: center
gap: 1rem
label
display: flex
flex-direction: column
.desc
opacity: 0.6
.field
display: flex
flex-direction: column
label
max-width: max-content
> div:not(.bool)
display: flex
align-items: center
gap: 1rem
margin-top: 0.5rem
> input, > textarea
flex: 1
img
max-width: 100px
max-height: 100px
border-radius: vars.$border-radius
object-fit: cover
aspect-ratio: 1
.cropper-container
position: relative
width: 400px
aspect-ratio: 1
</style>
@@ -4,6 +4,6 @@
import GameSettingFields from "./GameSettingFields.svelte";
</script>
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
<div>
<GameSettingFields game="wacca"/>
</div>
+12 -7
View File
@@ -10,15 +10,15 @@
<div class="overlay" transition:fade>
<div>
<h2 class="error">{t('status.error')}</h2>
{#if !expected}
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
{/if}
<span class="detail">{error}</span>
<a class="hint" href="/support">{t("status.error.hint")}</a>
<div class="actions">
<button on:click={() => location.reload()} class="error">
{t('action.refresh')}
</button>
<button on:click={() => location.href = "/home"} class="error">
{t('action.home')}
</button>
</div>
</div>
</div>
@@ -26,14 +26,19 @@
<style lang="sass">
.actions
display: flex
gap: 16px
gap: 0.5em
button
width: 100%
.detail
white-space: pre-line
font-size: 0.9em
line-height: 1.2
opacity: 0.8
.hint
font-size: 0.875em
.overlay > div
min-width: 15rem
max-width: 25rem
</style>