refactor(modal): migrate CsvImportReportModal onto the Modal primitive

This commit is contained in:
Psychotoxical
2026-06-21 02:23:29 +02:00
parent e9d9365110
commit 72af9f7560
+90 -120
View File
@@ -1,9 +1,8 @@
import React from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { Download, X } from 'lucide-react';
import { Download } from 'lucide-react';
import { showToast } from '../../utils/ui/toast';
import type { SpotifyCsvTrack } from '../../utils/playlist/spotifyCsvImport';
import Modal from '../Modal';
interface CsvReportModalProps {
report: {
@@ -20,9 +19,6 @@ interface CsvReportModalProps {
export default function CsvImportReportModal({ report, playlistName, onClose }: CsvReportModalProps) {
const { t } = useTranslation();
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) onClose();
};
const downloadReport = () => {
try {
@@ -59,127 +55,101 @@ export default function CsvImportReportModal({ report, playlistName, onClose }:
}
};
return createPortal(
<div
className="modal-overlay modal-overlay--csv"
onClick={handleOverlayClick}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 99999,
}}
>
<div
className="modal-content"
style={{
maxWidth: 500,
maxHeight: '80vh',
width: '90%',
display: 'flex',
flexDirection: 'column',
position: 'relative',
}}
onClick={e => e.stopPropagation()}
>
<button className="btn btn-ghost modal-close" onClick={onClose} style={{ position: 'absolute', top: 16, right: 16 }}>
<X size={18} />
</button>
<h2 className="modal-title" style={{ fontSize: 20, marginBottom: 16 }}>{t('playlists.csvImportReport')}</h2>
<div style={{ display: 'grid', gridTemplateColumns: report.searchErrors && report.searchErrors.length > 0 ? 'repeat(5, 1fr)' : 'repeat(4, 1fr)', gap: 12, marginBottom: 20, textAlign: 'center' }}>
<div style={{ padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 700, color: 'var(--text)' }}>{report.total}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('playlists.csvImportTotal')}</div>
</div>
<div style={{ padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 700, color: 'var(--accent)' }}>{report.added}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('playlists.csvImportAdded')}</div>
</div>
<div style={{ padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 700, color: 'var(--text-muted)' }}>{report.duplicates}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('playlists.csvImportDuplicates')}</div>
</div>
<div style={{ padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 700, color: report.notFound.length > 0 ? '#ff6b6b' : 'var(--text-muted)' }}>{report.notFound.length}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('playlists.csvImportNotFound')}</div>
</div>
{report.searchErrors && report.searchErrors.length > 0 && (
<div style={{ padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 700, color: '#ffa500' }}>{report.searchErrors.length}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('playlists.csvImportNetworkErrors')}</div>
</div>
)}
</div>
{report.duplicateTracks.length > 0 && (
<>
<h3 style={{ fontSize: 14, marginBottom: 8, color: 'var(--text-muted)' }}>{t('playlists.csvImportDuplicatesTitle')}</h3>
<div style={{ overflowY: 'auto', maxHeight: 150, marginBottom: 16, border: '1px solid var(--surface)', borderRadius: 8 }}>
{report.duplicateTracks.map((track, i) => (
<div key={i} style={{ padding: '8px 12px', borderBottom: '1px solid var(--surface)', fontSize: 13 }}>
<div style={{ fontWeight: 500 }}>{track.trackName}</div>
<div style={{ color: 'var(--text-muted)' }}>{track.artistName}</div>
</div>
))}
</div>
</>
)}
{report.notFound.length > 0 && (
<>
<h3 style={{ fontSize: 14, marginBottom: 8, color: 'var(--text-muted)' }}>{t('playlists.csvImportNotFoundTitle')}</h3>
<div style={{ overflowY: 'auto', maxHeight: 200, marginBottom: 16, border: '1px solid var(--surface)', borderRadius: 8 }}>
{report.notFound.map((track, i) => (
<div key={i} style={{ padding: '8px 12px', borderBottom: '1px solid var(--surface)', fontSize: 13 }}>
<div style={{ fontWeight: 500 }}>{track.trackName}</div>
<div style={{ color: 'var(--text-muted)' }}>{track.artistName}</div>
{track.albumName && <div style={{ color: 'var(--text-muted)', fontSize: 11 }}>{track.albumName}</div>}
{track.score !== undefined && (
<div style={{ fontSize: 11, marginTop: 2 }}>
<span style={{ color: 'var(--text-muted)' }}>Score: </span>
<span style={{ color: track.score >= (track.thresholdNeeded ?? 0.6) ? '#4ade80' : '#f87171' }}>
{track.score.toFixed(2)}
</span>
<span style={{ color: 'var(--text-muted)' }}> (threshold: {(track.thresholdNeeded ?? 0.6).toFixed(2)})</span>
</div>
)}
</div>
))}
</div>
</>
)}
{report.searchErrors && report.searchErrors.length > 0 && (
<>
<h3 style={{ fontSize: 14, marginBottom: 8, color: '#ffa500' }}>{t('playlists.csvImportNetworkErrorsTitle')}</h3>
<div style={{ overflowY: 'auto', maxHeight: 150, marginBottom: 16, border: '1px solid var(--surface)', borderRadius: 8 }}>
{report.searchErrors.map((track, i) => (
<div key={i} style={{ padding: '8px 12px', borderBottom: '1px solid var(--surface)', fontSize: 13 }}>
<div style={{ fontWeight: 500 }}>{track.trackName}</div>
<div style={{ color: 'var(--text-muted)' }}>{track.artistName}</div>
</div>
))}
</div>
</>
)}
<div className="modal-actions" style={{ marginTop: 'auto', display: 'flex', justifyContent: 'flex-end', gap: 12 }}>
return (
<Modal
open
onClose={onClose}
title={t('playlists.csvImportReport')}
size="md"
closeLabel={t('playlists.csvImportClose')}
bodyClassName="ui-modal-body--padded"
footer={
<>
<button className="btn btn-surface" onClick={downloadReport}>
<Download size={14} /> {t('playlists.csvImportDownloadReport')}
</button>
<button className="btn btn-primary" onClick={onClose}>
{t('playlists.csvImportClose')}
</button>
</>
}
>
<div style={{ display: 'grid', gridTemplateColumns: report.searchErrors && report.searchErrors.length > 0 ? 'repeat(5, 1fr)' : 'repeat(4, 1fr)', gap: 12, marginBottom: 20, textAlign: 'center' }}>
<div style={{ padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 700, color: 'var(--text)' }}>{report.total}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('playlists.csvImportTotal')}</div>
</div>
<div style={{ padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 700, color: 'var(--accent)' }}>{report.added}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('playlists.csvImportAdded')}</div>
</div>
<div style={{ padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 700, color: 'var(--text-muted)' }}>{report.duplicates}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('playlists.csvImportDuplicates')}</div>
</div>
<div style={{ padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 700, color: report.notFound.length > 0 ? '#ff6b6b' : 'var(--text-muted)' }}>{report.notFound.length}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('playlists.csvImportNotFound')}</div>
</div>
{report.searchErrors && report.searchErrors.length > 0 && (
<div style={{ padding: 12, background: 'var(--surface)', borderRadius: 8 }}>
<div style={{ fontSize: 24, fontWeight: 700, color: '#ffa500' }}>{report.searchErrors.length}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('playlists.csvImportNetworkErrors')}</div>
</div>
)}
</div>
</div>,
document.body
{report.duplicateTracks.length > 0 && (
<>
<h3 style={{ fontSize: 14, marginBottom: 8, color: 'var(--text-muted)' }}>{t('playlists.csvImportDuplicatesTitle')}</h3>
<div style={{ overflowY: 'auto', maxHeight: 150, marginBottom: 16, border: '1px solid var(--surface)', borderRadius: 8 }}>
{report.duplicateTracks.map((track, i) => (
<div key={i} style={{ padding: '8px 12px', borderBottom: '1px solid var(--surface)', fontSize: 13 }}>
<div style={{ fontWeight: 500 }}>{track.trackName}</div>
<div style={{ color: 'var(--text-muted)' }}>{track.artistName}</div>
</div>
))}
</div>
</>
)}
{report.notFound.length > 0 && (
<>
<h3 style={{ fontSize: 14, marginBottom: 8, color: 'var(--text-muted)' }}>{t('playlists.csvImportNotFoundTitle')}</h3>
<div style={{ overflowY: 'auto', maxHeight: 200, marginBottom: 16, border: '1px solid var(--surface)', borderRadius: 8 }}>
{report.notFound.map((track, i) => (
<div key={i} style={{ padding: '8px 12px', borderBottom: '1px solid var(--surface)', fontSize: 13 }}>
<div style={{ fontWeight: 500 }}>{track.trackName}</div>
<div style={{ color: 'var(--text-muted)' }}>{track.artistName}</div>
{track.albumName && <div style={{ color: 'var(--text-muted)', fontSize: 11 }}>{track.albumName}</div>}
{track.score !== undefined && (
<div style={{ fontSize: 11, marginTop: 2 }}>
<span style={{ color: 'var(--text-muted)' }}>Score: </span>
<span style={{ color: track.score >= (track.thresholdNeeded ?? 0.6) ? '#4ade80' : '#f87171' }}>
{track.score.toFixed(2)}
</span>
<span style={{ color: 'var(--text-muted)' }}> (threshold: {(track.thresholdNeeded ?? 0.6).toFixed(2)})</span>
</div>
)}
</div>
))}
</div>
</>
)}
{report.searchErrors && report.searchErrors.length > 0 && (
<>
<h3 style={{ fontSize: 14, marginBottom: 8, color: '#ffa500' }}>{t('playlists.csvImportNetworkErrorsTitle')}</h3>
<div style={{ overflowY: 'auto', maxHeight: 150, marginBottom: 16, border: '1px solid var(--surface)', borderRadius: 8 }}>
{report.searchErrors.map((track, i) => (
<div key={i} style={{ padding: '8px 12px', borderBottom: '1px solid var(--surface)', fontSize: 13 }}>
<div style={{ fontWeight: 500 }}>{track.trackName}</div>
<div style={{ color: 'var(--text-muted)' }}>{track.artistName}</div>
</div>
))}
</div>
</>
)}
</Modal>
);
}