instance pages

This commit is contained in:
2026-04-24 14:37:16 +03:00
parent 17a3a89865
commit fa033c6d97
+159
View File
@@ -0,0 +1,159 @@
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { IconWorldFilled, IconHeartFilled, IconShieldFilled, IconArrowBack } from '@tabler/icons-react'
import { renderSimpleMarkdown } from '../markdown'
interface AboutData {
software: string
version: string
contact: string
federation: boolean
connected_instances: number
rules: string
}
interface HealthData {
status: string
delivery_success_rate?: number
queue_depth?: number
peers?: number
frozen?: boolean
enabled?: boolean
}
export default function InstancePages({ page, onBack }: { page: 'about' | 'health'; onBack: () => void }) {
const { t } = useTranslation()
const [about, setAbout] = useState<AboutData | null>(null)
const [health, setHealth] = useState<HealthData | null>(null)
useEffect(() => {
if (page === 'about') {
fetch('/api/about').then(r => r.json()).then(setAbout).catch(() => {})
fetch('/api/federation/health').then(r => r.json()).then(setHealth).catch(() => {})
}
if (page === 'health') {
fetch('/api/health').then(r => r.json()).then(setHealth).catch(() => {})
fetch('/api/federation/health').then(r => r.json()).then(d => setHealth(prev => ({ ...prev, ...d }))).catch(() => {})
}
}, [page])
return (
<div className="instance-page">
<div className="instance-page-header">
<button className="btn btn-sm btn-ghost" onClick={onBack} aria-label={t('common.back')}><IconArrowBack size={16} aria-hidden="true" /></button>
<h1 className="header-brand">{t('app.name')}</h1>
</div>
{page === 'about' && about && (
<div className="instance-page-body">
<div className="instance-hero">
<h2>{t('instance.aboutTitle')}</h2>
<p className="instance-tagline">{t('instance.tagline')}</p>
</div>
<div className="instance-section">
<div className="instance-info-grid">
<div className="instance-info-card">
<IconWorldFilled size={24} />
<div>
<strong>{about.software} {about.version}</strong>
<span>{t('instance.license')}</span>
</div>
</div>
{about.federation && (
<div className="instance-info-card">
<IconShieldFilled size={24} />
<div>
<strong>{t('instance.federationActive')}</strong>
<span>{about.connected_instances !== 1 ? t('instance.connectedInstances_plural', { count: about.connected_instances }) : t('instance.connectedInstances', { count: about.connected_instances })}</span>
</div>
</div>
)}
{about.contact && (
<div className="instance-info-card">
<IconHeartFilled size={24} />
<div>
<strong>{t('instance.contact')}</strong>
<span>{about.contact}</span>
</div>
</div>
)}
</div>
</div>
{about.rules && (
<div className="instance-section">
<h3>{t('instance.rules')}</h3>
<div className="instance-rules admin-md-rendered" dangerouslySetInnerHTML={{ __html: renderSimpleMarkdown(about.rules) }} />
</div>
)}
<div className="instance-section">
<h3>{t('instance.features')}</h3>
<div className="instance-features">
{[t('instance.featureList.dailyPuzzles'), t('instance.featureList.crypticMode'),
t('instance.featureList.puzzleEditor'), t('instance.featureList.communityClues'),
t('instance.featureList.themedCategories'), t('instance.featureList.gameModes'),
t('instance.featureList.achievements'), t('instance.featureList.pwa'),
about.federation ? t('instance.featureList.federation') : null,
].filter(Boolean).map((f, i) => (
<span key={i} className="instance-feature">{f}</span>
))}
</div>
</div>
{health && health.enabled && (
<div className="instance-section">
<h3>{t('instance.federationHealth')}</h3>
<div className="instance-health-grid">
<div className="instance-health-item">
<span className="instance-health-value">{health.delivery_success_rate?.toFixed(0) || 100}%</span>
<span className="instance-health-label">{t('instance.deliveryRate')}</span>
</div>
<div className="instance-health-item">
<span className="instance-health-value">{health.peers || 0}</span>
<span className="instance-health-label">{t('instance.peers')}</span>
</div>
<div className="instance-health-item">
<span className="instance-health-value">{health.queue_depth || 0}</span>
<span className="instance-health-label">{t('instance.queueDepth')}</span>
</div>
<div className="instance-health-item">
<span className={`instance-health-value ${health.frozen ? 'instance-health-warn' : ''}`}>
{health.frozen ? t('instance.frozen') : t('instance.active')}
</span>
<span className="instance-health-label">{t('instance.status')}</span>
</div>
</div>
</div>
)}
<div className="instance-footer">
<a href="/feeds/daily.rss" target="_blank" rel="noopener">{t('instance.rssFeed')}</a>
<a href="/feeds/daily.atom" target="_blank" rel="noopener">{t('instance.atomFeed')}</a>
<a href="/feeds/daily.json" target="_blank" rel="noopener">{t('instance.jsonFeed')}</a>
{about.federation && <a href="/.well-known/nodeinfo" target="_blank" rel="noopener">NodeInfo</a>}
</div>
</div>
)}
{page === 'health' && health && (
<div className="instance-page-body">
<div className="instance-hero">
<h2>{t('instance.healthTitle')}</h2>
</div>
<div className="instance-section">
<div className="instance-health-grid">
<div className="instance-health-item">
<span className={`instance-health-value ${health.status === 'ok' ? '' : 'instance-health-warn'}`}>
{health.status === 'ok' ? t('instance.healthy') : health.status}
</span>
<span className="instance-health-label">{t('instance.server')}</span>
</div>
</div>
</div>
</div>
)}
</div>
)
}