172 lines
5.2 KiB
React
172 lines
5.2 KiB
React
import { useState } from 'react';
|
|
import { useParams } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { fetchFormats, uploadFiles } from '../api.js';
|
|
import { FileCard } from '../FileCard.jsx';
|
|
import { Dropzone } from '../components/Dropzone.jsx';
|
|
import { FileConfigCard } from '../components/FileConfigCard.jsx';
|
|
import { ReassuranceStrip } from '../components/ReassuranceStrip.jsx';
|
|
import { FormatsGrid } from '../components/FormatsGrid.jsx';
|
|
import { FormatMarquee } from '../components/FormatMarquee.jsx';
|
|
import { SeoHead } from '../components/SeoHead.jsx';
|
|
import { extensionOf } from '../utils/archiveExtensions.js';
|
|
import '../styles/home.css';
|
|
import '../styles/sections.css';
|
|
|
|
const DEFAULT_QUALITY = {
|
|
jpg: 80,
|
|
jpeg: 80,
|
|
webp: 80,
|
|
avif: 50,
|
|
tiff: 80,
|
|
png: 6,
|
|
zip: 6,
|
|
'tar.gz': 6,
|
|
'tar.bz2': 9,
|
|
'7z': 5,
|
|
'tar.7z': 5,
|
|
mp3: 192,
|
|
ogg: 192,
|
|
aac: 192,
|
|
m4a: 192,
|
|
};
|
|
|
|
const DEFAULT_ICON_SIZE = 256;
|
|
|
|
function defaultQualityFor(targetFormat) {
|
|
return DEFAULT_QUALITY[targetFormat] ?? null;
|
|
}
|
|
|
|
export function HomePage() {
|
|
const { lang } = useParams();
|
|
const { t } = useTranslation();
|
|
const [pendingFiles, setPendingFiles] = useState([]);
|
|
const [submittedJobs, setSubmittedJobs] = useState([]);
|
|
|
|
async function handleFilesSelected(fileList) {
|
|
const files = Array.from(fileList);
|
|
const withTargets = await Promise.all(
|
|
files.map(async (file) => {
|
|
const targets = await fetchFormats(extensionOf(file.name));
|
|
const targetFormat = targets[0] ?? null;
|
|
return {
|
|
file,
|
|
targets,
|
|
targetFormat,
|
|
quality: defaultQualityFor(targetFormat),
|
|
iconSize: targetFormat === 'ico' ? DEFAULT_ICON_SIZE : null,
|
|
};
|
|
})
|
|
);
|
|
setPendingFiles((current) => [...current, ...withTargets]);
|
|
}
|
|
|
|
function updateTargetFormat(index, targetFormat) {
|
|
setPendingFiles((current) =>
|
|
current.map((item, i) =>
|
|
i === index
|
|
? {
|
|
...item,
|
|
targetFormat,
|
|
quality: defaultQualityFor(targetFormat),
|
|
iconSize: targetFormat === 'ico' ? DEFAULT_ICON_SIZE : null,
|
|
}
|
|
: item
|
|
)
|
|
);
|
|
}
|
|
|
|
function updateQuality(index, quality) {
|
|
setPendingFiles((current) => current.map((item, i) => (i === index ? { ...item, quality } : item)));
|
|
}
|
|
|
|
function updateIconSize(index, iconSize) {
|
|
setPendingFiles((current) => current.map((item, i) => (i === index ? { ...item, iconSize } : item)));
|
|
}
|
|
|
|
function removePendingFile(index) {
|
|
setPendingFiles((current) => current.filter((_, i) => i !== index));
|
|
}
|
|
|
|
function removeSubmittedJob(index) {
|
|
setSubmittedJobs((current) => current.filter((_, i) => i !== index));
|
|
}
|
|
|
|
async function handleConvert() {
|
|
const validItems = pendingFiles.filter((item) => item.targetFormat);
|
|
const jobs = await uploadFiles(validItems);
|
|
const jobsWithSize = jobs.map((job, i) => ({
|
|
...job,
|
|
size: validItems[i].file.size,
|
|
targetFormat: validItems[i].targetFormat,
|
|
}));
|
|
setSubmittedJobs((current) => [...current, ...jobsWithSize]);
|
|
setPendingFiles([]);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<SeoHead lang={lang} />
|
|
<section className="hero">
|
|
<div className="hero-aurora" aria-hidden="true" />
|
|
<div className="hero-content">
|
|
<span className="hero-kicker">{t('hero.kicker')}</span>
|
|
<h1 className="hero-title">{t('hero.title')}</h1>
|
|
<p>{t('hero.subtitle')}</p>
|
|
<Dropzone label={t('hero.dropzoneLabel')} onFilesSelected={handleFilesSelected} />
|
|
<FormatMarquee />
|
|
|
|
{pendingFiles.length > 0 && (
|
|
<div className="pending-files">
|
|
<ul>
|
|
{pendingFiles.map((item, index) => (
|
|
<FileConfigCard
|
|
key={`${item.file.name}-${index}`}
|
|
item={item}
|
|
index={index}
|
|
t={t}
|
|
onTargetFormatChange={updateTargetFormat}
|
|
onQualityChange={updateQuality}
|
|
onIconSizeChange={updateIconSize}
|
|
onRemove={removePendingFile}
|
|
/>
|
|
))}
|
|
</ul>
|
|
<button className="convert-button" onClick={handleConvert}>
|
|
{t('hero.convert')}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<ul className="job-list">
|
|
{submittedJobs.map((job, index) =>
|
|
job.id ? (
|
|
<FileCard
|
|
key={job.id}
|
|
fileName={job.file}
|
|
fileSize={job.size}
|
|
targetFormat={job.targetFormat}
|
|
jobId={job.id}
|
|
onRemove={() => removeSubmittedJob(index)}
|
|
/>
|
|
) : (
|
|
<FileCard
|
|
key={`${job.file}-${index}`}
|
|
fileName={job.file}
|
|
fileSize={job.size}
|
|
targetFormat={job.targetFormat}
|
|
initialError={job.error}
|
|
onRemove={() => removeSubmittedJob(index)}
|
|
/>
|
|
)
|
|
)}
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<ReassuranceStrip />
|
|
<FormatsGrid />
|
|
</>
|
|
);
|
|
}
|