feat: added app routing
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
@import '../../../../../styles/design-tokens';
|
||||
|
||||
.zone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 24px;
|
||||
border: 1.5px dashed $gray-300;
|
||||
border-radius: $r-md;
|
||||
color: $gray-400;
|
||||
font-size: 12.5px;
|
||||
cursor: pointer;
|
||||
transition: .18s;
|
||||
|
||||
&:hover,
|
||||
&.over {
|
||||
border-color: $gold-500;
|
||||
color: $gold-600;
|
||||
background: $gold-50;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
margin: 10px 0 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: $r-sm;
|
||||
background: $gray-50;
|
||||
font-size: 13px;
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.size {
|
||||
color: $gray-400;
|
||||
font-size: 11.5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.remove {
|
||||
display: inline-flex;
|
||||
padding: 2px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: $gray-400;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: $red-500;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
import styles from './FileDropzone.module.scss';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
interface IFileDropzoneProps {
|
||||
files: File[];
|
||||
onChange: (files: File[]) => void;
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} Б`;
|
||||
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} КБ`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} МБ`;
|
||||
}
|
||||
|
||||
/** Зона выбора файлов (перетащить/кликнуть) со списком выбранных и удалением. */
|
||||
export const FileDropzone: React.FC<IFileDropzoneProps> = ({ files, onChange }) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [dragOver, setDragOver] = useState<boolean>(false);
|
||||
|
||||
const addFiles = (incoming: FileList | null): void => {
|
||||
if (!incoming || incoming.length === 0) {
|
||||
return;
|
||||
}
|
||||
onChange([...files, ...Array.from(incoming)]);
|
||||
};
|
||||
|
||||
const removeAt = (index: number): void => {
|
||||
onChange(files.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const zoneClasses = [styles.zone, dragOver ? styles.over : ''].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={zoneClasses}
|
||||
onClick={() => inputRef.current && inputRef.current.click()}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setDragOver(true);
|
||||
}}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
addFiles(e.dataTransfer.files);
|
||||
}}
|
||||
>
|
||||
<Icon name="upload" size="sm" />
|
||||
<span>Нажмите или перетащите файлы сюда</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className={styles.hidden}
|
||||
onChange={(e) => {
|
||||
addFiles(e.target.files);
|
||||
e.target.value = ''; // чтобы можно было выбрать тот же файл повторно
|
||||
}}
|
||||
/>
|
||||
|
||||
{files.length > 0 && (
|
||||
<ul className={styles.list}>
|
||||
{files.map((file, index) => (
|
||||
<li key={`${file.name}-${index}`} className={styles.item}>
|
||||
<Icon name="document" size="sm" />
|
||||
<span className={styles.name}>{file.name}</span>
|
||||
<span className={styles.size}>{formatSize(file.size)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.remove}
|
||||
aria-label="Удалить файл"
|
||||
onClick={() => removeAt(index)}
|
||||
>
|
||||
<Icon name="close" size="sm" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user