Skip to content
This repository was archived by the owner on Nov 5, 2025. It is now read-only.

Commit 77e36a1

Browse files
committed
feat: update index title and enhance footer with new navigation and information; add tab functionality to history route for better user experience
1 parent 12d7bfc commit 77e36a1

File tree

5 files changed

+235
-31
lines changed

5 files changed

+235
-31
lines changed

frontend/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<title>Hackathon</title>
6+
<title>RandomTrust — прозрачные случайные числа</title>
77
<link rel="icon" href="/favicon.png" type="image/png" />
88
<link rel="stylesheet" href="/src/styles.css" />
99
<link rel="manifest" href="/manifest.json" />

frontend/src/components/Footer.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { Link } from '@tanstack/react-router'
2-
import { Shuffle } from 'lucide-react'
2+
import { Shuffle, ArrowUp, Camera, ShieldCheck } from 'lucide-react'
3+
import { Button } from '@/components/ui/button'
4+
import { Badge } from '@/components/ui/badge'
35

46
export function Footer() {
57
const year = new Date().getFullYear()
68
const nav = [
79
{ to: '/lottery', label: 'Лотерейный тираж' },
10+
{ to: '/verify', label: 'Проверка результатов' },
811
{ to: '/analysis', label: 'Статистический анализ' },
912
{ to: '/history', label: 'История тиражей' },
1013
{ to: '/export', label: 'Экспорт' },
@@ -20,8 +23,18 @@ export function Footer() {
2023
RandomTrust
2124
</Link>
2225
<p className="max-w-sm text-sm text-muted-foreground">
23-
Платформа прозрачных случайных чисел: сбор энтропии, генерация и независимая проверка.
26+
Платформа прозрачных случайных чисел: LavaRand (кадры лавовой лампы → криптографический хэш),
27+
генерация, подпись и независимая проверка.
2428
</p>
29+
<div className="flex flex-wrap items-center gap-2">
30+
<Badge variant="secondary" className="gap-1">
31+
<Camera className="h-3.5 w-3.5" /> LavaRand
32+
</Badge>
33+
<Badge variant="secondary">NIST STS</Badge>
34+
<Badge variant="secondary" className="gap-1">
35+
<ShieldCheck className="h-3.5 w-3.5" /> Подпись
36+
</Badge>
37+
</div>
2538
<div className="flex items-center gap-2 text-xs text-muted-foreground">
2639
<span>© {year} RandomTrust</span>
2740
<span aria-hidden className="mx-1"></span>
@@ -48,6 +61,20 @@ export function Footer() {
4861
</div>
4962
</nav>
5063
</div>
64+
<div className="mt-8 flex items-center justify-between">
65+
<p className="text-xs text-muted-foreground">
66+
Сделано с акцентом на прозрачность и воспроизводимость результатов.
67+
</p>
68+
<Button
69+
variant="outline"
70+
size="sm"
71+
className="gap-2"
72+
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
73+
aria-label="Вернуться наверх"
74+
>
75+
<ArrowUp className="h-4 w-4" /> Вверх
76+
</Button>
77+
</div>
5178
</div>
5279
</footer>
5380
)

frontend/src/routes/demo.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,6 @@ const CameraStream = ({ nextStep, isRunning }: { nextStep: () => void, isRunning
540540
<iframe
541541
src='https://hack.innohassle.ru/_webrtc/lavalamp/'
542542
className="h-full w-full"
543-
scrolling="no"
544543
allow="autoplay; fullscreen"
545544
/>
546545
</div>

frontend/src/routes/history.tsx

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { Badge } from '@/components/ui/badge'
77
import { Button } from '@/components/ui/button'
88
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
99
import { Progress } from '@/components/ui/progress'
10+
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
11+
import { $api } from '@/api'
12+
import type { SchemaShowLessGeneration } from '@/api/types'
1013
import {
1114
Sheet,
1215
SheetClose,
@@ -171,6 +174,7 @@ function mapStoredRunToHistoricalDraw(run: StoredLotteryRun): HistoricalDraw {
171174
export const Route = createFileRoute('/history')({
172175
component: function HistoryComponent() {
173176
const [draws, setDraws] = useState<HistoricalDraw[]>([])
177+
const [activeTab, setActiveTab] = useState<'mine' | 'all'>('mine')
174178
const [selectedDrawId, setSelectedDrawId] = useState<string | null>(null)
175179
const [isSheetOpen, setIsSheetOpen] = useState(false)
176180
const [replayState, setReplayState] = useState<{ drawId: string | null; isActive: boolean }>({
@@ -225,9 +229,40 @@ export const Route = createFileRoute('/history')({
225229
}
226230
}, [])
227231

232+
// Load public/all generations from backend
233+
const { data: allGenerations, isPending: isAllPending } = $api.useQuery('get', '/generation/')
234+
235+
const serverDraws: HistoricalDraw[] = useMemo(() => {
236+
const items = Array.isArray(allGenerations) ? allGenerations : []
237+
const toHistorical = (g: SchemaShowLessGeneration): HistoricalDraw => ({
238+
id: g.id,
239+
timestamp: g.created_at,
240+
numbers: g.result,
241+
footprint: g.footprint ?? null,
242+
prompt: '—',
243+
parameters: {
244+
count: g.generation_params.count,
245+
minValue: g.generation_params.from_,
246+
maxValue: g.generation_params.to,
247+
},
248+
statisticalTests: BASE_STATISTICAL_TESTS.map((test, index) => ({
249+
name: test.name,
250+
pValue: Number(((index + 1) / (BASE_STATISTICAL_TESTS.length + 2)).toFixed(3)),
251+
result: 'pass',
252+
})),
253+
replayData: {
254+
entropySources: [],
255+
processingSteps: [...PROCESSING_STEPS],
256+
},
257+
})
258+
return items.map(toHistorical)
259+
}, [allGenerations])
260+
261+
const currentDataset = activeTab === 'mine' ? draws : serverDraws
262+
228263
const selectedDraw = useMemo(
229-
() => draws.find((draw) => draw.id === selectedDrawId) ?? null,
230-
[draws, selectedDrawId],
264+
() => currentDataset.find((draw) => draw.id === selectedDrawId) ?? null,
265+
[currentDataset, selectedDrawId],
231266
)
232267

233268
useEffect(() => {
@@ -292,16 +327,24 @@ export const Route = createFileRoute('/history')({
292327
<div className="flex flex-col gap-12">
293328
<section className="space-y-4">
294329
<div className="space-y-3">
295-
<h1 className="text-3xl font-semibold text-foreground sm:text-4xl">
296-
История тиражей
297-
</h1>
330+
<div className="flex flex-wrap items-center justify-between gap-4">
331+
<h1 className="text-3xl font-semibold text-foreground sm:text-4xl">История тиражей</h1>
332+
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as 'mine' | 'all')}>
333+
<TabsList className="bg-muted/60">
334+
<TabsTrigger value="all" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none">Все</TabsTrigger>
335+
<TabsTrigger value="mine" className="data-[state=active]:bg-transparent data-[state=active]:shadow-none">Мои</TabsTrigger>
336+
</TabsList>
337+
</Tabs>
338+
</div>
298339
<p className="max-w-3xl text-muted-foreground">
299-
Локальная история ваших тиражей. Просмотрите подробную информацию о каждом тираже, включая параметры, результаты статистических тестов и подпись.
340+
{activeTab === 'mine'
341+
? 'Локальная история ваших тиражей. Просмотрите подробную информацию о каждом тираже, включая параметры, результаты статистических тестов и подпись.'
342+
: 'Публичные тиражи. Ознакомьтесь с параметрами и подписью, чтобы воспроизвести и проверить результат.'}
300343
</p>
301344
</div>
302345
</section>
303346

304-
{!draws.length ? (
347+
{activeTab === 'mine' && !draws.length ? (
305348
<Card className="border-dashed border-border/70 bg-card/70">
306349
<CardContent className="flex flex-col items-center gap-6 p-12 text-center">
307350
<FolderOpen className="h-12 w-12 text-muted-foreground" />
@@ -317,9 +360,109 @@ export const Route = createFileRoute('/history')({
317360
</Button>
318361
</CardContent>
319362
</Card>
363+
) : activeTab === 'mine' ? (
364+
<div className="space-y-6">
365+
{currentDataset.map((draw) => {
366+
return (
367+
<Card
368+
key={draw.id}
369+
className={cn(
370+
'border-border/70 bg-card/80 transition-colors hover:border-primary/40',
371+
selectedDrawId === draw.id && 'border-primary/60 shadow-lg shadow-primary/5',
372+
)}
373+
>
374+
<CardContent className="space-y-4 p-6">
375+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
376+
<div className="flex flex-wrap items-start gap-4">
377+
<div className="flex gap-1">
378+
{draw.numbers.map((number, index) => (
379+
<div
380+
key={`${number}-${index}`}
381+
className={cn(
382+
'flex h-10 w-10 items-center justify-center rounded-lg border text-sm font-semibold',
383+
'border-primary/30 bg-primary/10 text-primary',
384+
)}
385+
>
386+
{number}
387+
</div>
388+
))}
389+
</div>
390+
<div className="space-y-1">
391+
<div className="flex items-center gap-2 text-sm font-semibold text-foreground">
392+
<History className="h-4 w-4 text-primary" />
393+
Тираж #{draw.id}
394+
</div>
395+
<div className="text-xs text-muted-foreground">
396+
{new Date(draw.timestamp).toLocaleString()}
397+
</div>
398+
</div>
399+
</div>
400+
</div>
401+
402+
<div className="grid gap-3 text-sm text-muted-foreground md:grid-cols-2">
403+
<div className="space-y-1">
404+
<p className="font-medium text-foreground">Параметры генерации</p>
405+
<p className="text-xs">
406+
{draw.parameters.count} чисел от {draw.parameters.minValue} до {draw.parameters.maxValue}
407+
</p>
408+
</div>
409+
{draw.footprint && (
410+
<div className="space-y-1 md:col-span-2">
411+
<p className="font-medium text-foreground">Криптографическая подпись</p>
412+
<code className="block truncate rounded-md border border-border/60 bg-background px-3 py-2 text-xs">
413+
{draw.footprint}
414+
</code>
415+
</div>
416+
)}
417+
</div>
418+
419+
<div className="flex flex-wrap gap-3">
420+
{draw.footprint && (
421+
<Button
422+
asChild
423+
variant="outline"
424+
size="sm"
425+
className="gap-2"
426+
>
427+
<Link
428+
to="/verify"
429+
search={{
430+
footprint: draw.footprint ?? undefined,
431+
numbers: draw.numbers.join(', '),
432+
}}
433+
>
434+
<CheckCircle2 className="h-4 w-4" />
435+
Проверить подпись
436+
</Link>
437+
</Button>
438+
)}
439+
<Button variant="ghost" size="sm" className="gap-2" onClick={() => handleExport(draw)}>
440+
<Download className="h-4 w-4" />
441+
Экспорт
442+
</Button>
443+
</div>
444+
</CardContent>
445+
</Card>
446+
)
447+
})}
448+
</div>
449+
) : isAllPending ? (
450+
<Card className="border-border/70 bg-card/80">
451+
<CardContent className="p-8 text-center text-sm text-muted-foreground">Загружаем публичные тиражи…</CardContent>
452+
</Card>
453+
) : !serverDraws.length ? (
454+
<Card className="border-dashed border-border/70 bg-card/70">
455+
<CardContent className="flex flex-col items-center gap-6 p-12 text-center">
456+
<FolderOpen className="h-12 w-12 text-muted-foreground" />
457+
<div className="space-y-2">
458+
<h2 className="text-xl font-semibold text-foreground">Нет опубликованных тиражей</h2>
459+
<p className="max-w-md text-sm text-muted-foreground">Как только появятся новые публичные тиражи, они отобразятся здесь.</p>
460+
</div>
461+
</CardContent>
462+
</Card>
320463
) : (
321464
<div className="space-y-6">
322-
{draws.map((draw) => {
465+
{currentDataset.map((draw) => {
323466
return (
324467
<Card
325468
key={draw.id}

0 commit comments

Comments
 (0)