mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 23:35:44 +00:00
refactor(modal): migrate CsvImportReportModal onto the Modal primitive
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user