PATH:
home
/
cf7x
/
public_html
/
wp-content_
/
plugins
/
email-validator
/
src
import React from 'react'; import { render } from '@wordpress/element'; import { useState, useEffect, useCallback, useRef } from 'react'; import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Button, Pagination, Stack, CircularProgress, LinearProgress } from '@mui/material'; import RunValidationApp from './run-validation.jsx'; import { TextField, FormControl, InputLabel, Select, MenuItem } from '@mui/material'; import Skeleton from 'react-loading-skeleton'; import 'react-loading-skeleton/dist/skeleton.css'; import { __ } from '@wordpress/i18n'; import { FirstPage, LastPage, NavigateBefore, NavigateNext } from '@mui/icons-material'; const EVA_DATA = window.EVA || {}; const REST_BASE = EVA_DATA.restBase || window.location.origin; const formatBytes = (bytes) => { if (bytes === undefined || bytes === null) { return ''; } const size = Number(bytes); if (!Number.isFinite(size) || size <= 0) { return '0 B'; } const units = ['B', 'KB', 'MB', 'GB', 'TB']; const idx = Math.min(units.length - 1, Math.floor(Math.log(size) / Math.log(1024))); const value = size / Math.pow(1024, idx); return `${value.toFixed(1)} ${units[idx]}`; }; const formatNumber = (value) => { const parsed = Number(value); if (!Number.isFinite(parsed)) { return '0'; } return parsed.toLocaleString(); }; const initialAlert = { type: null, text: '' }; const ImportApp = () => { const [uploadedFiles, setUploadedFiles] = useState([]); const [filesLoading, setFilesLoading] = useState(true); const [deletingIndex, setDeletingIndex] = useState(null); const fetchUploadedFiles = useCallback(() => { setFilesLoading(true); fetch(EVA_DATA.ajaxUrl + '?action=eva_list_uploaded_files&nonce=' + EVA_DATA.nonce) .then(res => res.json()) .then(data => { console.log('EVA_DEBUG: Uploaded files response', data); const files = (data.data && Array.isArray(data.data.files)) ? data.data.files : (Array.isArray(data.files) ? data.files : []); setUploadedFiles(files); setFilesLoading(false); }); }, []); useEffect(() => { fetchUploadedFiles(); }, [fetchUploadedFiles]); const [file, setFile] = useState(null); const [fileLabel, setFileLabel] = useState(''); const [hasHeader, setHasHeader] = useState(true); const [turbo, setTurbo] = useState(false); const [uploadPercent, setUploadPercent] = useState(0); const [importPercent, setImportPercent] = useState(0); const [stats, setStats] = useState({ processed: 0, inserted: 0, inqueue: 0, total: 0, failed: 0, duplicates: 0 }); const [totalRows, setTotalRows] = useState(0); const [token, setToken] = useState(''); const [isUploading, setIsUploading] = useState(false); const [isImporting, setIsImporting] = useState(false); const [isDragging, setIsDragging] = useState(false); const [alert, setAlert] = useState(initialAlert); const turboSupported = typeof EVA_DATA.turboSupported === 'boolean' ? EVA_DATA.turboSupported : true; const uploadXhrRef = useRef(null); const pollTimeoutRef = useRef(null); const cancelRequestedRef = useRef(false); const fileInputRef = useRef(null); const resetAsyncState = useCallback(() => { if (uploadXhrRef.current) { uploadXhrRef.current.abort(); uploadXhrRef.current = null; } if (pollTimeoutRef.current) { clearTimeout(pollTimeoutRef.current); pollTimeoutRef.current = null; } cancelRequestedRef.current = false; }, []); useEffect(() => () => { resetAsyncState(); }, [resetAsyncState]); const clearProgress = useCallback(() => { setUploadPercent(0); setImportPercent(0); setStats({ processed: 0, inserted: 0, inqueue: 0, total: 0, failed: 0, duplicates: 0 }); setToken(''); setTotalRows(0); setIsUploading(false); setIsImporting(false); }, [setStats]); const setAlertMessage = useCallback((type, text) => { setAlert({ type, text }); }, []); const clearAlert = useCallback(() => { setAlert(initialAlert); }, []); const acceptFile = useCallback((candidate, resetInput = false) => { if (!candidate) { return false; } if (!/\.csv$/i.test(candidate.name || '')) { setAlertMessage('error', __('Please choose a CSV file.', 'email-validator')); return false; } resetAsyncState(); clearProgress(); clearAlert(); setFile(candidate); setFileLabel(`${candidate.name} • ${formatBytes(candidate.size)}`); if (resetInput && fileInputRef.current) { fileInputRef.current.value = ''; } return true; }, [setAlertMessage, resetAsyncState, clearProgress, clearAlert]); const handleDrop = useCallback((event) => { event.preventDefault(); event.stopPropagation(); setIsDragging(false); const dropped = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]; acceptFile(dropped, true); }, [acceptFile]); useEffect(() => { if (!turboSupported && turbo) { setTurbo(false); } }, [turboSupported, turbo]); const pollImport = useCallback((currentToken) => { if (!currentToken || cancelRequestedRef.current) { setIsImporting(false); return; } const params = new URLSearchParams(); params.append('action', 'eva_import_batch'); params.append('nonce', EVA_DATA.nonce || ''); params.append('token', currentToken); params.append('batch', '10000'); fetch(EVA_DATA.ajaxUrl || '', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', }, body: params.toString(), }) .then((response) => response.json()) .then((payload) => { if (!payload || !payload.success) { const message = payload && payload.data && payload.data.message ? payload.data.message : __('Import failed.', 'email-validator'); setAlertMessage('error', message); setIsImporting(false); return; } const data = payload.data || {}; if (data.notice) { setAlertMessage('warning', data.notice); } const percent = Number(data.percent); if (Number.isFinite(percent)) { setImportPercent(Math.max(0, Math.min(100, percent))); } const processed = Number(data.processed) || 0; const inserted = Number(data.inserted) || 0; const updated = Number(data.updated) || 0; const duplicates = Number(data.duplicates) || 0; const failed = Number(data.failed) || 0; const csv_total = Number(data.csv_total) || 0; setStats({ processed, inserted, updated, duplicates, failed, csv_total, total: Number(data.total) || 0, remaining: Number(data.remaining) || 0, percent: Number(data.percent) || 0, }); if (data.done) { setIsImporting(false); setAlertMessage('success', __('Import completed successfully.', 'email-validator')); fetchUploadedFiles(); // Refresh uploaded files list after import return; } pollTimeoutRef.current = setTimeout(() => pollImport(currentToken), 150); }) .catch(() => { setAlertMessage('error', __('Import failed.', 'email-validator')); setIsImporting(false); }); }, [setAlertMessage, totalRows, setTotalRows, setStats, fetchUploadedFiles]); const handleStart = useCallback(() => { if (!file || isUploading || isImporting) { return; } clearAlert(); cancelRequestedRef.current = false; setIsUploading(true); setUploadPercent(0); const xhr = new XMLHttpRequest(); uploadXhrRef.current = xhr; xhr.upload.addEventListener('progress', (e) => { if (!e.lengthComputable) { return; } const pct = Math.round((e.loaded / e.total) * 100); setUploadPercent(Number.isFinite(pct) ? pct : 0); }); xhr.addEventListener('error', () => { uploadXhrRef.current = null; setIsUploading(false); if (!cancelRequestedRef.current) { setAlertMessage('error', __('Upload failed.', 'email-validator')); } }); xhr.onreadystatechange = () => { if (xhr.readyState !== XMLHttpRequest.DONE) { return; } uploadXhrRef.current = null; setIsUploading(false); if (cancelRequestedRef.current) { return; } let payload = null; try { payload = JSON.parse(xhr.responseText || '{}'); } catch (error) { setAlertMessage('error', __('Upload failed.', 'email-validator')); return; } if (xhr.status !== 200 || !payload || !payload.success) { const message = payload && payload.data && payload.data.message ? payload.data.message : __('Upload failed.', 'email-validator'); setAlertMessage('error', message); return; } const data = payload.data || {}; const receivedToken = data.token || ''; const receivedTotal = Number(data.total) || 0; setToken(receivedToken); setTotalRows(receivedTotal); setUploadPercent(100); setStats({ processed: 0, inserted: 0, inqueue: receivedTotal > 0 ? receivedTotal : 0, total: receivedTotal > 0 ? receivedTotal : 0, failed: 0, duplicates: 0 }); if (data.warn) { setAlertMessage('warning', data.warn); } if (!receivedToken) { setAlertMessage('error', __('Upload did not return a valid token.', 'email-validator')); return; } setIsImporting(true); pollImport(receivedToken); }; const form = new FormData(); form.append('action', 'eva_upload_csv'); form.append('nonce', EVA_DATA.nonce || ''); form.append('file', file); form.append('hasHeader', hasHeader ? '1' : '0'); const turboEnabled = turboSupported && turbo; form.append('turbo', turboEnabled ? '1' : '0'); xhr.open('POST', EVA_DATA.ajaxUrl || ''); xhr.send(form); }, [file, isUploading, isImporting, hasHeader, turbo, turboSupported, clearAlert, setAlertMessage, pollImport, setStats]); const handleCancel = useCallback(() => { if (!isUploading && !isImporting) { return; } cancelRequestedRef.current = true; resetAsyncState(); clearProgress(); setAlertMessage('warning', __('Import cancelled.', 'email-validator')); }, [isUploading, isImporting, resetAsyncState, clearProgress, setAlertMessage]); const chooseFile = useCallback(() => { if (fileInputRef.current) { fileInputRef.current.click(); } }, []); const handleDeleteFile = useCallback((index) => { if (!window.confirm(__('Are you sure you want to delete this file?', 'email-validator'))) return; setDeletingIndex(index); const params = new URLSearchParams(); params.append('action', 'eva_delete_uploaded_file'); params.append('nonce', EVA_DATA.nonce || ''); params.append('index', index); fetch(EVA_DATA.ajaxUrl || '', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', }, body: params.toString(), }) .then(res => res.json()) .then(data => { setDeletingIndex(null); if (data.success) { setUploadedFiles(prevFiles => prevFiles.filter((_, i) => i !== index)); } }) .catch(() => { setDeletingIndex(null); }); }, []); const disableStart = !file || isUploading || isImporting; const disableCancel = !isUploading && !isImporting; const showImportStatus = isImporting || importPercent > 0 || stats.processed > 0 || stats.inqueue > 0 || stats.total > 0 || stats.failed > 0 || stats.duplicates > 0; return ( <> <div className="eva-card eva-card--wide"> <h2 className="eva-title">{__('AJAX Import (with Progress)', 'email-validator')}</h2> <p className="eva-subtitle"> {__('Upload a CSV and import it in the background while tracking progress.', 'email-validator')} </p> {alert.text && ( <div className={`notice ${ alert.type === 'error' ? 'notice-error' : alert.type === 'warning' ? 'notice-warning' : 'notice-success' }`} > <p>{alert.text}</p> </div> )} <div className="eva-uploader"> <div className={`eva-dropzone${isDragging ? ' is-drag' : ''}`} tabIndex={0} role="button" onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); chooseFile(); } }} onDragOver={(event) => { event.preventDefault(); event.stopPropagation(); setIsDragging(true); }} onDragEnter={(event) => { event.preventDefault(); event.stopPropagation(); setIsDragging(true); }} onDragLeave={(event) => { event.preventDefault(); event.stopPropagation(); setIsDragging(false); }} onDrop={handleDrop} onClick={chooseFile} > <div className="eva-dropzone__icon">⬆</div> <div className="eva-dropzone__text"> {__('Drag & drop CSV here or', 'email-validator')}{' '} <button type="button" className="button button-secondary eva-file-button" onClick={(event) => { event.stopPropagation(); chooseFile(); }} > {__('Choose file', 'email-validator')} </button> </div> <input ref={fileInputRef} id="eva-csv-file" type="file" accept=".csv" className="eva-file-input" onChange={(event) => { const candidate = event.target.files && event.target.files[0]; acceptFile(candidate, true); }} /> <div className="eva-dropzone__hint"> {__('CSV only. Max size depends on server limits.', 'email-validator')} </div> </div> {file && ( <div className="eva-file-meta">{fileLabel}</div> )} <div className="eva-options"> <label htmlFor="eva-has-header"> <input id="eva-has-header" type="checkbox" checked={hasHeader} onChange={(event) => setHasHeader(event.target.checked)} />{' '} {__('File has header row', 'email-validator')} </label> <label htmlFor="eva-turbo-mode"> <input id="eva-turbo-mode" type="checkbox" checked={turbo} disabled={!turboSupported} onChange={(event) => setTurbo(turboSupported ? event.target.checked : false)} />{' '} {__('Turbo mode (LOAD DATA INFILE)', 'email-validator')} </label> {!turboSupported && ( <p className="description"> {__('Turbo mode is disabled because this server blocks LOAD DATA LOCAL INFILE.', 'email-validator')} </p> )} </div> <div className="eva-actions"> <button type="button" className="button button-primary" onClick={handleStart} disabled={disableStart} > {isUploading ? __('Uploading…', 'email-validator') : isImporting ? __('Importing…', 'email-validator') : __('Start Import', 'email-validator')} </button> <button type="button" className="button" onClick={handleCancel} disabled={disableCancel} > {__('Cancel', 'email-validator')} </button> </div> </div> {(isUploading || uploadPercent > 0) && ( <div className="eva-progress" style={{ width: '100%', marginBottom: 16 }}> <div className="eva-progress__label">{__('Uploading…', 'email-validator')}</div> <LinearProgress variant="determinate" value={uploadPercent} sx={{ height: 8, borderRadius: 4, width: '100%' }} /> </div> )} {showImportStatus && ( <div className="eva-progress" style={{ width: '100%', marginBottom: 16 }}> <div className="eva-progress__label">{__('Importing…', 'email-validator')}</div> <LinearProgress variant="determinate" value={importPercent} sx={{ height: 8, borderRadius: 4, width: '100%' }} /> <div className="eva-progress__stats"> <div className="eva-progress__stat"> <span className="eva-progress__stat-label">{__('Total CSV Rows', 'email-validator')}</span> <span className="eva-progress__stat-value">{formatNumber(stats.csv_total)}</span> </div> <div className="eva-progress__stat"> <span className="eva-progress__stat-label">{__('Unique Emails Imported', 'email-validator')}</span> <span className="eva-progress__stat-value">{formatNumber(stats.inserted)}</span> </div> <div className="eva-progress__stat"> <span className="eva-progress__stat-label">{__('Duplicate Emails', 'email-validator')}</span> <span className="eva-progress__stat-value">{formatNumber(stats.duplicates)}</span> </div> <div className="eva-progress__stat"> <span className="eva-progress__stat-label">{__('Updated Emails', 'email-validator')}</span> <span className="eva-progress__stat-value">{formatNumber(stats.updated)}</span> </div> <div className="eva-progress__stat"> <span className="eva-progress__stat-label">{__('Failed Records', 'email-validator')}</span> <span className="eva-progress__stat-value">{formatNumber(stats.failed)}</span> </div> </div> </div> )} </div> {/* Uploaded Files Meta Box - separate card */} <div className="eva-card eva-card--files" style={{ marginTop: 32 }}> <h3 style={{ fontWeight: 600, marginBottom: 12 }}>{__('Uploaded Files', 'email-validator')}</h3> {filesLoading ? ( <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: 80 }}> <CircularProgress size={32} color="primary" /> </div> ) : uploadedFiles.length === 0 ? ( <div style={{ color: '#888', fontStyle: 'italic' }}>{__('No files uploaded yet.', 'email-validator')}</div> ) : ( <TableContainer> <Table size="small"> <TableHead> <TableRow> <TableCell>{__('File Name', 'email-validator')}</TableCell> <TableCell>{__('Size', 'email-validator')}</TableCell> <TableCell>{__('Uploaded At', 'email-validator')}</TableCell> <TableCell>{__('Actions', 'email-validator')}</TableCell> </TableRow> </TableHead> <TableBody> {uploadedFiles.map((file, idx) => ( <TableRow key={idx}> <TableCell> {file.url ? ( <a href={file.url} target="_blank" rel="noopener noreferrer">{file.name}</a> ) : ( file.name )} </TableCell> <TableCell>{formatBytes(file.size)}</TableCell> <TableCell>{new Date(file.time * 1000).toLocaleString()}</TableCell> <TableCell> <Button variant="outlined" color="error" size="small" onClick={() => handleDeleteFile(idx)} disabled={deletingIndex === idx} startIcon={deletingIndex === idx ? <CircularProgress size={16} color="inherit" /> : null} > {__('Delete', 'email-validator')} </Button> </TableCell> </TableRow> ))} </TableBody> </Table> </TableContainer> )} </div> </> ); }; const EmailListApp = () => { const [emails, setEmails] = useState([]); const [total, setTotal] = useState(0); const [paged, setPaged] = useState(1); const [perPage, setPerPage] = useState(20); const [loading, setLoading] = useState(false); const [progress, setProgress] = useState(0); const [search, setSearch] = useState(''); const [synStatus, setSynStatus] = useState(''); const [status, setStatus] = useState(''); const [brand, setBrand] = useState(''); const [brands, setBrands] = useState([]); const [synStatusOptions, setSynStatusOptions] = useState([]); const [statusOptions, setStatusOptions] = useState([]); const [dateOptions, setDateOptions] = useState([]); const [exporting, setExporting] = useState(false); const [exportScope, setExportScope] = useState('page'); // Status icon mapping const statusIcon = (status) => { if (status === 'valid' || status === 'active') { // Green check for valid/active return <span title={__('Active', 'email-validator')} style={{color: 'green', display: 'flex', alignItems: 'center'}}> <svg width="18" height="18" viewBox="0 0 20 20" style={{marginRight: 4}}><circle cx="10" cy="10" r="9" fill="#e0f7e9"/><path d="M6 10l2.5 2.5L14 7" stroke="#2e7d32" strokeWidth="2" fill="none"/></svg> {status === 'active' ? __('Active', 'email-validator') : __('Valid', 'email-validator')} </span>; } if (status === 'invalid') { // Red X for invalid return <span title={__('Invalid', 'email-validator')} style={{color: 'red', display: 'flex', alignItems: 'center'}}> <svg width="18" height="18" viewBox="0 0 20 20" style={{marginRight: 4}}><circle cx="10" cy="10" r="9" fill="#fdecea"/><path d="M7 7l6 6M13 7l-6 6" stroke="#c62828" strokeWidth="2" fill="none"/></svg> {__('Invalid', 'email-validator')} </span>; } if (status === 'pending') { // Yellow clock for pending return <span title={__('Pending', 'email-validator')} style={{color: '#fbc02d', display: 'flex', alignItems: 'center'}}> <svg width="18" height="18" viewBox="0 0 20 20" style={{marginRight: 4}}><circle cx="10" cy="10" r="9" fill="#fff8e1"/><path d="M10 5v5l3 3" stroke="#fbc02d" strokeWidth="2" fill="none"/></svg> {__('Pending', 'email-validator')} </span>; } // Default: show status text only return <span style={{display: 'flex', alignItems: 'center'}}>{status}</span>; }; // Export CSV handler const handleExportCSV = useCallback(() => { setExporting(true); const params = new URLSearchParams({ paged: paged, per_page: perPage, search: search, syn_status: synStatus, status, brand, scope: exportScope, }); fetch(REST_BASE + `/wp-json/email-validator/v1/emails/export?${params.toString()}`, { credentials: 'same-origin', }) .then(res => res.blob()) .then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'email-list.csv'; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(url); setExporting(false); }) .catch(() => setExporting(false)); }, [paged, perPage, search, synStatus, status, brand, exportScope]); // Fetch filter options (brands, statuses, etc.) once useEffect(() => { fetch(REST_BASE + '/wp-json/email-validator/v1/emails/meta', { credentials: 'same-origin' }) .then((res) => res.json()) .then((meta) => { setBrands(meta.brands || []); setSynStatusOptions(meta.syn_statuses || []); setStatusOptions(meta.statuses || []); }); // Auto-load email data on mount fetchEmails(1, ''); }, []); const fetchEmails = useCallback((page = 1, searchValue = search) => { setLoading(true); setProgress(0); let progressInterval = setInterval(() => { setProgress((prev) => (prev < 90 ? prev + 10 : prev)); }, 100); const params = new URLSearchParams({ paged: page, per_page: perPage, search: searchValue, syn_status: synStatus, status, brand, }); fetch(REST_BASE + `/wp-json/email-validator/v1/emails?${params.toString()}`, { credentials: 'same-origin', }) .then((res) => res.json()) .then((data) => { setEmails(data.emails || []); setTotal(data.total || 0); setPaged(data.paged || 1); setLoading(false); setProgress(100); clearInterval(progressInterval); }) .catch(() => { setLoading(false); setProgress(0); clearInterval(progressInterval); }); }, [perPage, synStatus, status, brand, search]); // Removed auto-fetch on mount. Only fetch emails when filter/search is triggered. const totalPages = Math.ceil(total / perPage); // Helper to build pagination range const getPagination = () => { const pages = []; if (totalPages <= 10) { for (let i = 1; i <= totalPages; i++) pages.push(i); } else { pages.push(1); if (paged > 4) pages.push('...'); for (let i = Math.max(2, paged - 2); i <= Math.min(totalPages - 1, paged + 2); i++) { pages.push(i); } if (paged < totalPages - 3) pages.push('...'); pages.push(totalPages); } return pages; }; // Helper to trigger filter and reset to page 1 const handleFilter = () => fetchEmails(1, search); return ( <Paper sx={{ p: 2, mb: 2, borderRadius: 3, boxShadow: 3 }}> <h2 style={{ fontWeight: 600, marginBottom: 12 }}>{__('Email List', 'email-validator')}</h2> <Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 2 }}> <span style={{ fontWeight: 500 }}>{__('Total Records:', 'email-validator')} {formatNumber(total)}</span> <TextField label={__('Search email', 'email-validator')} variant="outlined" size="small" value={search} onChange={e => setSearch(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleFilter()} sx={{ minWidth: 180 }} /> <FormControl size="small" sx={{ minWidth: 120 }}> <InputLabel>{__('Syn Status', 'email-validator')}</InputLabel> <Select value={synStatus} label={__('Syn Status', 'email-validator')} onChange={e => setSynStatus(e.target.value)} > <MenuItem value=""><em>{__('All', 'email-validator')}</em></MenuItem> {synStatusOptions.map(opt => <MenuItem key={opt} value={opt}>{opt}</MenuItem>)} </Select> </FormControl> <FormControl size="small" sx={{ minWidth: 120 }}> <InputLabel>{__('Status', 'email-validator')}</InputLabel> <Select value={status} label={__('Status', 'email-validator')} onChange={e => setStatus(e.target.value)} > <MenuItem value=""><em>{__('All', 'email-validator')}</em></MenuItem> {statusOptions.map(opt => <MenuItem key={opt} value={opt}>{opt}</MenuItem>)} </Select> </FormControl> <FormControl size="small" sx={{ minWidth: 120 }}> <InputLabel>{__('Brand', 'email-validator')}</InputLabel> <Select value={brand} label={__('Brand', 'email-validator')} onChange={e => setBrand(e.target.value)} > <MenuItem value=""><em>{__('All', 'email-validator')}</em></MenuItem> {brands.map(opt => <MenuItem key={opt} value={opt}>{opt}</MenuItem>)} </Select> </FormControl> <Button variant="outlined" color="primary" onClick={handleFilter}>{__('Filter', 'email-validator')}</Button> <FormControl size="small" sx={{ minWidth: 200 }}> <InputLabel>{__('Export scope', 'email-validator')}</InputLabel> <Select value={exportScope} label={__('Export scope', 'email-validator')} onChange={e => setExportScope(e.target.value)} > <MenuItem value="page">{__('Current page only', 'email-validator')}</MenuItem> <MenuItem value="all">{__('All filtered results', 'email-validator')}</MenuItem> </Select> </FormControl> <Button variant="contained" color="success" onClick={handleExportCSV} disabled={exporting} sx={{ ml: 2 }}> {exporting ? __('Exporting…', 'email-validator') : __('Export CSV', 'email-validator')} </Button> </Stack> <TableContainer sx={{ mb: 2, borderRadius: 2, boxShadow: 1 }}> <Table size="small" sx={{ minWidth: 650 }}> <TableHead> <TableRow sx={{ backgroundColor: '#fafafa' }}> <TableCell sx={{ fontWeight: 500 }}>{__('ID', 'email-validator')}</TableCell> <TableCell sx={{ fontWeight: 500 }}>{__('Email', 'email-validator')}</TableCell> <TableCell sx={{ fontWeight: 500 }}>{__('First Name', 'email-validator')}</TableCell> <TableCell sx={{ fontWeight: 500 }}>{__('Last Name', 'email-validator')}</TableCell> <TableCell sx={{ fontWeight: 500 }}>{__('Status', 'email-validator')}</TableCell> <TableCell sx={{ fontWeight: 500 }}>{__('Syn Date', 'email-validator')}</TableCell> <TableCell sx={{ fontWeight: 500 }}>{__('Syn Status', 'email-validator')}</TableCell> <TableCell sx={{ fontWeight: 500 }}>{__('Log', 'email-validator')}</TableCell> <TableCell sx={{ fontWeight: 500 }}>{__('Brand', 'email-validator')}</TableCell> </TableRow> </TableHead> <TableBody> {loading ? ( <TableRow> <TableCell colSpan={9} align="center"> <span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: 60 }}> <CircularProgress size={32} color="primary" /> <span style={{ marginLeft: 12 }}>{__('Loading emails...', 'email-validator')}</span> </span> </TableCell> </TableRow> ) : emails.length > 0 ? emails.map((row) => ( <TableRow key={row.id}> <TableCell>{row.id}</TableCell> <TableCell>{row.email}</TableCell> <TableCell>{row.first_name}</TableCell> <TableCell>{row.last_name}</TableCell> <TableCell> <Button variant="outlined" size="small" sx={{textTransform: 'none', minWidth: 0, px: 1, py: 0.5, borderRadius: 2, fontWeight: 500, display: 'flex', alignItems: 'center'}}> {statusIcon(row.status)} </Button> </TableCell> <TableCell>{row.syn_date}</TableCell> <TableCell>{row.syn_status}</TableCell> <TableCell>{row.log}</TableCell> <TableCell>{row.brand}</TableCell> </TableRow> )) : ( <TableRow><TableCell colSpan={9}>{__('No emails found.', 'email-validator')}</TableCell></TableRow> )} </TableBody> </Table> </TableContainer> {totalPages > 1 && ( <Stack direction="row" justifyContent="center" alignItems="center" spacing={2}> <Pagination count={totalPages} page={paged} onChange={(e, page) => fetchEmails(page)} variant="contained" color="secondary" siblingCount={1} boundaryCount={1} /> </Stack> )} </Paper> ); }; const mountNode = document.getElementById('eva-admin-app'); if (mountNode) { render(<ImportApp />, mountNode); } const emailListNode = document.getElementById('eva-email-list-app'); if (emailListNode) { render(<EmailListApp />, emailListNode); }
[+]
..
[-] index.jsx
[open]
[-] run-validation-entry.js
[open]
[-] run-validation.jsx
[open]