instance pages
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user