354 lines
9.9 KiB
TypeScript
354 lines
9.9 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import {
|
|
ThemeProvider,
|
|
createTheme,
|
|
CssBaseline,
|
|
Box,
|
|
AppBar,
|
|
Toolbar,
|
|
Typography,
|
|
Paper,
|
|
Button,
|
|
Alert,
|
|
Chip,
|
|
Stack,
|
|
Divider,
|
|
} from '@mui/material';
|
|
import {
|
|
Upload as UploadIcon,
|
|
Delete as DeleteIcon,
|
|
Search as SearchIcon,
|
|
Description as DescriptionIcon,
|
|
} from '@mui/icons-material';
|
|
import type { ResumeData } from './types';
|
|
|
|
// Create a custom theme for the extension
|
|
const theme = createTheme({
|
|
palette: {
|
|
primary: {
|
|
main: '#667eea',
|
|
},
|
|
secondary: {
|
|
main: '#764ba2',
|
|
},
|
|
background: {
|
|
default: '#f5f5f5',
|
|
},
|
|
},
|
|
typography: {
|
|
fontFamily:
|
|
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif',
|
|
},
|
|
});
|
|
|
|
const PopupApp: React.FC = () => {
|
|
const [resumeData, setResumeData] = useState<ResumeData | null>(null);
|
|
|
|
useEffect(() => {
|
|
void loadResumeStatus();
|
|
}, []);
|
|
|
|
const loadResumeStatus = async () => {
|
|
try {
|
|
const response = (await browser.runtime.sendMessage({
|
|
type: 'GET_RESUME_DATA',
|
|
})) as { success: boolean; data?: ResumeData | null };
|
|
|
|
if (response.success && response.data) {
|
|
setResumeData(response.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading resume:', error);
|
|
}
|
|
};
|
|
|
|
const handleUploadResume = () => {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = '.pdf,.txt,.doc,.docx';
|
|
input.onchange = async (e) => {
|
|
const file = (e.target as HTMLInputElement).files?.[0];
|
|
if (!file) return;
|
|
|
|
try {
|
|
const text = await file.text();
|
|
const parsedData = parseResume(text);
|
|
|
|
await browser.runtime.sendMessage({
|
|
type: 'SAVE_RESUME_DATA',
|
|
data: parsedData,
|
|
});
|
|
|
|
await loadResumeStatus();
|
|
alert('Resume uploaded successfully!');
|
|
} catch (error) {
|
|
console.error('Error uploading resume:', error);
|
|
alert('Error uploading resume. Please try again.');
|
|
}
|
|
};
|
|
input.click();
|
|
};
|
|
|
|
const handleClearResume = async () => {
|
|
if (!confirm('Are you sure you want to clear your resume data?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await browser.runtime.sendMessage({
|
|
type: 'SAVE_RESUME_DATA',
|
|
data: null,
|
|
});
|
|
|
|
await loadResumeStatus();
|
|
alert('Resume cleared successfully!');
|
|
} catch (error) {
|
|
console.error('Error clearing resume:', error);
|
|
alert('Error clearing resume. Please try again.');
|
|
}
|
|
};
|
|
|
|
const handleScanPage = async () => {
|
|
try {
|
|
const tabs = await browser.tabs.query({
|
|
active: true,
|
|
currentWindow: true,
|
|
});
|
|
const activeTab = tabs[0];
|
|
|
|
if (!activeTab?.id) {
|
|
alert('No active tab found');
|
|
return;
|
|
}
|
|
|
|
await browser.tabs.sendMessage(activeTab.id, {
|
|
type: 'SCAN_PAGE',
|
|
});
|
|
|
|
window.close();
|
|
} catch (error) {
|
|
console.error('Error scanning page:', error);
|
|
alert('Error scanning page. Please try again.');
|
|
}
|
|
};
|
|
|
|
const parseResume = (text: string): ResumeData => {
|
|
const lines = text.split('\n').filter((line) => line.trim());
|
|
|
|
return {
|
|
personalInfo: {
|
|
fullName:
|
|
lines
|
|
.find((line) => line.match(/^[A-Z][a-z]+ [A-Z][a-z]+/))
|
|
?.trim() || '',
|
|
email:
|
|
lines
|
|
.find((line) =>
|
|
line.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/)
|
|
)
|
|
?.trim() || '',
|
|
phone:
|
|
lines
|
|
.find((line) =>
|
|
line.match(/(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/)
|
|
)
|
|
?.trim() || '',
|
|
address:
|
|
lines
|
|
.find((line) => line.match(/\d+\s+\w+\s+(St|Ave|Rd|Blvd|Ln|Dr)/i))
|
|
?.trim() || '',
|
|
city: '',
|
|
state: '',
|
|
zipCode: '',
|
|
country: '',
|
|
},
|
|
workExperience: [],
|
|
education: [],
|
|
skills: [],
|
|
summary: '',
|
|
};
|
|
};
|
|
|
|
return (
|
|
<ThemeProvider theme={theme}>
|
|
<CssBaseline />
|
|
<Box sx={{ width: 400, bgcolor: 'background.default' }}>
|
|
{/* Header */}
|
|
<AppBar
|
|
position="static"
|
|
sx={{
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
}}
|
|
>
|
|
<Toolbar>
|
|
<Box sx={{ flexGrow: 1, textAlign: 'center' }}>
|
|
<Typography variant="h5" component="h1" sx={{ fontWeight: 600 }}>
|
|
JobSpeed
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ opacity: 0.9 }}>
|
|
Auto-fill job applications
|
|
</Typography>
|
|
</Box>
|
|
</Toolbar>
|
|
</AppBar>
|
|
|
|
{/* Main Content */}
|
|
<Box sx={{ p: 2.5 }}>
|
|
{/* Resume Section */}
|
|
<Paper elevation={0} sx={{ p: 2, mb: 3, bgcolor: 'white' }}>
|
|
<Stack spacing={2}>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
}}
|
|
>
|
|
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
|
Resume
|
|
</Typography>
|
|
{resumeData && (
|
|
<Chip
|
|
icon={<DescriptionIcon />}
|
|
label="Uploaded"
|
|
color="success"
|
|
size="small"
|
|
/>
|
|
)}
|
|
</Box>
|
|
|
|
{!resumeData ? (
|
|
<Alert severity="info" sx={{ mb: 1 }}>
|
|
No resume uploaded
|
|
</Alert>
|
|
) : (
|
|
<Alert severity="success" sx={{ mb: 1 }}>
|
|
Resume data loaded
|
|
</Alert>
|
|
)}
|
|
|
|
<Button
|
|
variant="contained"
|
|
startIcon={<UploadIcon />}
|
|
onClick={handleUploadResume}
|
|
fullWidth
|
|
sx={{
|
|
background:
|
|
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
textTransform: 'none',
|
|
py: 1.5,
|
|
}}
|
|
>
|
|
Upload Resume
|
|
</Button>
|
|
|
|
{resumeData && (
|
|
<>
|
|
<Divider />
|
|
<Box sx={{ bgcolor: '#f8f9fa', p: 2, borderRadius: 1 }}>
|
|
<Typography
|
|
variant="subtitle2"
|
|
sx={{ mb: 1, fontWeight: 600 }}
|
|
>
|
|
Resume Data
|
|
</Typography>
|
|
<Stack
|
|
spacing={0.5}
|
|
sx={{ fontSize: '0.875rem', color: '#666' }}
|
|
>
|
|
{resumeData.personalInfo.fullName && (
|
|
<Typography variant="body2">
|
|
<strong>Name:</strong>{' '}
|
|
{resumeData.personalInfo.fullName}
|
|
</Typography>
|
|
)}
|
|
{resumeData.personalInfo.email && (
|
|
<Typography variant="body2">
|
|
<strong>Email:</strong>{' '}
|
|
{resumeData.personalInfo.email}
|
|
</Typography>
|
|
)}
|
|
{resumeData.personalInfo.phone && (
|
|
<Typography variant="body2">
|
|
<strong>Phone:</strong>{' '}
|
|
{resumeData.personalInfo.phone}
|
|
</Typography>
|
|
)}
|
|
{resumeData.personalInfo.address && (
|
|
<Typography variant="body2">
|
|
<strong>Address:</strong>{' '}
|
|
{resumeData.personalInfo.address}
|
|
</Typography>
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<DeleteIcon />}
|
|
onClick={() => void handleClearResume()}
|
|
fullWidth
|
|
color="error"
|
|
sx={{ textTransform: 'none' }}
|
|
>
|
|
Clear Resume
|
|
</Button>
|
|
</>
|
|
)}
|
|
</Stack>
|
|
</Paper>
|
|
|
|
{/* Actions Section */}
|
|
<Paper elevation={0} sx={{ p: 2, bgcolor: 'white' }}>
|
|
<Stack spacing={2}>
|
|
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
|
Actions
|
|
</Typography>
|
|
<Button
|
|
variant="contained"
|
|
startIcon={<SearchIcon />}
|
|
onClick={() => void handleScanPage()}
|
|
fullWidth
|
|
sx={{
|
|
background:
|
|
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
textTransform: 'none',
|
|
py: 1.5,
|
|
}}
|
|
>
|
|
Scan Current Page
|
|
</Button>
|
|
<Typography variant="caption" color="text.secondary">
|
|
Click to detect form fields on the current page and preview
|
|
auto-fill suggestions
|
|
</Typography>
|
|
</Stack>
|
|
</Paper>
|
|
</Box>
|
|
|
|
{/* Footer */}
|
|
<Box
|
|
sx={{
|
|
p: 1.5,
|
|
textAlign: 'center',
|
|
borderTop: '1px solid',
|
|
borderColor: 'divider',
|
|
bgcolor: '#f8f9fa',
|
|
}}
|
|
>
|
|
<Typography variant="caption" color="text.secondary">
|
|
v1.0.0
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</ThemeProvider>
|
|
);
|
|
};
|
|
|
|
// Mount the React app
|
|
const container = document.getElementById('root');
|
|
if (container) {
|
|
const root = createRoot(container);
|
|
root.render(<PopupApp />);
|
|
}
|