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 { 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 { formatBytes } from './utils/formatBytes.js';
|
||||
import { fileTypeMeta } from './utils/fileFamily.js';
|
||||
@@ -9,7 +9,7 @@ function extensionOf(fileName) {
|
||||
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 [status, setStatus] = useState(initialError ? 'failed' : 'pending');
|
||||
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>
|
||||
{targetFormat && <span className="format-badge">{targetFormat.toUpperCase()}</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>
|
||||
{(status === 'pending' || status === 'processing') && (
|
||||
<div className="job-progress">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { X } from '@phosphor-icons/react';
|
||||
import { formatBytes } from '../utils/formatBytes.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));
|
||||
|
||||
return (
|
||||
@@ -43,6 +44,14 @@ export function FileConfigCard({ item, index, t, onTargetFormatChange, onQuality
|
||||
</span>
|
||||
<span className="file-tile-name">{item.file.name}</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>
|
||||
|
||||
{item.targets.length > 0 ? (
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"brand": "Ombrora Convert",
|
||||
"common": {
|
||||
"remove": "Remove"
|
||||
},
|
||||
"nav": {
|
||||
"switchToEnglish": "English",
|
||||
"switchToFrench": "Français",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"brand": "Ombrora Convert",
|
||||
"common": {
|
||||
"remove": "Supprimer"
|
||||
},
|
||||
"nav": {
|
||||
"switchToEnglish": "English",
|
||||
"switchToFrench": "Français",
|
||||
|
||||
@@ -52,7 +52,7 @@ export function HomePage() {
|
||||
};
|
||||
})
|
||||
);
|
||||
setPendingFiles(withTargets);
|
||||
setPendingFiles((current) => [...current, ...withTargets]);
|
||||
}
|
||||
|
||||
function updateTargetFormat(index, targetFormat) {
|
||||
@@ -78,6 +78,14 @@ export function HomePage() {
|
||||
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);
|
||||
@@ -114,6 +122,7 @@ export function HomePage() {
|
||||
onTargetFormatChange={updateTargetFormat}
|
||||
onQualityChange={updateQuality}
|
||||
onIconSizeChange={updateIconSize}
|
||||
onRemove={removePendingFile}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
@@ -132,6 +141,7 @@ export function HomePage() {
|
||||
fileSize={job.size}
|
||||
targetFormat={job.targetFormat}
|
||||
jobId={job.id}
|
||||
onRemove={() => removeSubmittedJob(index)}
|
||||
/>
|
||||
) : (
|
||||
<FileCard
|
||||
@@ -140,6 +150,7 @@ export function HomePage() {
|
||||
fileSize={job.size}
|
||||
targetFormat={job.targetFormat}
|
||||
initialError={job.error}
|
||||
onRemove={() => removeSubmittedJob(index)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -215,6 +215,28 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user