Skip to content

Commit 0609cd9

Browse files
committed
feat: enhance verification tool with school rotation, styles, PDF export, and photo integration
1 parent 597a7cc commit 0609cd9

File tree

5 files changed

+576
-109
lines changed

5 files changed

+576
-109
lines changed

cloudflare-worker/worker.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Cloudflare Worker: Student Photo Proxy
3+
*
4+
* Purpose: Bypass CORS restrictions by proxying randomuser.me API requests
5+
* Deployment: Cloudflare Workers (Free tier: 100k requests/day)
6+
*
7+
* This worker:
8+
* 1. Receives requests from the frontend
9+
* 2. Fetches a random photo from randomuser.me API
10+
* 3. Returns the photo with proper CORS headers
11+
*/
12+
13+
export default {
14+
async fetch(request) {
15+
const url = new URL(request.url);
16+
17+
// Handle CORS preflight requests
18+
if (request.method === 'OPTIONS') {
19+
return handleOptions();
20+
}
21+
22+
// Only allow GET requests
23+
if (request.method !== 'GET') {
24+
return new Response('Method not allowed', { status: 405 });
25+
}
26+
27+
try {
28+
// Extract query parameters (optional: for controlling gender, etc.)
29+
const gender = url.searchParams.get('gender') || (Math.random() > 0.5 ? 'male' : 'female');
30+
31+
// Fetch from randomuser.me API (server-side, no CORS issues)
32+
const apiUrl = `https://randomuser.me/api/?inc=picture&gender=${gender}`;
33+
const apiResponse = await fetch(apiUrl);
34+
35+
if (!apiResponse.ok) {
36+
throw new Error('randomuser.me API failed');
37+
}
38+
39+
const data = await apiResponse.json();
40+
const photoUrl = data.results[0].picture.large;
41+
42+
console.log('[WORKER] Fetching photo:', photoUrl);
43+
44+
// Fetch the actual photo
45+
const photoResponse = await fetch(photoUrl);
46+
47+
if (!photoResponse.ok) {
48+
throw new Error('Photo fetch failed');
49+
}
50+
51+
// Get the photo as a blob
52+
const photoBlob = await photoResponse.blob();
53+
54+
// Return photo with CORS headers
55+
return new Response(photoBlob, {
56+
headers: {
57+
'Content-Type': 'image/jpeg',
58+
'Access-Control-Allow-Origin': '*',
59+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
60+
'Access-Control-Allow-Headers': 'Content-Type',
61+
'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
62+
'X-Proxy': 'Cloudflare Worker',
63+
},
64+
});
65+
66+
} catch (error) {
67+
console.error('[WORKER] Error:', error);
68+
69+
// Return error response with CORS headers
70+
return new Response(JSON.stringify({
71+
error: 'Failed to fetch photo',
72+
message: error.message
73+
}), {
74+
status: 500,
75+
headers: {
76+
'Content-Type': 'application/json',
77+
'Access-Control-Allow-Origin': '*',
78+
},
79+
});
80+
}
81+
},
82+
};
83+
84+
/**
85+
* Handle CORS preflight OPTIONS requests
86+
*/
87+
function handleOptions() {
88+
return new Response(null, {
89+
headers: {
90+
'Access-Control-Allow-Origin': '*',
91+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
92+
'Access-Control-Allow-Headers': 'Content-Type',
93+
'Access-Control-Max-Age': '86400', // 24 hours
94+
},
95+
});
96+
}

