Refactor project structure and dependencies; rename package to azaion-ui, update version to 0.0.1, and remove unused files. Introduce new routing and authentication features in App component.

This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-03-25 03:10:15 +02:00
parent e407308284
commit 157a33096a
112 changed files with 6530 additions and 17843 deletions
+111
View File
@@ -0,0 +1,111 @@
import { useRef, useState, useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { api } from '../../api/client'
import { getToken } from '../../api/client'
import type { Media } from '../../types'
interface Props {
media: Media
onTimeUpdate: (time: number) => void
selectedClassNum: number
}
export default function VideoPlayer({ media, onTimeUpdate, selectedClassNum }: Props) {
const { t } = useTranslation()
const videoRef = useRef<HTMLVideoElement>(null)
const [playing, setPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [muted, setMuted] = useState(false)
const token = getToken()
const videoUrl = `/api/annotations/media/${media.id}/file`
const stepFrames = useCallback((count: number) => {
const video = videoRef.current
if (!video) return
const fps = 30
video.currentTime = Math.max(0, Math.min(video.duration, video.currentTime + count / fps))
}, [])
const togglePlay = useCallback(() => {
const v = videoRef.current
if (!v) return
if (v.paused) { v.play(); setPlaying(true) }
else { v.pause(); setPlaying(false) }
}, [])
const stop = useCallback(() => {
const v = videoRef.current
if (!v) return
v.pause()
v.currentTime = 0
setPlaying(false)
}, [])
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
switch (e.key) {
case ' ': e.preventDefault(); togglePlay(); break
case 'ArrowLeft': e.preventDefault(); stepFrames(e.ctrlKey ? -150 : -1); break
case 'ArrowRight': e.preventDefault(); stepFrames(e.ctrlKey ? 150 : 1); break
case 'm': case 'M': setMuted(m => !m); break
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [togglePlay, stepFrames])
const formatTime = (s: number) => {
const m = Math.floor(s / 60)
const sec = Math.floor(s % 60)
return `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`
}
return (
<div className="bg-black flex flex-col">
<video
ref={videoRef}
src={videoUrl}
muted={muted}
className="w-full max-h-[50vh] object-contain"
onTimeUpdate={e => {
const t = (e.target as HTMLVideoElement).currentTime
setCurrentTime(t)
onTimeUpdate(t)
}}
onLoadedMetadata={e => setDuration((e.target as HTMLVideoElement).duration)}
onClick={togglePlay}
/>
{/* Progress bar */}
<div
className="h-1 bg-az-border cursor-pointer"
onClick={e => {
const rect = e.currentTarget.getBoundingClientRect()
const pct = (e.clientX - rect.left) / rect.width
if (videoRef.current) videoRef.current.currentTime = pct * duration
}}
>
<div className="h-full bg-az-orange" style={{ width: `${duration ? (currentTime / duration) * 100 : 0}%` }} />
</div>
{/* Controls */}
<div className="flex items-center gap-1 px-2 py-1 bg-az-header text-xs">
<button onClick={togglePlay} className="text-az-text hover:text-white px-1">{playing ? '⏸' : '▶'}</button>
<button onClick={stop} className="text-az-text hover:text-white px-1"></button>
{[1, 5, 10, 30, 60].map(n => (
<button key={`prev-${n}`} onClick={() => stepFrames(-n)} className="text-az-muted hover:text-white px-0.5">-{n}</button>
))}
<span className="text-az-muted mx-1">|</span>
{[1, 5, 10, 30, 60].map(n => (
<button key={`next-${n}`} onClick={() => stepFrames(n)} className="text-az-muted hover:text-white px-0.5">+{n}</button>
))}
<div className="flex-1" />
<button onClick={() => setMuted(m => !m)} className="text-az-text hover:text-white px-1">
{muted ? '🔇' : '🔊'}
</button>
<span className="text-az-muted">{formatTime(currentTime)} / {formatTime(duration)}</span>
</div>
</div>
)
}