feat(frontend): add remove button to pending and submitted file cards
Lets users drop an uploaded file from the UI list before or after conversion, without cancelling anything server-side. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CircleNotch, DownloadSimple, CheckCircle, WarningCircle } from '@phosphor-icons/react';
|
import { CircleNotch, DownloadSimple, CheckCircle, WarningCircle, X } from '@phosphor-icons/react';
|
||||||
import { fetchJobStatus, downloadUrl } from './api.js';
|
import { fetchJobStatus, downloadUrl } from './api.js';
|
||||||
import { formatBytes } from './utils/formatBytes.js';
|
import { formatBytes } from './utils/formatBytes.js';
|
||||||
import { fileTypeMeta } from './utils/fileFamily.js';
|
import { fileTypeMeta } from './utils/fileFamily.js';
|
||||||
@@ -9,7 +9,7 @@ function extensionOf(fileName) {
|
|||||||
return fileName.split('.').pop();
|
return fileName.split('.').pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileCard({ fileName, fileSize, targetFormat, jobId, initialError }) {
|
export function FileCard({ fileName, fileSize, targetFormat, jobId, initialError, onRemove }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [status, setStatus] = useState(initialError ? 'failed' : 'pending');
|
const [status, setStatus] = useState(initialError ? 'failed' : 'pending');
|
||||||
const [errorMessage, setErrorMessage] = useState(initialError ?? null);
|
const [errorMessage, setErrorMessage] = useState(initialError ?? null);
|
||||||
@@ -45,6 +45,14 @@ export function FileCard({ fileName, fileSize, targetFormat, jobId, initialError
|
|||||||
<span className="file-tile-name">{fileName}</span>
|
<span className="file-tile-name">{fileName}</span>
|
||||||
{targetFormat && <span className="format-badge">{targetFormat.toUpperCase()}</span>}
|
{targetFormat && <span className="format-badge">{targetFormat.toUpperCase()}</span>}
|
||||||
<span className="file-tile-size">{formatBytes(fileSize)}</span>
|
<span className="file-tile-size">{formatBytes(fileSize)}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="file-remove-button"
|
||||||
|
onClick={onRemove}
|
||||||
|
aria-label={t('common.remove')}
|
||||||
|
>
|
||||||
|
<X size={16} weight="bold" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{(status === 'pending' || status === 'processing') && (
|
{(status === 'pending' || status === 'processing') && (
|
||||||
<div className="job-progress">
|
<div className="job-progress">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { X } from '@phosphor-icons/react';
|
||||||
import { formatBytes } from '../utils/formatBytes.js';
|
import { formatBytes } from '../utils/formatBytes.js';
|
||||||
import { fileTypeMeta } from '../utils/fileFamily.js';
|
import { fileTypeMeta } from '../utils/fileFamily.js';
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ function RangeField({ label, value, min, max, onChange }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileConfigCard({ item, index, t, onTargetFormatChange, onQualityChange, onIconSizeChange }) {
|
export function FileConfigCard({ item, index, t, onTargetFormatChange, onQualityChange, onIconSizeChange, onRemove }) {
|
||||||
const { Icon, colorVar } = fileTypeMeta(extensionOf(item.file.name));
|
const { Icon, colorVar } = fileTypeMeta(extensionOf(item.file.name));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,6 +44,14 @@ export function FileConfigCard({ item, index, t, onTargetFormatChange, onQuality
|
|||||||
</span>
|
</span>
|
||||||
<span className="file-tile-name">{item.file.name}</span>
|
<span className="file-tile-name">{item.file.name}</span>
|
||||||
<span className="file-tile-size">{formatBytes(item.file.size)}</span>
|
<span className="file-tile-size">{formatBytes(item.file.size)}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="file-remove-button"
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
|
aria-label={t('common.remove')}
|
||||||
|
>
|
||||||
|
<X size={16} weight="bold" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{item.targets.length > 0 ? (
|
{item.targets.length > 0 ? (
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"brand": "Ombrora Convert",
|
"brand": "Ombrora Convert",
|
||||||
|
"common": {
|
||||||
|
"remove": "Remove"
|
||||||
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"switchToEnglish": "English",
|
"switchToEnglish": "English",
|
||||||
"switchToFrench": "Français",
|
"switchToFrench": "Français",
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"brand": "Ombrora Convert",
|
"brand": "Ombrora Convert",
|
||||||
|
"common": {
|
||||||
|
"remove": "Supprimer"
|
||||||
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"switchToEnglish": "English",
|
"switchToEnglish": "English",
|
||||||
"switchToFrench": "Français",
|
"switchToFrench": "Français",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function HomePage() {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
setPendingFiles(withTargets);
|
setPendingFiles((current) => [...current, ...withTargets]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTargetFormat(index, targetFormat) {
|
function updateTargetFormat(index, targetFormat) {
|
||||||
@@ -78,6 +78,14 @@ export function HomePage() {
|
|||||||
setPendingFiles((current) => current.map((item, i) => (i === index ? { ...item, iconSize } : item)));
|
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() {
|
async function handleConvert() {
|
||||||
const validItems = pendingFiles.filter((item) => item.targetFormat);
|
const validItems = pendingFiles.filter((item) => item.targetFormat);
|
||||||
const jobs = await uploadFiles(validItems);
|
const jobs = await uploadFiles(validItems);
|
||||||
@@ -114,6 +122,7 @@ export function HomePage() {
|
|||||||
onTargetFormatChange={updateTargetFormat}
|
onTargetFormatChange={updateTargetFormat}
|
||||||
onQualityChange={updateQuality}
|
onQualityChange={updateQuality}
|
||||||
onIconSizeChange={updateIconSize}
|
onIconSizeChange={updateIconSize}
|
||||||
|
onRemove={removePendingFile}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -132,6 +141,7 @@ export function HomePage() {
|
|||||||
fileSize={job.size}
|
fileSize={job.size}
|
||||||
targetFormat={job.targetFormat}
|
targetFormat={job.targetFormat}
|
||||||
jobId={job.id}
|
jobId={job.id}
|
||||||
|
onRemove={() => removeSubmittedJob(index)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FileCard
|
<FileCard
|
||||||
@@ -140,6 +150,7 @@ export function HomePage() {
|
|||||||
fileSize={job.size}
|
fileSize={job.size}
|
||||||
targetFormat={job.targetFormat}
|
targetFormat={job.targetFormat}
|
||||||
initialError={job.error}
|
initialError={job.error}
|
||||||
|
onRemove={() => removeSubmittedJob(index)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -215,6 +215,28 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-remove-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-foreground);
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 150ms ease, background 150ms ease, color 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-remove-button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--color-destructive);
|
||||||
|
background: color-mix(in srgb, var(--color-destructive) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.file-config-controls {
|
.file-config-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user