LoVisual/frontend/src/features/account/components/DeviceList.tsx
loki5512344 e00ea8eb11
feat: frontend perf and a11y pass, plus mod visuals, hero main menu, Simple Voice Chat and vanilla chat autocomplete
Frontend:
- replace the i18next stack with a small typed i18n module
- hero clouds removed, llms.txt, HUD widget ARIA fixes and tests

Mod:
- TargetESP constellation mode; tracker and colors extracted
- JumpCircles: 7 new shapes, shared animation and color pipeline
- KillEffect blood mode with Burst/Fountain/Ring/Spray styles
- main menu: menuLayout setting (orbs/hero) and hero screen
- Simple Voice Chat soft integration: VoiceChat module and HUD panel
- renderer/quick: static Draw facade with glass outline
- vanilla chat: Baritone-style autocomplete popup for the % command prefix
2026-10-01 21:29:54 +02:00

72 lines
2.6 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from '@shared/i18n/react'
import { Link } from 'react-router'
import { Button } from '@shared/ui/Button'
import { listDevices, revokeDevice, type DeviceLink } from '../api'
function relativeDate(iso: string): string {
return new Intl.DateTimeFormat('ru-RU', { dateStyle: 'long' }).format(new Date(iso))
}
function DeviceRow({ device }: { device: DeviceLink }) {
const { t } = useTranslation('account')
const client = useQueryClient()
const [confirming, setConfirming] = useState(false)
const mutation = useMutation({
mutationFn: () => revokeDevice(device.id),
onSuccess: () => client.invalidateQueries({ queryKey: ['devices'] }),
})
return (
<li className="flex items-center justify-between gap-4 rounded-(--radius-panel) border border-shoal bg-slate/60 px-4 py-3">
<div>
<p className="text-sm text-snow">{t('devices.linkedAt', { date: relativeDate(device.linked_at) })}</p>
<p className="text-sm text-frost">
{t('devices.lastSeen', { when: device.last_seen ? relativeDate(device.last_seen) : t('devices.neverSeen') })}
</p>
</div>
{confirming ? (
<div className="flex items-center gap-2">
<Button variant="danger" busy={mutation.isPending} onClick={() => mutation.mutate()}>
{t('devices.confirmRevoke')}
</Button>
<Button variant="quiet" onClick={() => setConfirming(false)}>
{t('devices.cancel')}
</Button>
</div>
) : (
<Button variant="danger" onClick={() => setConfirming(true)}>
{t('devices.revoke')}
</Button>
)}
</li>
)
}
/** ClickGui-like rows listing devices the mod linked, each with a two-step confirm to revoke. */
export function DeviceList() {
const { t } = useTranslation('account')
const query = useQuery({ queryKey: ['devices'], queryFn: listDevices })
const devices = query.data ?? []
return (
<div className="rounded-(--radius-panel) border border-shoal bg-slate/40 p-5">
<h2 className="font-display text-lg">{t('devices.heading')}</h2>
{devices.length === 0 ? (
<p className="mt-3 text-sm text-frost">
{t('devices.empty')}{' '}
<Link to="/link" className="text-ice underline-offset-4 hover:underline">
{t('devices.emptyLink')}
</Link>
</p>
) : (
<ul className="mt-3 flex flex-col gap-2">
{devices.map((device) => (
<DeviceRow key={device.id} device={device} />
))}
</ul>
)}
</div>
)
}