@@ -17,75 +17,58 @@ const DATA_CONFIG = {
1717
1818// Helper to decode optimized JSON keys
1919const 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 ? | P r o v i s i o n a l ) $ / ) ;
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" >
0 commit comments