diff --git a/frontend/src/components/InstancePages.tsx b/frontend/src/components/InstancePages.tsx new file mode 100644 index 0000000..155cb04 --- /dev/null +++ b/frontend/src/components/InstancePages.tsx @@ -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(null) + const [health, setHealth] = useState(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 ( +
+
+ +

{t('app.name')}

+
+ + {page === 'about' && about && ( +
+
+

{t('instance.aboutTitle')}

+

{t('instance.tagline')}

+
+ +
+
+
+ +
+ {about.software} {about.version} + {t('instance.license')} +
+
+ {about.federation && ( +
+ +
+ {t('instance.federationActive')} + {about.connected_instances !== 1 ? t('instance.connectedInstances_plural', { count: about.connected_instances }) : t('instance.connectedInstances', { count: about.connected_instances })} +
+
+ )} + {about.contact && ( +
+ +
+ {t('instance.contact')} + {about.contact} +
+
+ )} +
+
+ + {about.rules && ( +
+

{t('instance.rules')}

+
+
+ )} + +
+

{t('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) => ( + {f} + ))} +
+
+ + {health && health.enabled && ( +
+

{t('instance.federationHealth')}

+
+
+ {health.delivery_success_rate?.toFixed(0) || 100}% + {t('instance.deliveryRate')} +
+
+ {health.peers || 0} + {t('instance.peers')} +
+
+ {health.queue_depth || 0} + {t('instance.queueDepth')} +
+
+ + {health.frozen ? t('instance.frozen') : t('instance.active')} + + {t('instance.status')} +
+
+
+ )} + + +
+ )} + + {page === 'health' && health && ( +
+
+

{t('instance.healthTitle')}

+
+
+
+
+ + {health.status === 'ok' ? t('instance.healthy') : health.status} + + {t('instance.server')} +
+
+
+
+ )} +
+ ) +}