主页原先内嵌整个皮肤站网页,现在换成启动器自己的界面,数据全部走 接口: - SkinSiteAccountCard:登录状态、玩家列表(头像由皮肤裁剪生成)、 每日运势。 - SkinSiteNavCards:资源下载、玩家审核、Wiki、漏洞追踪、地图、个人 资料的入口,点击后用系统浏览器打开。 - SkinSiteAnnouncements:拉取公告并按重要度与时间排序。 - SkinSiteDonationCard:捐款累计金额与人数。 - SkinSiteHome:把以上拼装成主页,并显示可复制的接入地址。 - API 层补充捐款与公告接口。 Index.vue 的 iframe 换成 SkinSiteHome。
This commit is contained in:
1 parent
bf175160b0
commit
8599a014ad
7 files changed
+546
-2
No files matched your search
@@ -0,0 +1,146 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { SparklesIcon, SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { Avatar, ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
requestSkinSiteLuck,
|
||||||
|
skinSitePlayers,
|
||||||
|
skinSitePlayersStatus,
|
||||||
|
skinSiteStatus,
|
||||||
|
skinSiteUser,
|
||||||
|
} from '@/composables/skin-site-session'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
signedOutTitle: { id: 'app.skin-site.home.signed-out-title', defaultMessage: 'Not signed in' },
|
||||||
|
signedOutBody: {
|
||||||
|
id: 'app.skin-site.home.signed-out-body',
|
||||||
|
defaultMessage: 'Sign in to the StarLight skin site to manage players and download packs.',
|
||||||
|
},
|
||||||
|
players: { id: 'app.skin-site.home.players', defaultMessage: 'Your players' },
|
||||||
|
noPlayers: {
|
||||||
|
id: 'app.skin-site.home.no-players',
|
||||||
|
defaultMessage: 'No players yet. Add one on the skin site.',
|
||||||
|
},
|
||||||
|
luck: { id: 'app.skin-site.home.luck', defaultMessage: "Today's luck" },
|
||||||
|
luckUnavailable: { id: 'app.skin-site.home.luck-unavailable', defaultMessage: 'Unavailable' },
|
||||||
|
loading: { id: 'app.skin-site.home.loading', defaultMessage: 'Loading…' },
|
||||||
|
error: { id: 'app.skin-site.home.error', defaultMessage: 'Could not load players.' },
|
||||||
|
signIn: { id: 'app.skin-site.home.sign-in', defaultMessage: 'Sign in' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'sign-in'): void }>()
|
||||||
|
|
||||||
|
const luck = ref<number | null>(null)
|
||||||
|
const luckLoading = ref(false)
|
||||||
|
|
||||||
|
const signedIn = computed(() => skinSiteStatus.value === 'signed-in' && !!skinSiteUser.value)
|
||||||
|
const players = computed(() => skinSitePlayers.value)
|
||||||
|
const playersLoading = computed(
|
||||||
|
() => skinSitePlayersStatus.value === 'checking' || skinSitePlayersStatus.value === 'idle',
|
||||||
|
)
|
||||||
|
const playersError = computed(() => skinSitePlayersStatus.value === 'error')
|
||||||
|
|
||||||
|
async function loadLuck() {
|
||||||
|
if (!signedIn.value) {
|
||||||
|
luck.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
luckLoading.value = true
|
||||||
|
try {
|
||||||
|
luck.value = await requestSkinSiteLuck()
|
||||||
|
} catch {
|
||||||
|
luck.value = null
|
||||||
|
} finally {
|
||||||
|
luckLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadLuck)
|
||||||
|
watch(signedIn, loadLuck)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-4 rounded-2xl border border-solid border-surface-5 bg-bg-raised p-4"
|
||||||
|
>
|
||||||
|
<div v-if="!signedIn" class="flex flex-col items-start gap-2">
|
||||||
|
<h3 class="m-0 text-base font-semibold text-contrast">
|
||||||
|
{{ formatMessage(messages.signedOutTitle) }}
|
||||||
|
</h3>
|
||||||
|
<p class="m-0 text-sm text-secondary">{{ formatMessage(messages.signedOutBody) }}</p>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button type="button" @click="emit('sign-in')">
|
||||||
|
{{ formatMessage(messages.signIn) }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
class="flex size-10 shrink-0 items-center justify-center rounded-full bg-brand-highlight text-lg font-bold text-contrast"
|
||||||
|
>
|
||||||
|
{{ skinSiteUser!.username.slice(0, 1).toUpperCase() }}
|
||||||
|
</div>
|
||||||
|
<div class="flex min-w-0 flex-col">
|
||||||
|
<span class="truncate font-semibold text-contrast">{{ skinSiteUser!.username }}</span>
|
||||||
|
<span class="truncate text-xs text-secondary">{{ skinSiteUser!.uuid }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="ml-auto flex shrink-0 items-center gap-1 rounded-full bg-button-bg px-3 py-1 text-sm"
|
||||||
|
:title="formatMessage(messages.luck)"
|
||||||
|
>
|
||||||
|
<SpinnerIcon v-if="luckLoading" class="size-4 animate-spin" />
|
||||||
|
<template v-else-if="luck !== null">
|
||||||
|
<SparklesIcon class="size-4 text-brand" />
|
||||||
|
<span class="font-semibold text-contrast">{{ luck }}</span>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-xs text-secondary">{{
|
||||||
|
formatMessage(messages.luckUnavailable)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm font-semibold text-contrast">{{
|
||||||
|
formatMessage(messages.players)
|
||||||
|
}}</span>
|
||||||
|
|
||||||
|
<div v-if="playersLoading" class="flex items-center gap-2 text-sm text-secondary">
|
||||||
|
<SpinnerIcon class="size-4 animate-spin" />
|
||||||
|
{{ formatMessage(messages.loading) }}
|
||||||
|
</div>
|
||||||
|
<p v-else-if="playersError" class="m-0 text-sm text-red">
|
||||||
|
{{ formatMessage(messages.error) }}
|
||||||
|
</p>
|
||||||
|
<p v-else-if="!players.length" class="m-0 text-sm text-secondary">
|
||||||
|
{{ formatMessage(messages.noPlayers) }}
|
||||||
|
</p>
|
||||||
|
<div v-else class="flex flex-wrap gap-3">
|
||||||
|
<div
|
||||||
|
v-for="player in players"
|
||||||
|
:key="player.uuid"
|
||||||
|
class="flex items-center gap-2 rounded-xl bg-button-bg px-2 py-1.5"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
:src="player.headDataUrl"
|
||||||
|
:alt="player.name"
|
||||||
|
size="2rem"
|
||||||
|
pixelated
|
||||||
|
class="!rounded-md"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="text-sm font-medium text-contrast">{{ player.name }}</span>
|
||||||
|
<span v-if="player.isMojang" class="text-[10px] uppercase text-secondary"
|
||||||
|
>Mojang</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import { type SkinSiteAnnouncement, skinSiteAnnouncements } from '@/helpers/skin-site-api'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'app.skin-site.home.announcements.title', defaultMessage: 'Announcements' },
|
||||||
|
empty: {
|
||||||
|
id: 'app.skin-site.home.announcements.empty',
|
||||||
|
defaultMessage: 'No announcements right now.',
|
||||||
|
},
|
||||||
|
loading: { id: 'app.skin-site.home.announcements.loading', defaultMessage: 'Loading…' },
|
||||||
|
failed: {
|
||||||
|
id: 'app.skin-site.home.announcements.failed',
|
||||||
|
defaultMessage: 'Could not load announcements.',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const announcements = ref<SkinSiteAnnouncement[]>([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const failed = ref(false)
|
||||||
|
|
||||||
|
function formatDate(ts: number): string {
|
||||||
|
try {
|
||||||
|
return new Date(ts).toLocaleDateString()
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const result = await skinSiteAnnouncements()
|
||||||
|
if (result.success) {
|
||||||
|
announcements.value = [...(result.payload ?? [])]
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.star !== b.star) return a.star ? -1 : 1
|
||||||
|
return b.updateAt - a.updateAt
|
||||||
|
})
|
||||||
|
.slice(0, 5)
|
||||||
|
} else {
|
||||||
|
failed.value = true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
failed.value = true
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<h3 class="m-0 text-base font-semibold text-contrast">
|
||||||
|
{{ formatMessage(messages.title) }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div v-if="loading" class="flex items-center gap-2 text-sm text-secondary">
|
||||||
|
<SpinnerIcon class="size-4 animate-spin" />
|
||||||
|
{{ formatMessage(messages.loading) }}
|
||||||
|
</div>
|
||||||
|
<p v-else-if="failed" class="m-0 text-sm text-secondary">
|
||||||
|
{{ formatMessage(messages.failed) }}
|
||||||
|
</p>
|
||||||
|
<p v-else-if="!announcements.length" class="m-0 text-sm text-secondary">
|
||||||
|
{{ formatMessage(messages.empty) }}
|
||||||
|
</p>
|
||||||
|
<div v-else class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-for="item in announcements"
|
||||||
|
:key="item.id"
|
||||||
|
class="flex flex-col gap-1 rounded-xl border border-solid border-surface-5 bg-bg-raised p-3"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<span class="min-w-0 flex-1 truncate text-sm font-semibold text-contrast">
|
||||||
|
{{ item.title }}
|
||||||
|
</span>
|
||||||
|
<span class="shrink-0 text-xs text-secondary">{{ formatDate(item.updateAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="item.subtitle" class="truncate text-xs text-secondary">{{
|
||||||
|
item.subtitle
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { HeartIcon, SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import { type SkinSiteDonationSummary, skinSiteDonationSummary } from '@/helpers/skin-site-api'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'app.skin-site.home.donation.title', defaultMessage: 'Donation thanks' },
|
||||||
|
total: { id: 'app.skin-site.home.donation.total', defaultMessage: 'Total raised' },
|
||||||
|
donors: { id: 'app.skin-site.home.donation.donors', defaultMessage: '{count} donors' },
|
||||||
|
unavailable: {
|
||||||
|
id: 'app.skin-site.home.donation.unavailable',
|
||||||
|
defaultMessage: 'Donation summary unavailable.',
|
||||||
|
},
|
||||||
|
loading: { id: 'app.skin-site.home.donation.loading', defaultMessage: 'Loading…' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const summary = ref<SkinSiteDonationSummary | null>(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const failed = ref(false)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const result = await skinSiteDonationSummary()
|
||||||
|
if (result.success) summary.value = result.payload
|
||||||
|
else failed.value = true
|
||||||
|
} catch {
|
||||||
|
failed.value = true
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 rounded-xl border border-solid border-surface-5 bg-bg-raised px-4 py-3"
|
||||||
|
>
|
||||||
|
<HeartIcon class="size-5 shrink-0 text-brand" />
|
||||||
|
<div class="flex min-w-0 flex-1 flex-col">
|
||||||
|
<span class="text-sm font-semibold text-contrast">{{ formatMessage(messages.title) }}</span>
|
||||||
|
<span v-if="loading" class="flex items-center gap-1 text-xs text-secondary">
|
||||||
|
<SpinnerIcon class="size-3 animate-spin" />
|
||||||
|
{{ formatMessage(messages.loading) }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="failed" class="text-xs text-secondary">
|
||||||
|
{{ formatMessage(messages.unavailable) }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="summary" class="text-xs text-secondary">
|
||||||
|
{{ formatMessage(messages.total) }}: {{ summary.totalAmount }} ·
|
||||||
|
{{ formatMessage(messages.donors, { count: summary.donorCount }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CopyIcon } from '@modrinth/assets'
|
||||||
|
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import SkinSiteAccountCard from './SkinSiteAccountCard.vue'
|
||||||
|
import SkinSiteAnnouncements from './SkinSiteAnnouncements.vue'
|
||||||
|
import SkinSiteDonationCard from './SkinSiteDonationCard.vue'
|
||||||
|
import SkinSiteLoginModal from './SkinSiteLoginModal.vue'
|
||||||
|
import SkinSiteNavCards from './SkinSiteNavCards.vue'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
const { addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'app.skin-site.home.title', defaultMessage: 'StarLight Skin Site' },
|
||||||
|
subtitle: {
|
||||||
|
id: 'app.skin-site.home.subtitle',
|
||||||
|
defaultMessage: 'The wardrobe for StarLight Server players.',
|
||||||
|
},
|
||||||
|
address: { id: 'app.skin-site.home.address', defaultMessage: 'Skin site address' },
|
||||||
|
copied: { id: 'app.skin-site.home.copied', defaultMessage: 'Address copied.' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const YGGDRASIL_URL = 'https://skin.starlight.cool/yggdrasil'
|
||||||
|
const loginModal = ref<InstanceType<typeof SkinSiteLoginModal>>()
|
||||||
|
|
||||||
|
async function copyAddress() {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(YGGDRASIL_URL)
|
||||||
|
addNotification({ type: 'success', title: formatMessage(messages.copied) })
|
||||||
|
} catch {
|
||||||
|
// Clipboard may be unavailable; the address stays selectable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full min-h-0 w-full flex-col overflow-y-auto">
|
||||||
|
<div class="mx-auto flex w-full max-w-5xl flex-col gap-6 p-6">
|
||||||
|
<header class="flex flex-col items-center gap-1 text-center">
|
||||||
|
<h1 class="m-0 text-2xl font-bold text-contrast">{{ formatMessage(messages.title) }}</h1>
|
||||||
|
<p class="m-0 text-sm text-secondary">{{ formatMessage(messages.subtitle) }}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<SkinSiteAccountCard @sign-in="loginModal?.show()" />
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||||
|
<SkinSiteNavCards />
|
||||||
|
<SkinSiteAnnouncements />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SkinSiteDonationCard />
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center gap-2 rounded-xl border border-solid border-surface-5 bg-bg-raised px-4 py-3"
|
||||||
|
>
|
||||||
|
<span class="text-sm font-semibold text-contrast">{{
|
||||||
|
formatMessage(messages.address)
|
||||||
|
}}</span>
|
||||||
|
<code class="min-w-0 flex-1 truncate text-xs text-secondary">{{ YGGDRASIL_URL }}</code>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex shrink-0 items-center gap-1 rounded-full bg-button-bg px-3 py-1 text-xs font-semibold text-contrast hover:brightness-110"
|
||||||
|
@click="copyAddress"
|
||||||
|
>
|
||||||
|
<CopyIcon class="size-3.5" />
|
||||||
|
{{ formatMessage({ id: 'app.skin-site.home.copy', defaultMessage: 'Copy' }) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SkinSiteLoginModal ref="loginModal" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
BookIcon,
|
||||||
|
BugIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
MapIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
|
UserIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'app.skin-site.home.nav.title', defaultMessage: 'Skin site' },
|
||||||
|
download: { id: 'app.skin-site.home.nav.download', defaultMessage: 'Downloads' },
|
||||||
|
downloadDesc: {
|
||||||
|
id: 'app.skin-site.home.nav.download-desc',
|
||||||
|
defaultMessage: 'Browse files shared by the server.',
|
||||||
|
},
|
||||||
|
review: { id: 'app.skin-site.home.nav.review', defaultMessage: 'Player review' },
|
||||||
|
reviewDesc: {
|
||||||
|
id: 'app.skin-site.home.nav.review-desc',
|
||||||
|
defaultMessage: 'Submit and review player applications.',
|
||||||
|
},
|
||||||
|
wiki: { id: 'app.skin-site.home.nav.wiki', defaultMessage: 'Server wiki' },
|
||||||
|
wikiDesc: {
|
||||||
|
id: 'app.skin-site.home.nav.wiki-desc',
|
||||||
|
defaultMessage: 'Guides and documentation for the server.',
|
||||||
|
},
|
||||||
|
bugs: { id: 'app.skin-site.home.nav.bugs', defaultMessage: 'Bug tracker' },
|
||||||
|
bugsDesc: {
|
||||||
|
id: 'app.skin-site.home.nav.bugs-desc',
|
||||||
|
defaultMessage: 'Report issues and follow their progress.',
|
||||||
|
},
|
||||||
|
map: { id: 'app.skin-site.home.nav.map', defaultMessage: 'Server map' },
|
||||||
|
mapDesc: {
|
||||||
|
id: 'app.skin-site.home.nav.map-desc',
|
||||||
|
defaultMessage: 'Explore the live world map.',
|
||||||
|
},
|
||||||
|
profile: { id: 'app.skin-site.home.nav.profile', defaultMessage: 'Profile' },
|
||||||
|
profileDesc: {
|
||||||
|
id: 'app.skin-site.home.nav.profile-desc',
|
||||||
|
defaultMessage: 'Manage your account and textures.',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
interface NavItem {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
icon: Component
|
||||||
|
href: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKIN_SITE = 'https://skin.starlight.cool'
|
||||||
|
|
||||||
|
const items: NavItem[] = [
|
||||||
|
{
|
||||||
|
id: 'download',
|
||||||
|
title: formatMessage(messages.download),
|
||||||
|
description: formatMessage(messages.downloadDesc),
|
||||||
|
icon: DownloadIcon,
|
||||||
|
href: `${SKIN_SITE}/download`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'review',
|
||||||
|
title: formatMessage(messages.review),
|
||||||
|
description: formatMessage(messages.reviewDesc),
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
href: `${SKIN_SITE}/review`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wiki',
|
||||||
|
title: formatMessage(messages.wiki),
|
||||||
|
description: formatMessage(messages.wikiDesc),
|
||||||
|
icon: BookIcon,
|
||||||
|
href: `${SKIN_SITE}/wiki`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bugs',
|
||||||
|
title: formatMessage(messages.bugs),
|
||||||
|
description: formatMessage(messages.bugsDesc),
|
||||||
|
icon: BugIcon,
|
||||||
|
href: `${SKIN_SITE}/bugs`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'map',
|
||||||
|
title: formatMessage(messages.map),
|
||||||
|
description: formatMessage(messages.mapDesc),
|
||||||
|
icon: MapIcon,
|
||||||
|
href: `${SKIN_SITE}/map`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'profile',
|
||||||
|
title: formatMessage(messages.profile),
|
||||||
|
description: formatMessage(messages.profileDesc),
|
||||||
|
icon: UserIcon,
|
||||||
|
href: `${SKIN_SITE}/profile`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function open(href: string) {
|
||||||
|
void import('@tauri-apps/plugin-opener').then(({ openUrl }) => openUrl(href))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<h3 class="m-0 text-base font-semibold text-contrast">{{ formatMessage(messages.title) }}</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||||
|
<button
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.id"
|
||||||
|
type="button"
|
||||||
|
class="flex flex-col items-start gap-1 rounded-xl border border-solid border-surface-5 bg-bg-raised p-3 text-left transition-colors hover:bg-button-bg"
|
||||||
|
@click="open(item.href)"
|
||||||
|
>
|
||||||
|
<component :is="item.icon" class="size-5 text-brand" />
|
||||||
|
<span class="text-sm font-semibold text-contrast">{{ item.title }}</span>
|
||||||
|
<span class="text-xs text-secondary">{{ item.description }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -382,3 +382,52 @@ export function skinSitePasswordReset(payload: {
|
|||||||
auth: false,
|
auth: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Donations & announcements
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface SkinSiteDonationSummary {
|
||||||
|
totalAmount: string
|
||||||
|
donorCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteDonationEntry {
|
||||||
|
id: number
|
||||||
|
rank: number
|
||||||
|
username: string
|
||||||
|
avatar: string | null
|
||||||
|
amount: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteDonationPage {
|
||||||
|
items: SkinSiteDonationEntry[]
|
||||||
|
summary: SkinSiteDonationSummary
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteDonationSummary(): Promise<SkinSiteResponse<SkinSiteDonationSummary>> {
|
||||||
|
return skinSiteRequest<SkinSiteDonationSummary>('donation/summary', { auth: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skinSiteDonationList(offset = 0): Promise<SkinSiteResponse<SkinSiteDonationPage>> {
|
||||||
|
return skinSiteRequest<SkinSiteDonationPage>(`donation/list?limit=20&offset=${offset}`, {
|
||||||
|
auth: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkinSiteAnnouncement {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
imageName?: string
|
||||||
|
content: string
|
||||||
|
updateAt: number
|
||||||
|
star: boolean
|
||||||
|
importantLevel: number
|
||||||
|
source: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Public announcements shown on the skin site. Requires the JWT when signed in. */
|
||||||
|
export function skinSiteAnnouncements(): Promise<SkinSiteResponse<SkinSiteAnnouncement[]>> {
|
||||||
|
return skinSiteRequest<SkinSiteAnnouncement[]>('broadcast/all', { type: 'skin' })
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ import HomeLaunchProgress from '@/components/home/HomeLaunchProgress.vue'
|
|||||||
import HomeMinecraftNews from '@/components/home/HomeMinecraftNews.vue'
|
import HomeMinecraftNews from '@/components/home/HomeMinecraftNews.vue'
|
||||||
import HomeMinimal from '@/components/home/HomeMinimal.vue'
|
import HomeMinimal from '@/components/home/HomeMinimal.vue'
|
||||||
import HomePlayInsights from '@/components/home/HomePlayInsights.vue'
|
import HomePlayInsights from '@/components/home/HomePlayInsights.vue'
|
||||||
import SkinSiteFrame from '@/components/ui/SkinSiteFrame.vue'
|
import SkinSiteHome from '@/components/ui/skin-site/SkinSiteHome.vue'
|
||||||
import { useNetworkStatus } from '@/composables/useNetworkStatus'
|
import { useNetworkStatus } from '@/composables/useNetworkStatus'
|
||||||
import { get_default_user, users } from '@/helpers/auth'
|
import { get_default_user, users } from '@/helpers/auth'
|
||||||
import { DIRECT_LINKS_SYNCED_EVENT } from '@/helpers/direct-link-sync'
|
import { DIRECT_LINKS_SYNCED_EVENT } from '@/helpers/direct-link-sync'
|
||||||
@@ -162,7 +162,7 @@ onUnmounted(() => {
|
|||||||
@select="selectMinimalInstance"
|
@select="selectMinimalInstance"
|
||||||
/>
|
/>
|
||||||
<div class="home-content" :class="{ 'is-minimal': isMinimal }">
|
<div class="home-content" :class="{ 'is-minimal': isMinimal }">
|
||||||
<SkinSiteFrame v-if="!isMinimal" />
|
<SkinSiteHome v-if="!isMinimal" />
|
||||||
|
|
||||||
<HomeMinimal
|
<HomeMinimal
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
Reference in new issue
Block a user