src/App.jsx

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useState, useRef } from 'react'
1+
import { useState, useRef, useEffect } from 'react'
22
import { INITIAL_STATE } from './constants/initialState'
33
import { generateRandomData } from './utils/randomData'
4-
import { exportPayslipToPdf, exportToPng, exportToZip } from './utils/pdfExport'
4+
import { exportPayslipToPdf, exportToPng, exportToZip, exportTeacherCardToPdf, getTeacherCardPdfBase64 } from './utils/pdfExport'
55
import Editor from './components/Editor'
66
import Preview from './components/Preview'
77
import './index.css'
@@ -10,9 +10,38 @@ function App() {
1010
const [state, setState] = useState(INITIAL_STATE)
1111
const [docType, setDocType] = useState('payslip') // 'payslip', 'tax', 'employment'
1212
const [mode, setMode] = useState('employee') // 'employee' or 'contractor'
13+
const [cardStyle, setCardStyle] = useState('original') // 'original', 'modern', 'simple'
1314
const [companyLogo, setCompanyLogo] = useState(null)
15+
const [photoBase64, setPhotoBase64] = useState(null)
1416
const logoInputRef = useRef(null)
1517

18+
// Cloudflare Worker URL for photo proxying
19+
const WORKER_URL = 'https://student-id-photo-proxy.thanhnguyxn.workers.dev';
20+
21+
const fetchPhoto = async (gender = 'male') => {
22+
try {
23+
const response = await fetch(`${WORKER_URL}?gender=${gender}`);
24+
if (!response.ok) throw new Error('Photo fetch failed');
25+
26+
const blob = await response.blob();
27+
const reader = new FileReader();
28+
reader.onloadend = () => {
29+
setPhotoBase64(reader.result);
30+
};
31+
reader.readAsDataURL(blob);
32+
} catch (error) {
33+
console.error('Failed to fetch photo:', error);
34+
// Fallback to Dicebear if worker fails
35+
const seed = Math.random().toString(36).substring(7);
36+
setPhotoBase64(`https://api.dicebear.com/7.x/avataaars/svg?seed=${seed}`);
37+
}
38+
};
39+
40+
// Fetch initial photo
41+
useEffect(() => {
42+
fetchPhoto();
43+
}, []);
44+
1645
const handleChange = (section, field, value) => {
1746
setState(prev => ({
1847
...prev,
@@ -58,6 +87,11 @@ function App() {
5887
}
5988
}
6089

90+
// Expose PDF generation to window for Puppeteer
91+
useEffect(() => {
92+
window.getTeacherCardPdfBase64 = () => getTeacherCardPdfBase64(state);
93+
}, [state]);
94+
6195
return (
6296
<div className="app-container">
6397
{/* Modern Navigation Bar */}
@@ -125,18 +159,43 @@ function App() {
125159
>
126160
Teacher ID
127161
</button>
162+
163+
{/* Style Selector */}
164+
{(docType === 'teacherCard') && (
165+
<select
166+
value={cardStyle}
167+
onChange={(e) => setCardStyle(e.target.value)}
168+
style={{
169+
marginLeft: '1rem',
170+
padding: '8px 12px',
171+
borderRadius: '8px',
172+
border: 'none',
173+
background: 'rgba(255,255,255,0.2)',
174+
color: 'white',
175+
fontSize: '0.9rem',
176+
cursor: 'pointer'
177+
}}
178+
>
179+
<option value="original" style={{ color: '#333' }}>🎨 Original</option>
180+
<option value="modern" style={{ color: '#333' }}>✨ Modern</option>
181+
<option value="simple" style={{ color: '#333' }}>📄 Simple</option>
182+
</select>
183+
)}
128184
</div>
129185

130186
<div className="nav-right">
131187
<button
132188
className="action-btn primary"
133-
onClick={() => setState(generateRandomData())}
189+
onClick={() => {
190+
setState(generateRandomData());
191+
fetchPhoto(Math.random() > 0.5 ? 'male' : 'female');
192+
}}
134193
>
135194
🎲 Random
136195
</button>
137196
<button
138197
className="action-btn secondary"
139-
onClick={() => exportPayslipToPdf(state)}
198+
onClick={() => docType === 'teacherCard' ? exportTeacherCardToPdf(state) : exportPayslipToPdf(state)}
140199
title="Download current document as PDF"
141200
>
142201
📄 PDF
@@ -184,6 +243,8 @@ function App() {
184243
docType={docType}
185244
mode={mode}
186245
companyLogo={companyLogo}
246+
cardStyle={cardStyle}
247+
photoBase64={photoBase64}
187248
/>
188249
</main>
189250
</div>

0 commit comments

Comments
 (0)