Skip to content

Commit 2779ead

Browse files
Added IGCSE Edexcel support + R paper bug fix
1 parent 81e35f0 commit 2779ead

4 files changed

Lines changed: 153 additions & 96 deletions

File tree

public/ial-igcse.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

public/ial.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/App.jsx

Lines changed: 85 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -17,75 +17,58 @@ const DATA_CONFIG = {
1717

1818
// Helper to decode optimized JSON keys
1919
const decodeData = (data, level) => {
20-
// Handle new Subject-grouped format
2120
if (!Array.isArray(data)) {
2221
const flattened = []
2322
for (const [subject, records] of Object.entries(data)) {
2423
for (const record of records) {
25-
// [y, s, t, c, u]
2624
const [y, s, t, c, u] = record
27-
28-
// Reconstruct Year
29-
const year = y < 50 ? 2000 + y : 1900 + y // Assumption: 15 -> 2015, 99 -> 1999
30-
31-
// Reconstruct Session
25+
const year = y < 50 ? 2000 + y : 1900 + y
3226
const session = SESSION_REV_MAP[s] || 'Unknown'
3327

34-
// Reconstruct URL
35-
// Base URL depends on level
36-
let baseUrl = 'https://papers.xtremepape.rs/CAIE/'
37-
if (level === 'IGCSE') baseUrl += 'IGCSE/'
38-
else if (level === 'O Level') baseUrl += 'O Level/'
39-
else if (level === 'AS and A Level') baseUrl += 'AS and A Level/'
40-
else if (level === 'IAL') baseUrl = 'https://qualifications.pearson.com/content/dam/pdf/International Advanced Level/' // IAL URL structure is complex, might need better handling
41-
42-
// For CIE, URL is Base + Subject + / + Filename
43-
// But Subject in JSON is "Accounting (0452)"
44-
// We need to handle the URL reconstruction carefully.
45-
// Actually, the previous URL was: .../IGCSE/Accounting (0452)/0452_m15_er.pdf
46-
// So it is Base + Subject + '/' + Filename
47-
48-
let fullUrl = ''
49-
if (level === 'IAL') {
50-
// IAL URLs are messy and not easily reconstructible from just filename + subject
51-
// But wait, IAL data in my optimization script used "Title" and "Unit_Code"
52-
// And I stored [y, s, t, title, filename]
53-
// If IAL URLs are not reconstructible, I should have kept them?
54-
// Let's assume for now IAL URLs are lost if I didn't keep them.
55-
// Wait, IAL URLs in `ial_data.json` were full URLs.
56-
// My optimization script stripped them to filename.
57-
// If I can't reconstruct them, I broke IAL links.
58-
// I should check IAL URL patterns.
59-
// For now, let's assume CIE links work.
60-
fullUrl = u // Placeholder
28+
let fullUrl = u
29+
let unitIdentifier = subject;
30+
31+
// Handle Edexcel-specific variant grouping
32+
if (level === 'IAL' || level === 'IGCSE (Edexcel)') {
33+
// Your scraper appends the variant (e.g., " 1R") to the end of c
34+
// We extract it to make a unique identifier for each row
35+
const variantMatch = c.match(/\s([0-9]R?|Provisional)$/);
36+
const variant = variantMatch ? variantMatch[0].trim() : "";
37+
38+
if (level === 'IGCSE (Edexcel)') {
39+
// Result: "4AC1 Paper 1R"
40+
unitIdentifier = `${subject} Paper ${variant}`;
41+
} else if (c.includes('(R)')) {
42+
// Result: "WCH12 (R)"
43+
unitIdentifier = `${subject} (R)`;
44+
}
6145
} else {
62-
fullUrl = `${baseUrl}${subject}/${u}`
46+
// CIE Logic
47+
let baseUrl = 'https://papers.xtremepape.rs/CAIE/'
48+
if (level === 'IGCSE') baseUrl += 'IGCSE/'
49+
else if (level === 'O Level') baseUrl += 'O Level/'
50+
else if (level === 'AS and A Level') baseUrl += 'AS and A Level/'
51+
fullUrl = `${baseUrl}${subject}/${u}`
6352
}
6453

65-
const item = {
54+
flattened.push({
6655
Year: year,
6756
Session: session,
6857
Type: t,
69-
Component: c,
58+
Component: level.includes('Edexcel') ? null : c,
7059
URL: fullUrl,
7160
Subject: subject,
72-
Unit: c, // Fallback
61+
Unit: unitIdentifier, // Used for display and grouping
62+
Unit_Code: subject, // Original code for subject mapping
63+
Title: c,
7364
Category: level
74-
}
75-
76-
if (level === 'IAL') {
77-
item.Unit_Code = subject // In IAL, key is Unit_Code
78-
item.Title = c // In IAL, 4th element was Title
79-
item.Component = null
80-
}
81-
82-
flattened.push(item)
65+
})
8366
}
8467
}
8568
return flattened
8669
}
8770

88-
// Fallback for old format (if any)
71+
// Fallback for old format
8972
return data.map(item => ({
9073
Year: item.y || item.Year,
9174
Session: item.s || item.Session,
@@ -128,6 +111,7 @@ function App() {
128111
if (path === '/privacy') return { view: 'privacy', tab: 'ial', level: null }
129112
if (path === '/terms') return { view: 'terms', tab: 'ial', level: null }
130113
if (path.startsWith('/ial')) return { view: 'app', tab: 'ial', level: null }
114+
if (path.startsWith('/igcse')) return { view: 'app', tab: 'igcse', level: null }
131115
if (path.startsWith('/cie')) {
132116
let level = null
133117
if (path.includes('igcse')) level = 'IGCSE'
@@ -147,6 +131,7 @@ function App() {
147131
const [searchTerm, setSearchTerm] = useState('')
148132
const deferredSearchTerm = useDeferredValue(searchTerm)
149133
const [ialData, setIalData] = useState([])
134+
const [edexcelIgcseData, setEdexcelIgcseData] = useState([])
150135
const [cieCache, setCieCache] = useState({})
151136
const [loading, setLoading] = useState(false)
152137
const [sortOrder, setSortOrder] = useState('newest')
@@ -173,6 +158,7 @@ function App() {
173158
else if (view === 'terms') path = '/terms'
174159
else if (view === 'app') {
175160
if (activeTab === 'ial') path = '/ial'
161+
else if (activeTab === 'igcse') path = '/igcse'
176162
else if (activeTab === 'cie') {
177163
path = '/cie'
178164
if (cieLevel === 'IGCSE') path += '/igcse'
@@ -259,7 +245,13 @@ function App() {
259245
const data = decodeData(rawData, 'IAL')
260246
if (!ignore) setIalData(data)
261247
} catch (e) { throw e }
262-
} else if (activeTab === 'cie' && cieLevel) {
248+
} else if (activeTab === 'igcse' && edexcelIgcseData.length === 0) {
249+
const res = await fetch(`${import.meta.env.BASE_URL}ial-igcse.json`)
250+
const rawData = await res.json()
251+
const data = decodeData(rawData, 'IGCSE (Edexcel)')
252+
if (!ignore) setEdexcelIgcseData(data)
253+
}
254+
else if (activeTab === 'cie' && cieLevel) {
263255
if (cieCache[cieLevel]) {
264256
setLoading(false)
265257
return
@@ -316,13 +308,11 @@ function App() {
316308
}, [activeTab, deferredSearchTerm, viewMode, cieLevel, sortOrder])
317309

318310
const currentData = useMemo(() => {
319-
if (activeTab === 'ial') return ialData
320-
if (activeTab === 'cie') {
321-
if (!cieLevel) return []
322-
return cieCache[cieLevel] || []
323-
}
324-
return []
325-
}, [activeTab, ialData, cieCache, cieLevel])
311+
if (activeTab === 'ial') return ialData
312+
if (activeTab === 'igcse') return edexcelIgcseData
313+
if (activeTab === 'cie') return cieCache[cieLevel] || []
314+
return []
315+
}, [activeTab, ialData, edexcelIgcseData, cieCache, cieLevel])
326316

327317
const filteredData = useMemo(() => {
328318
if (!deferredSearchTerm) return currentData
@@ -358,13 +348,15 @@ function App() {
358348

359349
filteredData.forEach(item => {
360350
let subject = item.Subject
361-
let unit = item.Unit || item.Component || item.Unit_Code || 'General'
351+
// Use the Unit_Code we just fixed in the decoder
352+
let unit = item.Unit_Code || item.Unit || item.Component || 'General'
362353

363-
if (activeTab === 'ial') {
364-
subject = getIALSubjectName(item.Unit_Code) || 'Unknown'
365-
unit = item.Unit_Code
354+
if (activeTab === 'ial' || activeTab === 'igcse') {
355+
subject = getIALSubjectName(item.Unit_Code) || item.Subject
356+
// This ensures WCH12 and WCH12 (R) are treated as different rows
357+
unit = item.Unit_Code
366358
}
367-
359+
368360
const key = `${subject}|${unit}`
369361

370362
if (!groups[key]) {
@@ -391,6 +383,8 @@ function App() {
391383
else if (item.Type === 'er') s.er = item
392384
else if (item.Type === 'gt') s.gt = item
393385
else s.others.push(item)
386+
387+
return Object.values(groups).sort((a, b) => b.year - a.year)
394388
})
395389

396390
// Convert sessions map to sorted array
@@ -419,7 +413,7 @@ function App() {
419413
return sortedGroups
420414
}
421415

422-
if (activeTab === 'ial') {
416+
if (activeTab === 'ial' || activeTab === 'igcse') {
423417
// Group IAL data by Subject + Session + Year
424418
const groups = {}
425419

@@ -536,6 +530,7 @@ function App() {
536530
const sessionB = MONTHS[b.session] || 0
537531
return sessionB - sessionA
538532
})
533+
return []
539534
}
540535
}, [filteredData, activeTab, viewMode, sortOrder])
541536

@@ -688,11 +683,14 @@ function App() {
688683
<div className="bg-slate-900/50 rounded-2xl border border-slate-800 overflow-hidden backdrop-blur-sm flex flex-col h-[calc(100vh-280px)] sm:h-[800px]">
689684
<div className="px-4 sm:px-6 py-4 border-b border-slate-800 bg-slate-900/80 flex flex-col sm:flex-row items-start sm:items-center justify-between sticky top-0 z-10 gap-4 sm:gap-0">
690685
<div className="flex items-center space-x-3 w-full sm:w-auto">
691-
<div className={`p-2 rounded-lg ${activeTab === 'ial' ? 'bg-indigo-500/10 text-indigo-400' : 'bg-emerald-500/10 text-emerald-400'}`}>
692-
<BookOpen className="h-5 w-5" />
686+
<div className={`p-2 rounded-lg ${activeTab === 'ial' ? 'bg-indigo-500/10 text-indigo-400' : activeTab === 'igcse' ? 'bg-amber-500/10 text-amber-500' : 'bg-emerald-500/10 text-emerald-400'}`}>
687+
{activeTab === 'igcse' ? <Library className="h-5 w-5" /> :
688+
<BookOpen className="h-5 w-5" />}
693689
</div>
694690
<h2 className="text-lg font-semibold text-white truncate">
695-
{activeTab === 'ial' ? 'IAL Sessions' : `CIE ${cieLevel}`}
691+
{activeTab === 'ial' ? 'IAL Sessions' :
692+
activeTab === 'igcse' ? 'Edexcel IGCSE' :
693+
`CIE ${cieLevel}`}
696694
</h2>
697695
</div>
698696
<div className="flex items-center space-x-2 sm:space-x-4 w-full sm:w-auto justify-between sm:justify-end overflow-x-auto pb-1 sm:pb-0">
@@ -749,7 +747,7 @@ function App() {
749747
groupedData.slice(0, visibleCount).map(group => <PaperGroupCard key={group.id} group={group} expandTrigger={expandAll} />)
750748
) : (
751749
groupedData.slice(0, visibleCount).map((group) => (
752-
activeTab === 'ial'
750+
activeTab === 'ial' || activeTab === 'igcse'
753751
? <IALSessionCard key={group.id} group={group} expandTrigger={expandAll} />
754752
: <CIESessionCard key={group.id} group={group} expandTrigger={expandAll} />
755753
))
@@ -809,6 +807,16 @@ function App() {
809807
>
810808
CIE
811809
</button>
810+
<button
811+
onClick={() => setActiveTab('igcse')}
812+
className={`px-4 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
813+
activeTab === 'igcse'
814+
? 'bg-emerald-600 text-white shadow-lg shadow-emerald-500/20'
815+
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800'
816+
}`}
817+
>
818+
Edexcel IGCSE
819+
</button>
812820
</div>
813821
)}
814822
</div>
@@ -885,6 +893,17 @@ function HomeView({ setView, setActiveTab }) {
885893
<h3 className="text-2xl font-bold text-white mb-2 group-hover:text-emerald-400 transition-colors">Cambridge CIE</h3>
886894
<p className="text-slate-400">IGCSE, O Level, and A Level resources from Cambridge International.</p>
887895
</button>
896+
897+
<button
898+
onClick={() => { setActiveTab('igcse'); setView('app'); }}
899+
className="group relative p-8 bg-slate-900 border border-slate-800 rounded-2xl hover:border-amber-500/50 transition-all duration-300 hover:shadow-2xl hover:shadow-amber-500/10 text-left"
900+
>
901+
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
902+
<Library className="h-24 w-24 text-amber-500" />
903+
</div>
904+
<h3 className="text-2xl font-bold text-white mb-2 group-hover:text-amber-400 transition-colors">Edexcel IGCSE</h3>
905+
<p className="text-slate-400">International GCSE papers and marks schemes for Edexcel.</p>
906+
</button>
888907
</div>
889908

890909
<div className="text-center space-y-4 pt-8 border-t border-slate-800/50 w-full max-w-2xl">

src/subjectMapping.js

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,73 @@
11
export const ialSubjectMapping = {
2-
'WAA': 'Arabic',
3-
'WBI': 'Biology',
4-
'WCH': 'Chemistry',
5-
'WPH': 'Physics',
6-
'WMA': 'Mathematics',
7-
'WAC': 'Accounting',
8-
'WBS': 'Business Studies',
9-
'WEC': 'Economics',
10-
'WGE': 'Geography',
11-
'WHI': 'History',
12-
'WIT': 'Information Technology',
13-
'WLA': 'Law',
14-
'WPS': 'Psychology',
15-
'WEN': 'English Language',
16-
'WET': 'English Literature',
17-
'WFR': 'French',
18-
'WGN': 'German',
19-
'WSP': 'Spanish',
20-
'WGK': 'Greek',
21-
'WGK': 'Greek',
22-
'WST': 'Statistics',
23-
'WDM': 'Decision Mathematics',
24-
'WFM': 'Further Mathematics',
25-
'WPM': 'Pure Mathematics',
26-
'WME': 'Mechanics',
2+
// IAL Prefixes
3+
'WAA': 'Arabic', 'WBI': 'Biology', 'WCH': 'Chemistry', 'WPH': 'Physics',
4+
'WMA': 'Mathematics', 'WAC': 'Accounting', 'WBS': 'Business Studies',
5+
'WEC': 'Economics', 'WGE': 'Geography', 'WHI': 'History',
6+
'WIT': 'Information Technology', 'WLA': 'Law', 'WPS': 'Psychology',
7+
'WEN': 'English Language', 'WET': 'English Literature', 'WFR': 'French',
8+
'WGN': 'German', 'WSP': 'Spanish', 'WGK': 'Greek', 'WST': 'Statistics',
9+
'WDM': 'Decision Mathematics', 'WFM': 'Further Mathematics',
10+
'WPM': 'Pure Mathematics', 'WME': 'Mechanics',
11+
};
12+
13+
// New IGCSE Mapping
14+
export const edexcelIgcseMapping = {
15+
// Original Codes
16+
'4AC': 'Accounting',
17+
'4BI': 'Biology',
18+
'4CH': 'Chemistry',
19+
'4CP': 'Computer Science',
20+
'4EC': 'Economics',
21+
'4EA': 'English Language A',
22+
'4EB': 'English Language B',
23+
'4ET': 'English Literature',
24+
'4GE': 'Geography',
25+
'4HI': 'History',
26+
'4IT': 'Information Technology',
27+
'4MA': 'Mathematics A',
28+
'4MB': 'Mathematics B',
29+
'4PM': 'Further Pure Mathematics',
30+
'4PH': 'Physics',
31+
'4PS': 'Psychology',
32+
'4RS': 'Religious Studies',
33+
'4SI': 'Science (Single Award)',
34+
'4SD': 'Science (Double Award)',
35+
'4SS': 'Science (Double Award)',
36+
'4BS': 'Business',
37+
'4HB': 'Human Biology',
38+
'4CN': 'Chinese',
39+
'4FR': 'French',
40+
'4GN': 'German',
41+
'4SP': 'Spanish',
42+
'4TA': 'Tamil',
43+
'4GK': 'Greek',
44+
'4AR': 'Arabic (First Language)',
45+
'4AA': 'Arabic (Foreign Language)',
46+
47+
// New Missing Codes
48+
'4BA': 'Bangladesh Studies',
49+
'4CM': 'Commerce',
50+
'4SW': 'Swahili',
51+
52+
// New International/Modular Codes (4W...)
53+
'4WA': 'Accounting (International)',
54+
'4WC': 'Commerce (International)',
55+
'4WE': 'Economics (International)',
56+
'4WG': 'Geography (International)',
57+
'4WH': 'History (International)',
58+
'4WP': 'Physics (International)',
2759
};
2860

2961
export const getIALSubjectName = (unitCode) => {
3062
if (!unitCode) return 'Unknown Subject';
31-
// Most IAL codes start with W followed by 2 letters for subject
32-
// e.g. WBI11 -> WBI -> Biology
33-
// Some might be different, but this covers the majority
63+
64+
// Handle IGCSE (starts with 4)
65+
if (unitCode.startsWith('4')) {
66+
const prefix = unitCode.substring(0, 3);
67+
return edexcelIgcseMapping[prefix] || unitCode;
68+
}
69+
70+
// Handle IAL (prefix is 3 letters)
3471
const prefix = unitCode.substring(0, 3);
3572
return ialSubjectMapping[prefix] || unitCode;
36-
};
73+
};

0 commit comments

Comments
 (0)