This commit is contained in:
2025-10-19 22:57:23 -05:00
parent 4d04f86067
commit 9ee0dbff1d
17 changed files with 1887 additions and 113 deletions
@@ -0,0 +1,50 @@
name: Build Playwright Runner Image
on:
push:
branches: [main]
paths:
- 'scripts/Dockerfile'
- '.github/workflows/build-playwright-image.yml'
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/jobspeed-playwright-runner
jobs:
build-and-push:
name: Build and Push Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=sha,prefix={{branch}}-
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: scripts/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+8 -15
View File
@@ -56,29 +56,22 @@ jobs:
demo-e2e-tests:
name: Demo App E2E Tests
runs-on: ubuntu-latest
container:
image: ghcr.io/${{ github.repository_owner }}/jobspeed-playwright-runner:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: cd job-application-demo && npx playwright install --with-deps chromium firefox
working-directory: job-application-demo
- name: Run E2E tests
run: cd job-application-demo && pnpm test:e2e
run: pnpm test:e2e
working-directory: job-application-demo
- name: Upload test results
if: failure()
+2
View File
@@ -10,3 +10,5 @@ coverage
*~
web-ext-artifacts
.claude/
.pnpm*
.turbo
+55 -13
View File
@@ -17,6 +17,9 @@ A Firefox browser extension that helps you auto-fill job applications using your
- **Vitest** - Unit testing
- **ESLint** + **Prettier** - Code quality and formatting
- **Native Browser API** - Firefox WebExtensions API
- **PDF.js** - Client-side PDF parsing
- **Mammoth** - Client-side DOCX parsing
- **Turborepo** - Monorepo management
## Getting Started
@@ -33,7 +36,7 @@ A Firefox browser extension that helps you auto-fill job applications using your
pnpm install
```
2. Start development mode:
2. Start extension development mode:
```bash
pnpm dev
@@ -57,6 +60,7 @@ The production-ready extension will be in the `dist` folder.
### Available Scripts
**Extension Scripts:**
- `pnpm dev` - Build and watch for changes
- `pnpm build` - Production build
- `pnpm test` - Run tests
@@ -66,6 +70,17 @@ The production-ready extension will be in the `dist` folder.
- `pnpm format` - Format code with Prettier
- `pnpm format:check` - Check code formatting
**Turborepo Scripts (run tasks across all packages):**
- `pnpm turbo:build` - Build all packages in parallel with caching
- `pnpm turbo:test` - Run tests across all packages
- `pnpm turbo:lint` - Lint all packages
- `pnpm turbo:format` - Format all packages
**Advanced Turbo Commands:**
- `turbo run build --filter=job-application-demo` - Build only the demo app
- `turbo run test --force` - Skip cache and run all tests
- `turbo run build --dry` - See what would run without executing
### Project Structure
```
@@ -76,16 +91,33 @@ src/
├── popup.ts # Popup UI logic
├── popup.html # Popup HTML
├── popup.css # Popup styles
├── upload.tsx # Resume upload UI (React)
├── upload.html # Upload page HTML
├── types/
│ ├── index.ts # Type definitions
│ └── browser.d.ts # Browser API types
└── services/
├── formDetector.ts # Form field detection logic
└── fieldMatcher.ts # Resume-to-field matching logic
├── services/
│ ├── formDetector.ts # Form field detection logic
│ └── fieldMatcher.ts # Resume-to-field matching logic
└── utils/
└── resumeParser.ts # Client-side resume parsing (PDF, DOCX)
job-application-demo/ # Sample job application form (separate app)
```
### Client-Side Resume Parsing
The extension includes a built-in client-side resume parser that handles file parsing directly in the browser:
- Parses PDF files using PDF.js (Mozilla's PDF library)
- Parses DOCX files using Mammoth
- Extracts structured resume data (personal info, work experience, education, skills)
- All parsing happens locally in your browser
- No external servers or network requests required
- Works completely offline
The parser is located in `src/utils/resumeParser.ts` and is automatically used when you upload a resume through the extension.
### Demo Application
The `job-application-demo` folder contains a standalone sample job application form built with Vite, React, Material UI, and TypeScript. This demo app:
@@ -111,7 +143,8 @@ See [job-application-demo/README.md](job-application-demo/README.md) for more de
1. **Upload Your Resume**:
- Click the JobSpeed extension icon
- Click "Upload Resume"
- Select your resume file (PDF, TXT, DOC, DOCX)
- Select your resume file (PDF or DOCX)
- The extension will parse the file directly in your browser
2. **Auto-fill a Job Application**:
- Navigate to a job application page
@@ -124,12 +157,18 @@ See [job-application-demo/README.md](job-application-demo/README.md) for more de
### Resume Parsing
The extension extracts key information from your resume:
- Personal information (name, email, phone, address)
- Work experience
- Education
- Skills
- Professional summary
The extension parses your resume files directly in the browser and extracts key information:
- **Personal information** (name, email, phone, address)
- **Work experience**
- **Education**
- **Skills**
- **Professional summary**
Supported formats:
- **PDF** - Parsed using PDF.js (Mozilla's library)
- **DOCX** - Parsed using Mammoth
The parsed data is stored locally in your browser storage.
### Smart Field Matching
@@ -141,8 +180,11 @@ The field matcher uses intelligent algorithms to match resume data with form fie
### Privacy & Security
- All data is stored locally in your browser
- No data is sent to external servers
- All resume parsing happens **client-side** in your browser
- All parsed resume data is stored locally in your browser storage
- No data is sent to external servers or third parties
- Resume files are processed entirely in memory within the extension
- Works completely offline
- You have full control over your resume data
## Contributing
+12 -3
View File
@@ -5,14 +5,21 @@
"type": "module",
"scripts": {
"dev": "vite build --watch --mode development",
"build": "tsc && vite build && pnpm copy:icons",
"build": "tsc && vite build && pnpm copy:icons && pnpm build:upload",
"build:upload": "node scripts/build-upload.mjs",
"copy:icons": "mkdir -p dist/icons && cp src/icons/*.svg dist/icons/",
"test": "vitest",
"test:ui": "vitest --ui",
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext .ts,.tsx",
"lint:fix": "ESLINT_USE_FLAT_CONFIG=false eslint . --ext .ts,.tsx --fix",
"format": "prettier --write \"{src,job-application-demo/src}/**/*.{ts,tsx,json,css}\"",
"format:check": "prettier --check \"{src,job-application-demo/src}/**/*.{ts,tsx,json,css}\""
"format:check": "prettier --check \"{src,job-application-demo/src}/**/*.{ts,tsx,json,css}\"",
"turbo:build": "turbo run build",
"turbo:dev": "turbo run dev",
"turbo:test": "turbo run test",
"turbo:lint": "turbo run lint",
"turbo:format": "turbo run format",
"turbo:format:check": "turbo run format:check"
},
"keywords": [
"firefox",
@@ -37,6 +44,7 @@
"eslint-plugin-prettier": "^5.1.3",
"jsdom": "^27.0.1",
"prettier": "^3.3.2",
"turbo": "^2.5.8",
"typescript": "^5.5.3",
"vite": "^7.1.10",
"vite-plugin-web-extension": "^4.1.6",
@@ -47,8 +55,9 @@
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.4",
"@mui/material": "^7.3.4",
"mammoth": "^1.11.0",
"pdfjs-dist": "^5.4.296",
"react": "^19.2.0",
"react-dom": "^19.2.0"
}
}
+1085 -21
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
# Node modules
node_modules/
**/node_modules/
# Build outputs
dist/
build/
out/
*.vsix
# Dependencies
.pnpm-store/
# Test outputs
coverage/
playwright-report/
test-results/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Git
.git/
.gitignore
# Misc
*.log
.env
.env.*
+35
View File
@@ -0,0 +1,35 @@
# Ubuntu 24.04 LTS (Noble Numbat) - pinned version
FROM ubuntu:24.04
# Set environment variables
ENV NODE_VERSION=22
ENV PNPM_VERSION=10.18.1
ENV DEBIAN_FRONTEND=noninteractive
# Install system dependencies
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
ca-certificates \
gnupg \
&& rm -rf /var/lib/apt/lists/*
# Install Node.js 22
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Enable corepack and setup pnpm
RUN corepack enable \
&& corepack prepare pnpm@${PNPM_VERSION} --activate
# Install Playwright dependencies
# This installs all system dependencies needed for Chromium and Firefox
RUN npx playwright@1.56.1 install --with-deps chromium firefox
# Set working directory
WORKDIR /workspace
# Default command
CMD ["/bin/bash"]
+43
View File
@@ -0,0 +1,43 @@
# Playwright Runner Docker Image
This directory contains a Dockerfile for running Playwright e2e tests in CI/CD.
## Overview
The Docker image is based on **Ubuntu 24.04 LTS (Noble Numbat)** and includes:
- Node.js 22
- pnpm 10.18.1 (via corepack)
- Playwright 1.56.1 with Chromium and Firefox browsers and all system dependencies
## Local Testing
Build the image locally:
```bash
docker build -t jobspeed-playwright-runner:latest -f scripts/Dockerfile .
```
Run e2e tests in the container:
```bash
docker run --rm -e CI=true \
-v $(pwd):/workspace \
-w /workspace/job-application-demo \
jobspeed-playwright-runner:latest \
bash -c "pnpm install --frozen-lockfile && pnpm test:e2e"
```
## CI/CD Usage
The image is automatically built and pushed to GitHub Container Registry (ghcr.io) when:
- Changes are pushed to the `main` branch that affect `scripts/Dockerfile`
- The workflow is manually triggered
The `status-checks.yml` workflow uses this image for running e2e tests, eliminating the need for:
- Manual pnpm setup
- Node.js setup
- Playwright browser installation
## Image Location
The image is published to: `ghcr.io/<repository-owner>/jobspeed-playwright-runner:latest`
+35
View File
@@ -0,0 +1,35 @@
import { build } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
async function buildUploadPage() {
try {
await build({
root: path.resolve(__dirname, '../src'),
build: {
outDir: path.resolve(__dirname, '../dist'),
emptyOutDir: false, // Don't empty the dir - other files are already there
rollupOptions: {
input: {
upload: path.resolve(__dirname, '../src/upload.html'),
},
output: {
entryFileNames: '[name].js',
chunkFileNames: '[name]-[hash].js',
assetFileNames: '[name].[ext]',
},
},
},
plugins: [react()],
});
console.log('✓ Upload page built successfully');
} catch (error) {
console.error('✗ Error building upload page:', error);
process.exit(1);
}
}
buildUploadPage();
+6
View File
@@ -26,5 +26,11 @@
"js": ["content.ts"],
"run_at": "document_idle"
}
],
"web_accessible_resources": [
{
"resources": ["upload.html"],
"matches": ["<all_urls>"]
}
]
}
+64
View File
@@ -0,0 +1,64 @@
/**
* Splits raw resume text into distinct logical sections based on common headers.
* The part before the first recognized header is stored as 'header'.
*/
export function sectionizeResume(rawText: string): Record<string, string[]> {
const lines = rawText.split('\n').map(line => line.trim()).filter(line => line.length > 0);
// Use a map of normalized keywords to the internal section key
const keywordMap: Record<string, string> = {
'summary': 'summary',
'objective': 'summary',
'professional summary': 'summary',
'profile': 'summary',
'experience': 'workExperience',
'professional experience': 'workExperience',
'work history': 'workExperience',
'employment': 'workExperience',
'relevant experience': 'workExperience',
'education': 'education',
'academic background': 'education',
'skills': 'skills',
'technical skills': 'skills',
'proficiencies': 'skills',
'expertise': 'skills',
};
const sections: Record<string, string[]> = {
header: [],
summary: [],
workExperience: [],
education: [],
skills: [],
};
let currentSection = 'header';
for (const line of lines) {
const normalizedLine = line.toLowerCase();
let foundSection = false;
for (const [keyword, sectionKey] of Object.entries(keywordMap)) {
// Check if the line is a major section header
if (normalizedLine.startsWith(keyword) && line.length < 50) {
currentSection = sectionKey;
foundSection = true;
break;
}
}
// I'm not sure if this is the best approach. Re-evalutate in morning/soon
if (!foundSection) {
const section = sections[currentSection];
if (section) {
section.push(line);
} else {
sections[currentSection] = [line];
}
}
}
return sections;
}
+5 -61
View File
@@ -64,30 +64,11 @@ const PopupApp: React.FC = () => {
};
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();
// Open the upload page in a new tab to avoid popup closing in Firefox
void browser.tabs.create({
url: browser.runtime.getURL('upload.html'),
});
window.close();
};
const handleClearResume = async () => {
@@ -133,43 +114,6 @@ const PopupApp: React.FC = () => {
}
};
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 />
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JobSpeed - Upload Resume</title>
</head>
<body>
<div id="root"></div>
<script src="upload.tsx" type="module"></script>
</body>
</html>
+269
View File
@@ -0,0 +1,269 @@
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
import {
ThemeProvider,
createTheme,
CssBaseline,
Box,
AppBar,
Toolbar,
Typography,
Paper,
Button,
Alert,
Stack,
LinearProgress,
} from '@mui/material';
import {
Upload as UploadIcon,
CheckCircle as CheckCircleIcon,
Close as CloseIcon,
} from '@mui/icons-material';
import type { ResumeData } from './types';
import { parseResumeFile } from './utils/resumeParser';
// 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 UploadApp: React.FC = () => {
const [uploading, setUploading] = useState(false);
const [uploadSuccess, setUploadSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const [resumeData, setResumeData] = useState<ResumeData | null>(null);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
setError(null);
try {
// Parse the resume file client-side
const parsedData = await parseResumeFile(file);
await browser.runtime.sendMessage({
type: 'SAVE_RESUME_DATA',
data: parsedData,
});
setResumeData(parsedData);
setUploadSuccess(true);
} catch (err) {
console.error('Error uploading resume:', err);
const errorMessage =
err instanceof Error ? err.message : 'Error uploading resume. Please try again.';
setError(errorMessage);
} finally {
setUploading(false);
}
};
const handleClose = () => {
window.close();
};
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Box
sx={{
minHeight: '100vh',
bgcolor: 'background.default',
display: 'flex',
flexDirection: 'column',
}}
>
{/* Header */}
<AppBar
position="static"
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
}}
>
<Toolbar>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h5" component="h1" sx={{ fontWeight: 600 }}>
JobSpeed - Upload Resume
</Typography>
<Typography variant="caption" sx={{ opacity: 0.9 }}>
Select your resume file to upload
</Typography>
</Box>
<Button
color="inherit"
startIcon={<CloseIcon />}
onClick={handleClose}
sx={{ textTransform: 'none' }}
>
Close
</Button>
</Toolbar>
</AppBar>
{/* Main Content */}
<Box
sx={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
p: 3,
}}
>
<Paper
elevation={3}
sx={{
p: 4,
maxWidth: 600,
width: '100%',
}}
>
<Stack spacing={3}>
{uploading && (
<>
<Typography variant="h6" align="center">
Uploading Resume...
</Typography>
<LinearProgress />
</>
)}
{!uploading && !uploadSuccess && (
<>
<Typography variant="h6" align="center">
Select Resume File
</Typography>
<Typography
variant="body2"
color="text.secondary"
align="center"
>
Supported formats: PDF, DOCX
</Typography>
<Button
variant="contained"
component="label"
startIcon={<UploadIcon />}
fullWidth
size="large"
sx={{
background:
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
textTransform: 'none',
py: 2,
}}
>
Choose File
<input
type="file"
hidden
accept=".pdf,.docx"
onChange={(e) => void handleFileChange(e)}
/>
</Button>
</>
)}
{uploadSuccess && resumeData && (
<>
<Box sx={{ textAlign: 'center' }}>
<CheckCircleIcon
sx={{ fontSize: 64, color: 'success.main', mb: 2 }}
/>
<Typography variant="h6" gutterBottom>
Resume Uploaded Successfully!
</Typography>
</Box>
<Alert severity="success">
Your resume data has been saved and is ready to use for
auto-filling job applications.
</Alert>
<Box sx={{ bgcolor: '#f8f9fa', p: 2, borderRadius: 1 }}>
<Typography
variant="subtitle2"
sx={{ mb: 1, fontWeight: 600 }}
>
Extracted 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="contained"
onClick={handleClose}
fullWidth
sx={{
background:
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
textTransform: 'none',
py: 1.5,
}}
>
Done
</Button>
</>
)}
{error && <Alert severity="error">{error}</Alert>}
</Stack>
</Paper>
</Box>
</Box>
</ThemeProvider>
);
};
// Mount the React app
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render(<UploadApp />);
}
+98
View File
@@ -0,0 +1,98 @@
import * as pdfjsLib from 'pdfjs-dist';
import mammoth from 'mammoth';
import type { ResumeData } from '../types';
// Configure PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.mjs',
import.meta.url
).toString();
/**
* Extract text from a PDF file
*/
async function extractTextFromPDF(file: File): Promise<string> {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let fullText = '';
// Extract text from each page
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items
.map((item: any) => item.str)
.join(' ');
fullText += pageText + '\n';
}
return fullText;
}
/**
* Extract text from a DOCX file
*/
async function extractTextFromDOCX(file: File): Promise<string> {
const arrayBuffer = await file.arrayBuffer();
const result = await mammoth.extractRawText({ arrayBuffer });
return result.value;
}
/**
* Parse resume text to extract structured data
*/
function parseResumeText(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,}/)
)
?.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/)?.[0] || '',
phone:
lines
.find((line) =>
line.match(/(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/)
)
?.match(/(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/)?.[0] || '',
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: '',
};
}
/**
* Main function to parse a resume file (PDF or DOCX)
*/
export async function parseResumeFile(file: File): Promise<ResumeData> {
const fileName = file.name.toLowerCase();
let text = '';
if (fileName.endsWith('.pdf')) {
text = await extractTextFromPDF(file);
} else if (fileName.endsWith('.docx')) {
text = await extractTextFromDOCX(file);
} else {
throw new Error('Unsupported file type. Only PDF and DOCX files are supported.');
}
return parseResumeText(text);
}
+72
View File
@@ -0,0 +1,72 @@
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "build/**"],
"inputs": ["src/**", "tsconfig.json", "vite.config.ts", "package.json"]
},
"dev": {
"cache": false,
"persistent": true
},
"start": {
"cache": false,
"persistent": true,
"dependsOn": ["build"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"],
"inputs": ["src/**", "test/**", "vitest.config.ts", "tsconfig.json"]
},
"test:ui": {
"cache": false,
"persistent": true,
"dependsOn": ["^build"]
},
"test:coverage": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"],
"inputs": ["src/**", "test/**", "vitest.config.ts", "tsconfig.json"]
},
"test:e2e": {
"dependsOn": ["build"],
"outputs": ["playwright-report/**", "test-results/**"],
"inputs": ["src/**", "e2e/**", "playwright.config.ts"]
},
"test:e2e:ui": {
"cache": false,
"persistent": true,
"dependsOn": ["build"]
},
"lint": {
"outputs": [],
"inputs": ["src/**", ".eslintrc.json", "tsconfig.json"]
},
"lint:fix": {
"cache": false,
"outputs": ["src/**"],
"inputs": ["src/**", ".eslintrc.json", "tsconfig.json"]
},
"format": {
"cache": false,
"outputs": ["src/**"],
"inputs": [".prettierrc", ".prettierignore"]
},
"format:check": {
"outputs": [],
"inputs": ["src/**", ".prettierrc", ".prettierignore"]
},
"copy:icons": {
"outputs": ["dist/icons/**"],
"inputs": ["src/icons/**"]
},
"build:upload": {
"dependsOn": ["build"],
"outputs": ["dist/upload.html", "dist/upload.js"],
"inputs": ["src/upload.tsx", "src/upload.html", "scripts/build-upload.mjs"]
}
}
}