website/app/pages/docs/policy/[id].vue
kittentm 303b96d8e7
All checks were successful
ci / ci (22, debian-trixie) (push) Successful in 32s
fix‼️
2026-07-20 22:56:06 +02:00

492 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
const { t } = useLocale()
const parsedHtml = ref('')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let markedParser: any = null
const baseRules = ref<Array<{ id: string, name: string }>>([])
const gameRules = ref<Array<{ id: string, name: string, version: string }>>([])
const selectedCategory = ref<string>('general')
const isPageLoading = ref(true)
const isDocLoading = ref(false)
interface PolicyMetadata {
name: string
iso8601: string
version: string
icons?: {
icon?: string
}
}
const activeMetadata = ref<PolicyMetadata | null>(null)
onMounted(async () => {
try {
const { marked } = await import('marked')
const { default: markedAlert } = await import('marked-alert')
marked.use(markedAlert())
markedParser = marked
const data = await $fetch<{
baseRules: Array<{ id: string, name: string }>
games: Array<{ id: string, name: string, version: string }>
}>('/api/policies')
baseRules.value = data.baseRules
gameRules.value = data.games
const pathSegments = window.location.pathname.split('/')
const currentId = pathSegments[pathSegments.length - 1]
const allValidIds = [...data.baseRules, ...data.games].map(item => item.id)
if (currentId && allValidIds.includes(currentId)) {
selectedCategory.value = currentId
}
await loadPolicyData(selectedCategory.value)
} catch (err) {
console.error(err)
} finally {
isPageLoading.value = false
}
})
const formatDate = (isoString?: string) => {
if (!isoString) return ''
return new Date(isoString).toLocaleDateString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
const getAssetUrl = (fileName: string | undefined, itemId: string) => {
if (!fileName) return ''
return `/policy/game-rules/${itemId}/${fileName}`
}
const loadPolicyData = async (id: string) => {
if (!markedParser) return
isDocLoading.value = true
activeMetadata.value = null
parsedHtml.value = ''
try {
let markdownPath = ''
let metadataPath = ''
if (id === 'general') markdownPath = '/policy/GENERAL-NETWORK-RULES.md'
else if (id === 'conduct') markdownPath = '/policy/CODE_OF_CONDUCT.md'
else if (id === 'privacy') markdownPath = '/policy/PRIVACY-POLICY.md'
else {
markdownPath = `/policy/game-rules/${id}/rule.md`
metadataPath = `/policy/game-rules/${id}/metadata.json`
}
const mdResponse = await fetch(markdownPath)
if (mdResponse.ok) {
const text = await mdResponse.text()
parsedHtml.value = await markedParser.parse(text)
} else {
parsedHtml.value = `<p class="error-text">${t.value.policies?.errorText || 'Error'}</p>`
}
if (metadataPath) {
const metaResponse = await fetch(metadataPath)
if (metaResponse.ok) {
activeMetadata.value = await metaResponse.json()
}
}
} catch (err) {
console.error(err)
parsedHtml.value = `<p class="error-text">${t.value.policies?.errorText || 'Error'}</p>`
} finally {
isDocLoading.value = false
}
}
const handleCategorySelect = (id: string) => {
selectedCategory.value = id
window.history.pushState({}, '', `/docs/policy/${id}`)
}
watch(selectedCategory, (newId) => {
loadPolicyData(newId)
})
</script>
<template>
<div class="settings-page-wrapper">
<div
v-if="isPageLoading"
class="loader-container full-page-loader"
>
<div class="spinner" />
<p>{{ t.policies?.loading }}</p>
</div>
<div
v-else
class="settings-container"
>
<aside class="settings-sidebar">
<div class="sidebar-header archivo-black">
{{ t.policies?.sidebarHeader }}
</div>
<nav class="sidebar-menu">
<button
v-for="rule in baseRules"
:key="rule.id"
class="nav-item"
:class="{ 'active-nav': selectedCategory === rule.id }"
@click="handleCategorySelect(rule.id)"
>
<span>{{ rule.name }}</span>
</button>
<template v-if="gameRules.length > 0">
<div class="sidebar-subheader archivo-black">
{{ t.policies?.sidebarSubheader }}
</div>
<button
v-for="game in gameRules"
:key="game.id"
class="nav-item"
:class="{ 'active-nav': selectedCategory === game.id }"
@click="handleCategorySelect(game.id)"
>
<span>{{ game.name }}</span>
<span class="chevron"></span>
</button>
</template>
</nav>
</aside>
<main class="settings-main-content">
<section class="settings-panel">
<div class="panel-header archivo-black split-header">
<div class="header-left">
<img
v-if="activeMetadata?.icons?.icon"
:src="getAssetUrl(activeMetadata.icons.icon, selectedCategory)"
class="item-icon"
alt="Icon"
>
<span>{{ activeMetadata?.name || selectedCategory.replace('-', ' ').toUpperCase() }}</span>
</div>
<div class="header-right">
<span
v-if="activeMetadata?.iso8601"
class="last-updated-text"
>
{{ t.policies?.lastUpdated }}{{ formatDate(activeMetadata.iso8601) }}
</span>
<span
v-if="activeMetadata?.version"
class="version-tag"
>
v{{ activeMetadata.version }}
</span>
</div>
</div>
<div class="panel-body">
<div
v-show="isDocLoading"
class="loader-container"
>
<div class="spinner" />
<p>{{ t.policies?.loading }}</p>
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div
v-show="!isDocLoading"
class="markdown-body"
v-html="parsedHtml"
/>
</div>
</section>
</main>
</div>
</div>
</template>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap');
.archivo-black {
font-family: "Archivo Black", sans-serif;
font-weight: 400;
}
.text-wiiu-blue {
color: #00aeef;
}
.settings-page-wrapper {
min-height: 100vh;
width: 100vw;
background-color: #1d1e1f;
color: #c9d1d9;
padding: 2rem 1rem;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
.settings-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
max-width: 1280px;
margin: 0 auto;
width: 100%;
}
.settings-sidebar {
background-color: #1a1a1f;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
overflow: hidden;
height: max-content;
}
.sidebar-header {
padding: 0.85rem 1rem;
font-size: 1.1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background-color: rgba(0, 0, 0, 0.15);
}
.sidebar-subheader {
padding: 0.85rem 1rem 0.35rem 1rem;
font-size: 0.8rem;
text-transform: uppercase;
color: #64748b;
letter-spacing: 0.05em;
background-color: rgba(0, 0, 0, 0.05);
}
.sidebar-menu {
display: flex;
flex-direction: column;
}
.nav-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: transparent;
border: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
color: #8b949e;
text-align: left;
cursor: pointer;
font-size: 0.95rem;
transition: all 0.2s;
width: 100%;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.03);
color: #ffffff;
}
.active-nav {
background: rgba(0, 174, 239, 0.1) !important;
border-left: 3px solid #00aeef;
color: #00aeef;
font-weight: 600;
padding-left: calc(1rem - 3px);
}
.chevron {
font-size: 1.25rem;
line-height: 1;
opacity: 0.7;
}
.settings-main-content {
flex: 1;
}
.settings-panel {
background-color: #1a1a1f;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
overflow: hidden;
}
.split-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left {
display: flex;
align-items: center;
gap: 0.75rem;
}
.header-right {
display: flex;
align-items: center;
gap: 0.85rem;
}
.last-updated-text {
font-size: 0.8rem;
color: #8b949e;
font-family: system-ui, sans-serif;
font-weight: normal;
text-transform: none;
}
.item-icon {
width: 32px;
height: 32px;
object-fit: cover;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.15);
}
.version-tag {
font-size: 0.85rem;
background-color: rgba(0, 174, 239, 0.15);
color: #00aeef;
padding: 0.2rem 0.6rem;
border-radius: 4px;
border: 1px solid rgba(0, 174, 239, 0.3);
font-family: monospace;
}
.panel-header {
padding: 0.85rem 1.25rem;
font-size: 1.1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background-color: rgba(0, 0, 0, 0.15);
}
.panel-body {
padding: 1.5rem;
}
.markdown-body {
line-height: 1.6;
font-size: 1rem;
color: #c9d1d9;
}
.markdown-body :deep(h1),
.markdown-body :deep(h2),
.markdown-body :deep(h3) {
color: #ffffff;
margin-top: 1.5rem;
margin-bottom: 1rem;
font-weight: 600;
}
.markdown-body :deep(p) {
margin-bottom: 1rem;
}
.markdown-body :deep(a) {
color: #00aeef;
text-decoration: underline;
}
.error-text {
color: #f85149;
}
.markdown-body :deep(.markdown-alert) {
padding: 0.75rem 1rem 0.75rem 1.25rem;
margin-bottom: 1.5rem;
border-left: 4px solid #30363d;
border-radius: 0 6px 6px 0;
background-color: rgba(255, 255, 255, 0.02);
}
.markdown-body :deep(.markdown-alert-title) {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 0.95rem;
text-transform: capitalize;
}
.markdown-body :deep(.markdown-alert-note) {
border-left-color: #00aeef;
background-color: rgba(0, 174, 239, 0.06);
}
.markdown-body :deep(.markdown-alert-note .markdown-alert-title) {
color: #ffffff !important;
}
.markdown-body :deep(.markdown-alert-note .markdown-alert-title svg) {
fill: #ffffff !important;
stroke: #ffffff !important;
color: #ffffff !important;
}
.markdown-body :deep(.markdown-alert-warning) {
border-left-color: #f0883e;
background-color: rgba(240, 136, 62, 0.06);
}
.markdown-body :deep(.markdown-alert-warning .markdown-alert-title) {
color: #f0883e;
}
.loader-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 3rem 0;
color: #8b949e;
}
.full-page-loader {
min-height: 50vh;
justify-content: center;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
border-top-color: #00aeef;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (min-width: 768px) {
.settings-container {
flex-direction: row;
align-items: flex-start;
}
.settings-sidebar {
width: 280px;
flex-shrink: 0;
}
}
</style>