144 lines
3.6 KiB
TypeScript
144 lines
3.6 KiB
TypeScript
"use client"
|
||
|
||
import type { IUploadStepProps } from "./UploadStep.d"
|
||
import type { JSX } from "react"
|
||
|
||
import { Upload } from "lucide-react"
|
||
import { FunctionComponent, useCallback, useRef, useState } from "react"
|
||
|
||
import cs from "classnames"
|
||
|
||
import { uploadFileWithProgress } from "@shared/api/uploadFile"
|
||
import { useWizard } from "@shared/context/WizardContext"
|
||
import { Button } from "@shared/ui"
|
||
|
||
import styles from "./UploadStep.module.scss"
|
||
|
||
const ACCEPTED_VIDEO_TYPES = "video/*"
|
||
const ERROR_UPLOAD_FAILED = "Не удалось загрузить файл"
|
||
|
||
export const UploadStep: FunctionComponent<IUploadStepProps> = ({
|
||
className,
|
||
}): JSX.Element => {
|
||
const { projectId, setFileKey } = useWizard()
|
||
const [isDragging, setIsDragging] = useState(false)
|
||
const [isUploading, setIsUploading] = useState(false)
|
||
const [progress, setProgress] = useState(0)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const inputRef = useRef<HTMLInputElement>(null)
|
||
|
||
const handleUpload = useCallback(
|
||
async (file: File) => {
|
||
setIsUploading(true)
|
||
setProgress(0)
|
||
setError(null)
|
||
|
||
try {
|
||
const result = await uploadFileWithProgress(
|
||
file,
|
||
`projects/${projectId}`,
|
||
setProgress,
|
||
)
|
||
await setFileKey(
|
||
result.file_path,
|
||
result.file_id,
|
||
result.filename ?? null,
|
||
)
|
||
} catch {
|
||
setError(ERROR_UPLOAD_FAILED)
|
||
} finally {
|
||
setIsUploading(false)
|
||
}
|
||
},
|
||
[projectId, setFileKey],
|
||
)
|
||
|
||
const handleFileChange = useCallback(
|
||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0]
|
||
if (file) handleUpload(file)
|
||
/* Reset input so re-selecting the same file triggers change */
|
||
e.target.value = ""
|
||
},
|
||
[handleUpload],
|
||
)
|
||
|
||
const handleDrop = useCallback(
|
||
(e: React.DragEvent) => {
|
||
e.preventDefault()
|
||
setIsDragging(false)
|
||
const file = e.dataTransfer.files[0]
|
||
if (file) handleUpload(file)
|
||
},
|
||
[handleUpload],
|
||
)
|
||
|
||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||
e.preventDefault()
|
||
setIsDragging(true)
|
||
}, [])
|
||
|
||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||
e.preventDefault()
|
||
setIsDragging(false)
|
||
}, [])
|
||
|
||
return (
|
||
<div className={cs(styles.root, className)} data-testid="UploadStep">
|
||
<div className={styles.content}>
|
||
<div
|
||
className={cs(styles.dropZone, {
|
||
[styles.dropZoneActive]: isDragging,
|
||
[styles.dropZoneUploading]: isUploading,
|
||
})}
|
||
onDrop={handleDrop}
|
||
onDragOver={handleDragOver}
|
||
onDragLeave={handleDragLeave}
|
||
onClick={() => inputRef.current?.click()}
|
||
>
|
||
<input
|
||
ref={inputRef}
|
||
type="file"
|
||
accept={ACCEPTED_VIDEO_TYPES}
|
||
className={styles.fileInput}
|
||
onChange={handleFileChange}
|
||
disabled={isUploading}
|
||
/>
|
||
<Upload size={48} className={styles.icon} />
|
||
|
||
{isUploading ? (
|
||
<>
|
||
<p className={styles.title}>Загрузка файла...</p>
|
||
<div className={styles.progressTrack}>
|
||
<div
|
||
className={styles.progressBar}
|
||
style={{ width: `${progress}%` }}
|
||
/>
|
||
</div>
|
||
<p className={styles.progressLabel}>{Math.round(progress)}%</p>
|
||
</>
|
||
) : (
|
||
<>
|
||
<p className={styles.title}>Перетащите видеофайл сюда</p>
|
||
<p className={styles.subtitle}>или нажмите для выбора файла</p>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
type="button"
|
||
onClick={(e) => {
|
||
e.stopPropagation()
|
||
inputRef.current?.click()
|
||
}}
|
||
>
|
||
Выбрать файл
|
||
</Button>
|
||
</>
|
||
)}
|
||
|
||
{error && <p className={styles.error}>{error}</p>}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|