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