mirror of https://github.com/hykilpikonna/AquaDX
implement memorial photo viewer (#119)
* commit current progress\ will prob work on my mac ltr * more transferring to different device * grammar * [F] Fix warning inconsistency * [O] Split status overlays * [S] Better styling * [+] i18n * [+] Display photos tab conditionally --------- Co-authored-by: Azalea <22280294+hykilpikonna@users.noreply.github.com>v1-dev
parent
6c21afaa57
commit
eef40e39d1
Binary file not shown.
|
@ -4,9 +4,10 @@
|
||||||
import UserHome from "./pages/UserHome.svelte";
|
import UserHome from "./pages/UserHome.svelte";
|
||||||
import Home from "./pages/Home.svelte";
|
import Home from "./pages/Home.svelte";
|
||||||
import Ranking from "./pages/Ranking.svelte";
|
import Ranking from "./pages/Ranking.svelte";
|
||||||
import { USER } from "./libs/sdk";
|
import { CARD, USER } from "./libs/sdk";
|
||||||
import type { AquaNetUser } from "./libs/generalTypes";
|
import type { AquaNetUser } from "./libs/generalTypes";
|
||||||
import Settings from "./pages/User/Settings.svelte";
|
import Settings from "./pages/User/Settings.svelte";
|
||||||
|
import MaiPhoto from "./pages/MaiPhoto.svelte";
|
||||||
import { pfp, tooltip } from "./libs/ui"
|
import { pfp, tooltip } from "./libs/ui"
|
||||||
import { ANNOUNCEMENT } from "./libs/config";
|
import { ANNOUNCEMENT } from "./libs/config";
|
||||||
import { t } from "./libs/i18n";
|
import { t } from "./libs/i18n";
|
||||||
|
@ -25,9 +26,18 @@
|
||||||
|
|
||||||
export let url = "";
|
export let url = "";
|
||||||
let me: AquaNetUser
|
let me: AquaNetUser
|
||||||
|
let playedMai = false
|
||||||
|
|
||||||
if (USER.isLoggedIn()) USER.me().then(m => me = m).catch(e => console.error(e))
|
if (USER.isLoggedIn())
|
||||||
|
{
|
||||||
|
USER.me().then(m => {
|
||||||
|
me = m
|
||||||
|
CARD.userGames(me.username).then(game => {
|
||||||
|
playedMai = !!game.mai2
|
||||||
|
})
|
||||||
|
}).catch(e => console.error(e))
|
||||||
|
|
||||||
|
}
|
||||||
let path = window.location.pathname;
|
let path = window.location.pathname;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -44,11 +54,14 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<a href="/home">{t('navigation.home').toLowerCase()}</a>
|
<a href="/home">{t('navigation.home').toLowerCase()}</a>
|
||||||
<div on:click={() => alert("Coming soon™")} on:keydown={e => e.key === "Enter" && alert("Coming soon™")}
|
<!-- <div on:click={() => alert("Coming soon™")} on:keydown={e => e.key === "Enter" && alert("Coming soon™")}
|
||||||
role="button" tabindex="0">{t('navigation.maps').toLowerCase()}</div>
|
role="button" tabindex="0">{t('navigation.maps').toLowerCase()}</div> -->
|
||||||
<a href="/ranking">{t('navigation.rankings').toLowerCase()}</a>
|
<a href="/ranking">{t('navigation.rankings').toLowerCase()}</a>
|
||||||
|
{#if playedMai}
|
||||||
|
<a href="/pictures">photo</a>
|
||||||
|
{/if}
|
||||||
{#if me}
|
{#if me}
|
||||||
<a href="/u/{me.username}" use:tooltip={t('navigation.profile')}>
|
<a href="/u/{me.username}" use:tooltip={t('navigation.profile')}>
|
||||||
<img alt="profile" class="pfp" use:pfp={me}/>
|
<img alt="profile" class="pfp" use:pfp={me}/>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -62,6 +75,7 @@
|
||||||
<Route path="/u/:username" component={UserHome} />
|
<Route path="/u/:username" component={UserHome} />
|
||||||
<Route path="/u/:username/:game" component={UserHome} />
|
<Route path="/u/:username/:game" component={UserHome} />
|
||||||
<Route path="/settings" component={Settings} />
|
<Route path="/settings" component={Settings} />
|
||||||
|
<Route path="/pictures" component={MaiPhoto} />
|
||||||
</Router>
|
</Router>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
|
|
|
@ -1,87 +1,54 @@
|
||||||
<!-- Svelte 4.2.11 -->
|
<!-- Svelte 4.2.11 -->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from 'svelte/transition'
|
||||||
import type { ConfirmProps } from "../libs/generalTypes";
|
import type { ConfirmProps } from "../libs/generalTypes";
|
||||||
import { DISCORD_INVITE } from "../libs/config";
|
import { DISCORD_INVITE } from "../libs/config";
|
||||||
import Icon from "@iconify/svelte";
|
import { t } from "../libs/i18n"
|
||||||
import { t } from "../libs/i18n"
|
import Loading from './ui/Loading.svelte';
|
||||||
|
import Error from './ui/Error.svelte';
|
||||||
// Props
|
|
||||||
export let confirm: ConfirmProps | null = null
|
// Props
|
||||||
export let error: string | null
|
export let confirm: ConfirmProps | null = null
|
||||||
export let loading: boolean = false
|
export let error: string | null
|
||||||
</script>
|
export let loading: boolean = false
|
||||||
|
</script>
|
||||||
{#if confirm}
|
|
||||||
<div class="overlay" transition:fade>
|
{#if confirm}
|
||||||
<div>
|
<div class="overlay" transition:fade>
|
||||||
<h2>{confirm.title}</h2>
|
<div>
|
||||||
<span>{confirm.message}</span>
|
<h2>{confirm.title}</h2>
|
||||||
|
<span>{confirm.message}</span>
|
||||||
<div class="actions">
|
|
||||||
{#if confirm.cancel}
|
<div class="actions">
|
||||||
<!-- Svelte LSP is very annoying here -->
|
{#if confirm.cancel}
|
||||||
<button on:click={() => {
|
<!-- Svelte LSP is very annoying here -->
|
||||||
confirm && confirm.cancel && confirm.cancel()
|
<button on:click={() => {
|
||||||
|
confirm && confirm.cancel && confirm.cancel()
|
||||||
// Set to null
|
|
||||||
confirm = null
|
// Set to null
|
||||||
}}>{t('action.cancel')}</button>
|
confirm = null
|
||||||
{/if}
|
}}>{t('action.cancel')}</button>
|
||||||
<button on:click={() => confirm && confirm.confirm()} class:error={confirm.dangerous}>{t('action.confirm')}</button>
|
{/if}
|
||||||
</div>
|
<button on:click={() => confirm && confirm.confirm()} class:error={confirm.dangerous}>{t('action.confirm')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
|
{/if}
|
||||||
{#if error}
|
|
||||||
<div class="overlay" transition:fade>
|
{#if error}
|
||||||
<div>
|
<Error {error}/>
|
||||||
<h2 class="error">{t('status.error')}</h2>
|
{/if}
|
||||||
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
|
|
||||||
<span>{t('status.detail', { detail: error })}</span>
|
{#if loading && !error}
|
||||||
|
<Loading/>
|
||||||
<div class="actions">
|
{/if}
|
||||||
<button on:click={() => location.reload()} class="error">
|
|
||||||
{t('action.refresh')}
|
<style lang="sass">
|
||||||
</button>
|
.actions
|
||||||
</div>
|
display: flex
|
||||||
</div>
|
gap: 16px
|
||||||
</div>
|
|
||||||
{/if}
|
button
|
||||||
|
width: 100%
|
||||||
{#if loading && !error}
|
</style>
|
||||||
<div class="overlay loading" transition:fade>
|
|
||||||
<Icon class="icon" icon="svg-spinners:pulse-2"/>
|
|
||||||
<span><span>LOADING</span></span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="sass">
|
|
||||||
.actions
|
|
||||||
display: flex
|
|
||||||
gap: 16px
|
|
||||||
|
|
||||||
button
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
.loading.overlay
|
|
||||||
font-size: 28rem
|
|
||||||
|
|
||||||
:global(.icon)
|
|
||||||
opacity: 0.5
|
|
||||||
|
|
||||||
> span
|
|
||||||
position: absolute
|
|
||||||
inset: 0
|
|
||||||
display: flex
|
|
||||||
justify-content: center
|
|
||||||
align-items: center
|
|
||||||
background: transparent
|
|
||||||
|
|
||||||
letter-spacing: 20px
|
|
||||||
margin-left: 20px
|
|
||||||
|
|
||||||
font-size: 1.5rem
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
|
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
|
||||||
<p class="warning">
|
<blockquote>
|
||||||
{ts("settings.gameNotice")}
|
{ts("settings.gameNotice")}
|
||||||
</p>
|
</blockquote>
|
||||||
<GameSettingFields game="general"/>
|
<GameSettingFields game="general"/>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="bool">
|
<div class="bool">
|
||||||
|
@ -59,14 +59,4 @@
|
||||||
|
|
||||||
> input
|
> input
|
||||||
flex: 1
|
flex: 1
|
||||||
|
|
||||||
.warning
|
|
||||||
background: #aa555510
|
|
||||||
padding: 10px
|
|
||||||
border-left: solid 2px vars.$c-error
|
|
||||||
|
|
||||||
&::before
|
|
||||||
color: vars.$c-error
|
|
||||||
font-weight: bold
|
|
||||||
content: "!"
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
import { t } from "../../libs/i18n";
|
||||||
|
import { DISCORD_INVITE } from "../../libs/config";
|
||||||
|
|
||||||
|
export let error: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="overlay" transition:fade>
|
||||||
|
<div>
|
||||||
|
<h2 class="error">{t('status.error')}</h2>
|
||||||
|
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
|
||||||
|
<span>{t('status.detail', { detail: error })}</span>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button on:click={() => location.reload()} class="error">
|
||||||
|
{t('action.refresh')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
.actions
|
||||||
|
display: flex
|
||||||
|
gap: 16px
|
||||||
|
|
||||||
|
button
|
||||||
|
width: 100%
|
||||||
|
</style>
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Icon from '@iconify/svelte';
|
||||||
|
import { fade } from 'svelte/transition'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="overlay loading" transition:fade>
|
||||||
|
<Icon class="icon" icon="svg-spinners:pulse-2"/>
|
||||||
|
<span><span>LOADING</span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
.loading.overlay
|
||||||
|
font-size: 28rem
|
||||||
|
|
||||||
|
:global(.icon)
|
||||||
|
opacity: 0.5
|
||||||
|
|
||||||
|
> span
|
||||||
|
position: absolute
|
||||||
|
inset: 0
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
align-items: center
|
||||||
|
background: transparent
|
||||||
|
|
||||||
|
letter-spacing: 20px
|
||||||
|
margin-left: 20px
|
||||||
|
|
||||||
|
font-size: 1.5rem
|
||||||
|
</style>
|
|
@ -229,7 +229,15 @@ export const EN_REF_USERBOX = {
|
||||||
'userbox.new.error.invalidUrl': 'The URL you inputted is invalid.'
|
'userbox.new.error.invalidUrl': 'The URL you inputted is invalid.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EN_REF_MAI_PHOTO = {
|
||||||
|
'maiphoto.title': 'Mai Memorial Photo Gallery',
|
||||||
|
'maiphoto.url_warning': 'Note: If you want to share a photo with your friend, please save the photo. Do not copy image URL because the URL contains sensitive information.',
|
||||||
|
'maiphoto.none': 'No photo found. You can upload photo by clicking upload at the end of each game session.',
|
||||||
|
}
|
||||||
|
|
||||||
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,
|
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,
|
||||||
...EN_REF_LEADERBOARD, ...EN_REF_HOME, ...EN_REF_SETTINGS, ...EN_REF_USERBOX }
|
...EN_REF_LEADERBOARD, ...EN_REF_HOME, ...EN_REF_SETTINGS, ...EN_REF_USERBOX,
|
||||||
|
...EN_REF_MAI_PHOTO
|
||||||
|
}
|
||||||
|
|
||||||
export type LocalizedMessages = typeof EN_REF
|
export type LocalizedMessages = typeof EN_REF
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
EN_REF_GENERAL,
|
EN_REF_GENERAL,
|
||||||
EN_REF_HOME,
|
EN_REF_HOME,
|
||||||
EN_REF_LEADERBOARD,
|
EN_REF_LEADERBOARD,
|
||||||
|
EN_REF_MAI_PHOTO,
|
||||||
EN_REF_SETTINGS,
|
EN_REF_SETTINGS,
|
||||||
EN_REF_USER,
|
EN_REF_USER,
|
||||||
EN_REF_USERBOX,
|
EN_REF_USERBOX,
|
||||||
|
@ -234,6 +235,11 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
|
||||||
'userbox.new.error.invalidUrl': '输入的 URL 无效。'
|
'userbox.new.error.invalidUrl': '输入的 URL 无效。'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const zhMaiPhoto: typeof EN_REF_MAI_PHOTO = {
|
||||||
|
'maiphoto.title': 'Mai 纪念照片库',
|
||||||
|
'maiphoto.url_warning': '注意:如果想与朋友分享图片的话,请先保存照片再发出去。不要复制图片 URL,因为 URL 中包含 AquaDX 账号信息。',
|
||||||
|
'maiphoto.none': '还没有图片哦~ 可以在每次游戏结束的时候点击上传来上传照片。',
|
||||||
|
}
|
||||||
|
|
||||||
export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral,
|
export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral,
|
||||||
...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox }
|
...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox, ...zhMaiPhoto }
|
||||||
|
|
|
@ -284,6 +284,8 @@ export const CARD = {
|
||||||
export const GAME = {
|
export const GAME = {
|
||||||
trend: (username: string, game: GameName): Promise<TrendEntry[]> =>
|
trend: (username: string, game: GameName): Promise<TrendEntry[]> =>
|
||||||
post(`/api/v2/game/${game}/trend`, { username }),
|
post(`/api/v2/game/${game}/trend`, { username }),
|
||||||
|
photos: (): Promise<string[]> =>
|
||||||
|
post(`/api/v2/game/mai2/my-photo`, { }),
|
||||||
userSummary: (username: string, game: GameName): Promise<GenericGameSummary> =>
|
userSummary: (username: string, game: GameName): Promise<GenericGameSummary> =>
|
||||||
post(`/api/v2/game/${game}/user-summary`, { username }),
|
post(`/api/v2/game/${game}/user-summary`, { username }),
|
||||||
ranking: (game: GameName): Promise<GenericRanking[]> =>
|
ranking: (game: GameName): Promise<GenericRanking[]> =>
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {GAME} from "../libs/sdk";
|
||||||
|
import {AQUA_HOST} from "../libs/config";
|
||||||
|
import Loading from "../components/ui/Loading.svelte";
|
||||||
|
import Error from "../components/ui/Error.svelte";
|
||||||
|
import { t } from "../libs/i18n";
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="content">
|
||||||
|
<div class="outer-title-options">
|
||||||
|
<h2>{t("maiphoto.title")}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#await GAME.photos()}
|
||||||
|
<Loading/>
|
||||||
|
{:then photos}
|
||||||
|
{#if photos.length === 0}
|
||||||
|
<blockquote>{t('maiphoto.none')}</blockquote>
|
||||||
|
{:else}
|
||||||
|
<blockquote>{t('maiphoto.url_warning')}</blockquote>
|
||||||
|
{/if}
|
||||||
|
<div class="pictures">
|
||||||
|
{#each photos as photo}
|
||||||
|
<div class="photo-container">
|
||||||
|
<img class="rounded-2xl" src="{AQUA_HOST}/api/v2/game/mai2/my-photo/{photo}?token={token}" alt="Memorial" />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:catch error}
|
||||||
|
<Error {error}/>
|
||||||
|
{/await}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
@use "../vars"
|
||||||
|
|
||||||
|
.pictures
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
justify-content: center
|
||||||
|
row-gap: 1rem
|
||||||
|
gap: 1rem
|
||||||
|
|
||||||
|
.photo-container
|
||||||
|
flex: 1 1 300px
|
||||||
|
min-width: 280px
|
||||||
|
max-width: 100%
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
|
||||||
|
.photo-container img
|
||||||
|
width: 100%
|
||||||
|
height: auto
|
||||||
|
object-fit: contain
|
||||||
|
</style>
|
Loading…
Reference in New Issue