1// hub-engine.js - Bradley Gold Standard Unified Engine 2 const BradleyHub = { 3 state: { 4 tier: localStorage.getItem('bradley_tier') || 'gcse', 5 seenIds: JSON.parse(localStorage.getItem('bradley_seen_ids') || '[]'), 6 correctIds: JSON.parse(localStorage.getItem('bradley_correct_ids') || '[]'), 7 // --- NEW: PAYWALL TRACKING --- 8 freeQuestionsLeft: localStorage.getItem('bradley_free_left') !== null ? parseInt(localStorage.getItem('bradley_free_left')) : 20, 9 isPremium: localStorage.getItem('bradley_premium') === 'true', 10 masterVault: [], 11 activeMonths: ['2025-12', '2026-01', '2026-02', '2026-03', '2026-04', '2026-05', '2026-06', '2026-07', '2026-08'], // Updated with standard YYYY-MM 12 currentGroup: null, 13 isTeacherMode: false 14 }, 15 16 // AUTOMATIC WORKSHEET LOOKUP DIRECTORY (Separated by Tier) 17 worksheetDirectory: { 18 gcse: { 19 // --- NUMBER --- 20 "Natural Numbers and Integers": "https://payhip.com/b/NF60m", 21 "Squares and Cubes": "https://payhip.com/b/qOE5o", 22 "Prime Numbers HCF LCM": "https://payhip.com/b/ul4LH", 23 "Rational and Irrational Numbers and Reciprocals": "https://payhip.com/b/ND45g", 24 "Priority of Operations": "https://payhip.com/b/85Ieb", 25 "Using a Calculator": "https://payhip.com/b/KTXaO", 26 "Integer Indices and Roots": "https://payhip.com/b/fBMNb", 27 "Fractional Indices and Roots": "https://payhip.com/b/iU6Nj", 28 "Standard Form": "https://payhip.com/b/ZAkTu", 29 "Surds": "https://payhip.com/b/BaZjD", 30 "Ordering": "https://payhip.com/b/peXuz", 31 "Operations with Integers and Decimals": "https://payhip.com/b/NKrd4", 32 "Multiplication and Division of Fractions": "https://payhip.com/b/Qd4GN", 33 "Addition and Subtraction of Fractions": "https://payhip.com/b/Da8mk", 34 "Operations with Mixed Numbers": "https://payhip.com/b/c0tPv", 35 "Rounding, Estimation and Error Intervals": "https://payhip.com/b/eg8sA", 36 "Limits of Accuracy and Bounds": "https://payhip.com/b/qYp8g", 37 "Fraction Decimal Conversion": "https://payhip.com/b/XoqhS", 38 "Fraction, Decimal, Percentage Conversions": "https://payhip.com/b/K7zsa", 39 "Recurring Decimals to Fractions": "https://payhip.com/b/dAvne", 40 "Basic Percentages": "https://payhip.com/b/U3CGa", 41 "Percentage Change and Interest": "https://payhip.com/b/X8Yo2", 42 "Reverse Percentages": "https://payhip.com/b/UYy0Q", 43 "Ratio and Sharing": "https://payhip.com/b/o7A2P", 44 "Direct and Inverse Proportion": "https://payhip.com/b/JYBkq", 45 "Compound Units: Speed, Density, Pressure": "https://payhip.com/b/DK7b1", 46 "Rates of Pay, Unit pricing, and Currency": "https://payhip.com/b/WeoVy", 47 "Time": "https://payhip.com/b/flD19", 48 "Growth and Decay": "https://payhip.com/b/eRHBx", 49 "General Iterative Processes": "https://payhip.com/b/rAIp4", 50 "Systematic Listing and the Product Rule": "https://payhip.com/b/o3aVn", 51 "Scale Factors, Diagrams, and Maps": "https://payhip.com/b/SIUqC", 52 "Comparing Lengths, Areas, and Volumes": "https://payhip.com/b/bgm5B", 53 54 // --- ALGEBRA --- 55 "Introduction to Algebra": "https://payhip.com/b/KaG8c", 56 "Simplifying Expressions": "https://payhip.com/b/yAeG9", 57 "Expanding Brackets": "https://payhip.com/b/yac9b", 58 "Factorising Expressions 1": "https://payhip.com/b/SHDG1", 59 "Factorising Expressions 2": "https://payhip.com/b/xqVUj", 60 "Completing the Square": "https://payhip.com/b/miLap", 61 "Algebraic Proof": "https://payhip.com/b/EKtuw", 62 "Algebraic Fractions 1": "https://payhip.com/b/KPeax", 63 "Algebraic Fractions 2": "https://payhip.com/b/jJHdP", 64 "Rational Expressions": "https://payhip.com/b/Natzp", 65 "Indices": "https://payhip.com/b/hGzMV", 66 "Constructing Expressions": "https://payhip.com/b/ZFqwQ", 67 "Solving Linear Equations": "https://payhip.com/b/v65fA", 68 "Solving Fractional Equations": "https://payhip.com/b/wdgX1", 69 "Simultaneous Linear Equations": "https://payhip.com/b/Cq1JG", 70 "Simultaneous Equations Non-Linear": "https://payhip.com/b/BqWhb", 71 "Quadratics Equations Factorising": "https://payhip.com/b/lGQis", 72 "Quadratic Equations - Formula and Completing the Square": "https://payhip.com/b/7CT3V", 73 "Iteration": "https://payhip.com/b/IoYTp", 74 "Changing the Subject of a Formula": "https://payhip.com/b/9r34U", 75 "GCSE Higher Tier Maths - Linear inequalities": "https://payhip.com/b/d84xC", 76 "Linear inequalities in One Variable": "https://payhip.com/b/d84xC",
77 "Linear Inequalities in two Variables": "https://payhip.com/b/r7o0O", 78 "Sequences": "https://payhip.com/b/GWKby", 79 "Functions: Domain and Range": "https://payhip.com/b/F9KRd", 80 "Inverse Functions": "https://payhip.com/b/SIpux", 81 "Composite Functions": "https://payhip.com/b/A7Wup", 82 "Cartesian Coordinates": "https://payhip.com/b/qkrPY", 83 "Plotting Linear Graphs": "https://payhip.com/b/lHJUa", 84 "Gradient": "https://payhip.com/b/uEo2p", 85 "Length and Midpoint of a Line Segment": "https://payhip.com/b/zijkw", 86 "Equations of Linear Graphs": "https://payhip.com/b/U8umo", 87 "Parallel and Perpendicular Lines 1": "https://payhip.com/b/y07Oc", 88 "Parallel and Perpendicular Lines 2": "https://payhip.com/b/NZ7ha", 89 "Graphs in Practical Situations": "https://payhip.com/b/7keWD", 90 "Rates of Change from Graphs": "https://payhip.com/b/cpuDV", 91 "Plotting Curved Graphs": "https://payhip.com/b/5b3Av", 92 "Solving Equations Graphically": "https://payhip.com/b/uF4cK", 93 "Sketching Graphs": "https://payhip.com/b/t8xQq", 94 "Graphical Representation of Inverse Proportion": "https://payhip.com/b/uEa8o", 95 "Quadratic Inequalities": "https://payhip.com/b/3oWKQ", 96 "Gradients and Area under Graphs": "https://payhip.com/b/9wXvI", 97 "Equation of a Circle": "https://payhip.com/b/OnqfT", 98 "Trigonometric Graphs": "https://payhip.com/b/uyDvx", 99 "Solving Trig Equations Graphically": "https://payhip.com/b/AtoLW", 100 "Transformations": "https://payhip.com/b/FDaTw", 101 102 // --- GEOMETRY --- 103 "Angle Properties": "https://payhip.com/b/lxSzN", 104 "Triangles": "https://payhip.com/b/BHgYW", 105 "Quadrilaterals": "https://payhip.com/b/eXEl8", 106 "Polygons": "https://payhip.com/b/4zntl", 107 "Circle Properties": "https://payhip.com/b/mCeEQ", 108 "Symmetry in 2D": "https://payhip.com/b/8nGQw", 109 "Geometrical Constructions": "https://payhip.com/b/ErOuY", 110 "Loci": "https://payhip.com/b/Ql5p0", 111 "Scale Drawings and Bearings": "https://payhip.com/b/iW9eY", 112 "Similarity in 2D": "https://payhip.com/b/L2dNU", 113 "Triangle Congruence": "https://payhip.com/b/FS9zC", 114 "Geometrical Proof": "https://payhip.com/b/R1v2e", 115 "Introduction to 3D Solids and Nets": "https://payhip.com/b/PjiBG", 116 "Plans and Elevations": "https://payhip.com/b/9KeKR", 117 "Symmetry in 3D": "https://payhip.com/b/AfsYi", 118 "Similarity in 3D": "https://payhip.com/b/iBzE5", 119 "Circle Theorems 1": "https://payhip.com/b/RLgSJ", 120 "Circle Theorems 2": "https://payhip.com/b/wm0xL", 121 "Circle Theorem Proofs": "https://payhip.com/b/1rJUj", 122 "Units of Measure": "https://payhip.com/b/OItpm", 123 "Perimeter and Area in 2D": "https://payhip.com/b/zukBR", 124 "Circumference and Area of Circles": "https://payhip.com/b/K8QAT", 125 "Arcs and Sectors": "https://payhip.com/b/fiURV", 126 "Surface Area and Volume in 3D": "https://payhip.com/b/aJhQL", 127 "Compound Shapes in 2D": "https://payhip.com/b/z2KF1", 128 "Compound Shapes in 3D": "https://payhip.com/b/m8Stz", 129 "Pythagoras Theorem": "https://payhip.com/b/9dSh3", 130 "Right Angled Trigonometry": "https://payhip.com/b/h3BS6", 131 "Exact Trigonometric values": "https://payhip.com/b/GToVZ", 132 "Sine Rule": "https://payhip.com/b/EaYM1", 133 "Cosine Rule": "https://payhip.com/b/RHeNL", 134 "Trigonometry in 3D": "https://payhip.com/b/QsiXK", 135 "Vectors in 2D": "https://payhip.com/b/lemlz", 136 "Vector Geometry": "https://payhip.com/b/CZOJ4", 137 "Vector Problem Solving": "https://payhip.com/b/LTjeD", 138 139 // --- PROBABILITY & STATISTICS --- 140 "Introduction to Probability": "https://payhip.com/b/Z7KYo", 141 "Relative and Expected Frequencies": "https://payhip.com/b/csAoM", 142 "Systematic listing Strategies": "https://payhip.com/b/1nLdO", 143 "Frequency Trees": "https://payhip.com/b/3Qiey", 144 "Sets and Venn Diagrams": "https://payhip.com/b/oW2c1", 145 "The Addition Rule": "https://payhip.com/b/Jr67s", 146 "Probability Tree diagrams": "https://payhip.com/b/8EhLZ", 147 "Probability of Combined Events": "https://payhip.com/b/FGC0u", 148 "Conditional Probability": "https://payhip.com/b/MoE4j", 149 "Sampling": "https://payhip.com/b/rCsYM", 150 "Data Handling and Interpretation": "https://payhip.com/b/YC2BF", 151 "Averages and Spread â Ungrouped Data": "https://payhip.com/b/vRHXh", 152 "Averages and Spread â Grouped Data": "https://payhip.com/b/vZuiV", 153 "Statistical Charts and Diagrams": "https://payhip.com/b/59j80", 154 "Time Series": "https://payhip.com/b/19T3Q", 155 "Scatter Diagrams": "https://payhip.com/b/JLTGt", 156 "Cumulative Frequency Diagrams": "https://payhip.com/b/fmvej",
157 "Box and Whisker Plots": "https://payhip.com/b/L5f6Y", 158 "Histograms": "https://payhip.com/b/ND5E9" 159 }, 160 igcse: { 161 // --- NUMBER --- 162 "Natural Numbers and Integers": "https://payhip.com/b/3mzNO", 163 "Squares and Cubes": "https://payhip.com/b/dOlIS", 164 "Prime Numbers, HCF, LCM": "https://payhip.com/b/Ljdog", 165 "Rationals, Irrationals and Reciprocals": "https://payhip.com/b/PivFe", 166 "Sets and Venn Diagrams": "https://payhip.com/b/Xg21p", 167 "Integer Indices": "https://payhip.com/b/ao0mZ", 168 "Fraction Decimal Conversion": "https://payhip.com/b/Kj3IV", 169 "Fractions, Decimals, Percentages": "https://payhip.com/b/x41Cy", 170 "Recurring Decimals": "https://payhip.com/b/XYDWV", 171 "Ordering": "https://payhip.com/b/2DA9q", 172 "The Four Operations (Integers and Decimals)": "https://payhip.com/b/sOdmt", 173 "Multiplication and Division of Fractions": "https://payhip.com/b/M1Q2H", 174 "Addition and Subtraction of Fractions": "https://payhip.com/b/5RxOz", 175 "The Four Operations with Mixed Numbers": "https://payhip.com/b/gpWKf", 176 "Fractional Indices and Roots": "https://payhip.com/b/ZKKG2", 177 "Standard Form": "https://payhip.com/b/rTMY4", 178 "Estimation": "https://payhip.com/b/dyVCM", 179 "Limits of Accuracy": "https://payhip.com/b/9LFtZ", 180 "Ratio and Proportion": "https://payhip.com/b/djNVP", 181 "Rates": "https://payhip.com/b/pVxqP", 182 "Percentages (Basic Calculations)": "https://payhip.com/b/ePmz8", 183 "Percentage Change and Interest": "https://payhip.com/b/Ebcut", 184 "Reverse Percentages": "https://payhip.com/b/1ZaHA", 185 "Using a Calculator": "https://payhip.com/b/L2W09", 186 "Time": "https://payhip.com/b/XKUVE", 187 "Money": "https://payhip.com/b/fVsJi", 188 "Exponential Growth and Decay": "https://payhip.com/b/JrS9d", 189 "Surds": "https://payhip.com/b/QTHoU", 190 191 // --- ALGEBRA --- 192 193 "Introduction to Algebra": "https://payhip.com/b/z4xbu", 194 "Simplifying Expressions": "https://payhip.com/b/HU9z8", 195 "Expanding Products": "https://payhip.com/b/G9rN4", 196 "Factoring Expressions": "https://payhip.com/b/emNE1", 197 "Factorising 2": "https://payhip.com/b/ZsgUq", 198 "Completing the Square": "https://payhip.com/b/NTGZX", 199 "Algebraic Fractions 1": "https://payhip.com/b/2UYz3", 200 "Algebraic Fractions 2": "https://payhip.com/b/x02Ge", 201 "Factorise and Simplify Rational Expressions": "https://payhip.com/b/V07A3", 202 "Indices II": "https://payhip.com/b/0RBC9", 203 "Constructing Expressions, Equations and Formulas": "https://payhip.com/b/5gq1m", 204 "Solving Linear Equations in One Unknown": "https://payhip.com/b/yxGRl", 205 "Fractional Linear Equations": "https://payhip.com/b/8H6Yg", 206 "Simultaneous Linear Equations": "https://payhip.com/b/XW1im", 207 "Simultaneous Equations (Linear â Quadratic)": "https://payhip.com/b/0CP5n", 208 "Simultaneous Equations in Context": "https://payhip.com/b/WZ92k", 209 "Quadratic Equations 1": "https://payhip.com/b/elEAX", 210 "Non-Factorisable Quadratics": "https://payhip.com/b/vs4gh", 211 "Changing the Subject of a Formula": "https://payhip.com/b/Ipgz3", 212 "Linear Inequalities": "https://payhip.com/b/HYsu3", 213 "Linear Inequalities in Two Variables": "https://payhip.com/b/Of64Q", 214 "Sequences": "https://payhip.com/b/5HqEG", 215 "Proportion": "https://payhip.com/b/e47ks", 216 "Graphs in Practical Situations": "https://payhip.com/b/wyZ3k", 217 "Plotting Curved Graphs": "https://payhip.com/b/D82tp", 218 "Solving equations graphically": "https://payhip.com/b/EnSXH", 219 "Sketching graphs": "https://payhip.com/b/Et8dD", 220 "Estimating Gradients and Basic Differentiation": "https://payhip.com/b/Znxo4", 221 "Stationary Points": "https://payhip.com/b/pL8io", 222 "Functions, Domain, Range and Notation": "https://payhip.com/b/Ttui3", 223 "Inverse Functions": "https://payhip.com/b/w0oYj", 224 "Composite Functions": "https://payhip.com/b/vU1Tk", 225 226 //COORDINATE GEOMETRY AND GEOMETRY 227 228 "Cartesian Coordinates": "https://payhip.com/b/nkrUT", 229 "Plotting Linear Graphs": "https://payhip.com/b/gdyY8", 230 "Gradient of a Line": "https://payhip.com/b/kqp1P", 231 "Length and Midpoint of a Line Segment": "https://payhip.com/b/BO8dT", 232 "Equations of Linear Graphs": "https://payhip.com/b/hboKE", 233 "Parallel and Perpendicular Lines, Core Concepts": "https://payhip.com/b/WfQZA", 234 "Parallel and Perpendicular Lines, Mixed Problems": "https://payhip.com/b/yhHQo", 235 "Triangles": "https://payhip.com/b/iCURe", 236 "Quadrilaterals": "https://payhip.com/b/JWU5q", 237 "Polygons and Circles": "https://payhip.com/b/InbTp", 238 "Introduction to 3D Solids and Nets": "https://payhip.com/b/C0nbc", 239 "Geometric Constructions and Nets": "https://payhip.com/b/k96M2", 240 "Scale Drawings and Bearings": "https://payhip.com/b/yjBmH", 241 "Similarity in 2D Shapes": "https://payhip.com/b/R9W2O", 242 "Solids and the Linear, Area, Volume Relationship": "https://payhip.com/b/e9UMS",
243 "Symmetry in 2D Shapes": "https://payhip.com/b/E0TnV", 244 "Symmetry in 3D Shapes": "https://payhip.com/b/8k2wM", 245 "Angle Properties": "https://payhip.com/b/hlX9d", 246 "Circle Theorems 1": "https://payhip.com/b/1y6TA", 247 "Circle Theorems 2": "https://payhip.com/b/xrmnz", 248 249 //--- MENSURATION AND TRIGONOMETRY 250 251 "Units of Measure": "https://payhip.com/b/Fp1f6", 252 "Area and Perimeter": "https://payhip.com/b/XrjWA", 253 "Circles": "https://payhip.com/b/K4up0", 254 "Arcs and Sectors": "https://payhip.com/b/gR9jw", 255 "Surface Areas and Volumes": "https://payhip.com/b/RH1sZ", 256 "2D Compound Shapes": "https://payhip.com/b/a1FT0", 257 "3D Compound Shapes": "https://payhip.com/b/Y6gsh", 258 "Pythagoras Theorem": "https://payhip.com/b/uZ8cD", 259 "SOHCAHTOA": "https://payhip.com/b/kXZLe", 260 "Exact Trigonometric Values": "https://payhip.com/b/cHHoW", 261 "Graphs of Trigonometric Functions": "https://payhip.com/b/VxA7J", 262 "Solving Trigonometric Equations": "https://payhip.com/b/ea1Ti", 263 "The Sine Rule": "https://payhip.com/b/5UfgG", 264 "The Cosine Rule": "https://payhip.com/b/KLYAp", 265 "Pythagoras and Trigonometry in 3D": "https://payhip.com/b/udQPT", 266 267 //tRANSFORMATIONS AND VECTORS, PROBABILITY, AND STATISTICS 268 269 "Transformations": "https://payhip.com/b/U2nMw", 270 "Vectors in Two Dimensions": "https://payhip.com/b/oKRjO", 271 "Magnitude of a Vector": "https://payhip.com/b/LbBqE", 272 "Vector Geometry": "https://payhip.com/b/wzbn9", 273 "An Introduction to Probability": "https://payhip.com/b/kMVWL", 274 "Relative and Expected Frequencies": "https://payhip.com/b/ug9KE", 275 "Probability of Combined Events": "https://payhip.com/b/3AZnV", 276 "Conditional Probability": "https://payhip.com/b/XIALH", 277 "Data Handling and Interpretation": "https://payhip.com/b/Rwbot", 278 "Averages and Measures of Spread (Ungrouped Data)": "https://payhip.com/b/Q8WfX", 279 "Averages and Measures of Spread (Grouped Data)": "https://payhip.com/b/reAKG", 280 "Statistical Charts and Diagrams": "https://payhip.com/b/xuigq", 281 "Scatter Diagrams": "https://payhip.com/b/i5O9W", 282 "Cumulative Frequency Diagrams": "https://payhip.com/b/NhqEx", 283 "Histograms": "https://payhip.com/b/4AZYG" 284 } 285 }, 286 287 // DEFINING THE REVISION GROUPS (Funnel Logic) 288 getMenuMapping() { 289 if (this.state.tier === 'gcse') { 290 return [ 291 { id: "number", label: "Number", areas: ["Number", "Ratio, Proportion & Rates of Change"], link: "https://payhip.com/b/IVWzJ", btnText: "Master GCSE Number: Download the Pack" }, 292 { id: "algebra", label: "Algebra", areas: ["Algebra"], link: "https://payhip.com/b/wRN86", btnText: "Master GCSE Algebra: Download the Pack" }, 293 { id: "geometry", label: "Geometry", areas: ["Geometry & Measures", "Coordinate Geometry & Geometry"], link: "https://payhip.com/b/XAGch", btnText: "Master GCSE Geometry: Download the Pack" }, 294 { id: "stats", label: "Stats & Prob", areas: ["Statistics", "Probability"], link: "https://payhip.com/b/RVbqM", btnText: "Master GCSE Stats & Prob: Download the Pack" } 295 ]; 296 } else { 297 // IGCSE Extended 0580 Mapping 298 return [ 299 { id: "num", label: "Number & Ratio", areas: ["Number", "Ratio, Proportion & Rates of Change"], link: "https://payhip.com/b/XEV2Z", btnText: "Master IGCSE Number: Download the Pack" }, 300 { id: "alg", label: "Algebra", areas: ["Algebra", "Differentiation"], link: "https://payhip.com/b/mg5YS", btnText: "Master IGCSE Algebra: Download the Pack" }, 301 { id: "geom", label: "Coordinate Geometry & Geometry", areas: ["Geometry & Measures", "Coordinate Geometry & Geometry", "Angles", "Circle Theorems", "Transformations"], link: "https://payhip.com/b/L6skH", btnText: "Master IGCSE Geometry: Download the Pack" }, 302 { id: "trig", label: "Mensuration & Trigonometry", areas: ["Mensuration", "Trigonometry", "3D Shapes", "SOHCAHTOA", "Sine rule", "Cosine rule"], link: "https://payhip.com/b/KjXoP", btnText: "Master IGCSE Mensuration & Trig: Download the Pack" }, 303 { id: "vec", label: "Vectors, Stats & Probability", areas: ["Vectors", "Statistics", "Probability"], link: "https://payhip.com/b/rsVCz", btnText: "Master IGCSE Vectors & Stats: Download the Pack" } 304 ]; 305 } 306 }, 307 308 async init(mode, tier) { 309 this.state.isTeacherMode = (mode === 'audit'); 310 this.state.tier = tier || this.state.tier; 311 localStorage.setItem('bradley_tier', this.state.tier); 312 313 if (this.state.isTeacherMode) { 314 const picker = document.getElementById('month-picker'); 315 await this.loadMonthData(picker ? picker.value : '2026-04'); 316 this.renderAuditList(); 317 } else { 318 await this.loadAllActiveMonths(); 319 this.renderMenu(); 320 } 321 }, 322 323 // --- DATA LOADING --- 324 async loadAllActiveMonths() { 325 this.state.masterVault = []; 326 for (const period of this.state.activeMonths) { 327 const [year, mm] = period.split('-'); 328 await this.fetchFile(`problems/${this.state.tier}/${year}/${mm}.js`); 329 } 330 }, 331 332 async loadMonthData(period) { 333 this.state.masterVault = []; 334 const [year, mm] = period.split('-'); 335 await this.fetchFile(`problems/${this.state.tier}/${year}/${mm}.js`); 336 }, 337 338 async fetchFile(path) { 339 try { 340 const response = await fetch(path + '?v=' + new Date().getTime()); 341 if (!response.ok) return; 342 const text = await response.text(); 343 const arrayMatch = text.match(/\[[\s\S]*\]/); 344 if (arrayMatch) { 345 const monthData = eval(arrayMatch[0]); 346 this.state.masterVault = [...this.state.masterVault, ...monthData]; 347 } 348 } catch (e) { console.error("Filing error:", path); } 349 }, 350 351 // --- VIEW B: THE STUDENT HUB (GAMIFIED) --- 352 renderMenu() { 353 const mapping = this.getMenuMapping(); 354 const score = this.state.correctIds.filter(id => (this.state.tier === 'gcse' ? id.startsWith('002') : id.startsWith('003'))).length; 355 356 let rank = "Novice"; 357 let nextTarget = 5; 358 if (score >= 50) { rank = "Grade 9 Elite"; nextTarget = "MAX"; } 359 else if (score >= 30) { rank = "Grade 7-8 Master"; nextTarget = 50; } 360 else if (score >= 15) { rank = "Grade 6 Scholar"; nextTarget = 30; } 361 else if (score >= 5) { rank = "Grade 5 Challenger"; nextTarget = 15; } 362 363 let progressHTML = nextTarget === "MAX" 364 ? `<div style="font-size: 0.9rem; color: #065f46;">Highest Rank Achieved!</div>` 365 : `<div style="font-size: 0.85rem; color: #555;">Next rank at ${nextTarget} correct (Currently ${score})</div> 366 <div style="width: 100%; background: #ddd; height: 10px; border-radius: 5px; margin-top: 5px;"> 367 <div style="width: ${(score/nextTarget)*100}%; background: var(--brand-purple); height: 100%; border-radius: 5px; transition: width 0.5s;"></div> 368 </div>`; 369 let premiumStatus = this.state.isPremium 370 ? `<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(0,0,0,0.1); font-weight: bold; color: #059669;">ð Premium Access Unlocked</div>` 371 : `<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(0,0,0,0.1); font-weight: bold; color: #d97706;">â³ Free Trial: ${this.state.freeQuestionsLeft}
371 Questions Remaining</div>`; 372 373 const today = new Date(); 374 const p1 = new Date("2026-05-14"); 375 const p2 = new Date("2026-06-03"); 376 const p3 = new Date("2026-06-10"); 377 378 let countdownText = ""; 379 let daysToP1 = Math.ceil((p1 - today) / (1000 * 60 * 60 * 24)); 380 let daysToP2 = Math.ceil((p2 - today) / (1000 * 60 * 60 * 24)); 381 let daysToP3 = Math.ceil((p3 - today) / (1000 * 60 * 60 * 24)); 382 383 if (daysToP1 >= 0) countdownText = `ð¨ <strong>${daysToP1} Days</strong> until Paper 1!`; 384 else if (daysToP2 >= 0) countdownText = `ð¨ <strong>${daysToP2} Days</strong> until Paper 2!`; 385 else if (daysToP3 >= 0) countdownText = `ð¨ <strong>${daysToP3} Days</strong> until Paper 3!`; 386 else countdownText = `GCSE Exams are finished! Great work!`; 387 388 document.getElementById('hub-stage').innerHTML = ` 389 <div class="info-panel" style="background: white; border: 1px solid #ddd; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.05);"> 390 <div style="background: #fee2e2; border: 1px solid #f87171; color: #991b1b; padding: 12px; border-radius: 8px; font-size: 1.2rem; margin-bottom: 20px;"> 391 ${this.state.tier === 'gcse' ? countdownText : 'IGCSE Exams Complete!'} 392 </div> 393 <div class="hub-stats" style="background: var(--brand-green-light); padding: 20px; border-radius: 8px; margin-bottom: 25px; border: 2px solid var(--brand-green);"> 394 <h3 style="margin: 0 0 10px 0; color: var(--brand-purple-dark);">Your Revision Rank: ${rank}</h3> 395 ${progressHTML} 396 ${premiumStatus} 397 </div> 398 <h3 style="color: var(--brand-purple);">Step 1: Choose an Area</h3> 399 <div class="menu-grid"> 400 ${mapping.map(m => `<button class="menu-btn" onclick="BradleyHub.showDifficultyMenu('${m.id}')">${m.label}</button>`).join('')} 401 <button class="menu-btn random" style="background: linear-gradient(135deg, var(--brand-purple), var(--brand-purple-dark)); color: white; border: none; font-weight: bold; box-shadow: 0 4px 10px rgba(0,0,0,0.15);" onclick="BradleyHub.showDifficultyMenu('all')">â Random Exam Mix â </button> 402 </div> 403 <p style="margin-top:30px;"><a href="index.html" style="color: var(--text-muted); font-size: 0.85rem; text-decoration: underline;">Return to Homepage</a></p> 404 </div>`; 405 }, 406 407 showDifficultyMenu(groupId) { 408 let diffButtons = ''; 409 if (this.state.tier === 'gcse') { 410 diffButtons = ` 411 <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', '35')">Grades 3 - 5</button> 412 <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', '46')">Grades 4 - 6</button> 413 <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', '57')">Grades 5 - 7</button> 414 <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', '68')">Grades 6 - 8</button> 415 <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', '79')">Grades 7 - 9</button> 416 `; 417 } else { 418 diffButtons = ` 419 <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', 'EC')">Grades E - C</button> 420 <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', 'DB')">Grades D - B</button> 421 <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', 'CA')">Grades C - A</button> 422 <button class="menu-btn" onclick="BradleyHub.serveArena('${groupId}', 'BA*')">Grades B - A*</button> 423 `; 424 } 425 426 document.getElementById('hub-stage').innerHTML = ` 427 <div class="info-panel" style="background: white; border: 1px solid #ddd; text-align: center; box-shadow: 0 4px 6px rgba(0,0,0,0.05);"> 428 <h3 style="color: var(--brand-purple);">Step 2: Select Target Difficulty</h3> 429 <p>We will tailor the questions to match your target grade.</p> 430 <div class="menu-grid"> 431 <button class="menu-btn random" style="background: linear-gradient(135deg, var(--brand-green), #047857); color: white;" onclick="BradleyHub.serveArena('${groupId}', 'all')">â Any Difficulty â </button> 432 ${diffButtons} 433 </div> 434 <p style="margin-top:30px;"> 435 <button class="btn" style="background: var(--text-muted); color: white !important;" onclick="BradleyHub.renderMenu()">â Back to Topics</button> 436 </p> 437 </div> 438 `; 439 }, 440 441 checkPremiumAccess() { 442 const unlocked = localStorage.getItem('app_unlocked') === 'true'; 443 if (!unlocked) { 444 window.location.href = "unlock.html"; 445 return false;
446 } 447 this.state.isPremium = true; 448 return true; 449 }, 450 451 serveArena(groupId, targetDifficulty = 'all') { 452 this.state.currentGroup = groupId; 453 if (!this.state.isPremium && this.state.freeQuestionsLeft <= 0) { 454 this.renderPaywall(); 455 return; 456 } 457 const today = new Date(); 458 today.setHours(0,0,0,0); 459 460 const mapping = this.getMenuMapping(); 461 const group = mapping.find(m => m.id === groupId); 462 463 let pool = this.state.masterVault.filter(p => { 464 const unseen = !this.state.correctIds.includes(p.id); 465 const isPast = new Date(p.date) <= today; 466 467 let diffMatch = true; 468 if (targetDifficulty !== 'all' && p.difficulty) { 469 let grades = p.difficulty.replace(/[^A-Za-z0-9*]/g, ' ').split(/\s+/).filter(Boolean); 470 471 const targetMap = { 472 '35': ['3', '4', '5'], '46': ['4', '5', '6'], '57': ['5', '6', '7'], '68': ['6', '7', '8'], '79': ['7', '8', '9'], 473 'EC': ['E', 'D', 'C'], 'DB': ['D', 'C', 'B'], 'CA': ['C', 'B', 'A'], 'BA*': ['B', 'A', 'A*'] 474 }; 475 476 const validGrades = targetMap[targetDifficulty] || []; 477 diffMatch = grades.some(g => validGrades.includes(g)); 478 } 479 480 if (groupId === 'all') return unseen && isPast && diffMatch; 481 482 const areaMatch = group.areas.includes(p.major_area) || group.areas.includes(p.topic); 483 return areaMatch && unseen && isPast && diffMatch; 484 }); 485 486 if (pool.length === 0) { 487 document.getElementById('hub-stage').innerHTML = ` 488 <div class="info-panel" style="text-align: center;"> 489 <h2>Grade Range Mastered!</h2> 490 <p>You have successfully answered all available questions for this specific topic and grade level!</p> 491 <button class="btn btn-purple" style="margin-top: 15px;" onclick="BradleyHub.showDifficultyMenu('${groupId}')">Try Another Grade</button> 492 <br><br> 493 <button class="btn" style="background: var(--text-muted); color: white !important;" onclick="BradleyHub.renderMenu()">Return to Main Menu</button> 494 </div>`; 495 return; 496 } 497 498 const prob = pool[Math.floor(Math.random() * pool.length)]; 499 500 const tierDirectory = this.worksheetDirectory[this.state.tier] || {}; 501 const specificLink = tierDirectory[prob.subtopic]; 502 503 let finalLink = specificLink ? specificLink : (groupId === 'all' ? prob.payhip_link : group.link); 504 let finalText = specificLink ? `Need practice? Get the ${prob.subtopic} Worksheet` : (groupId === 'all' ? prob.button_text : group.btnText); 505 506 if (!finalLink || finalLink === "undefined") finalLink = "https://bradleysmaths.co.uk"; 507 if (!finalText || finalText === "undefined") finalText = "Get the Complete Revision Pack"; 508 509 const stage = document.getElementById('hub-stage'); 510 stage.innerHTML = ''; 511 stage.appendChild(this.createInteractiveCard(prob, finalLink, finalText)); 512 if (window.MathJax) MathJax.typesetPromise(); 513 }, 514 515 // --- THE UNIVERSAL CARD GENERATOR --- 516 createProblemCard(prob, isAudit, forcedLink, forcedText) { 517 const card = document.createElement('div'); 518 card.className = 'daily-widget'; 519 if (isAudit) card.style.borderLeft = "8px solid #d9534f"; 520 521 let imgHTML = ''; 522 if (prob.img === "true") { 523 const d = new Date(prob.date); 524 const yyyy = d.getFullYear(); 525 const mm = String(d.getMonth() + 1).padStart(2, '0'); 526 const folderName = `${yyyy}-${mm}`; 527 const dd = String(d.getDate()).padStart(2, '0'); 528 const t = this.state.tier === 'gcse' ? 'g' : 'i'; 529 imgHTML = `<img src="images/${folderName}/${t}_${dd}.png" class="question-img" style="margin: 20px auto; display: block;">`; 530 } 531 532 const link = forcedLink || prob.payhip_link; 533 const bText = forcedText || prob.button_text; 534 535 card.innerHTML = `
536 <span class="widget-header">${prob.date.toUpperCase()} | ${prob.topic} | Grade ${prob.difficulty}</span> 537 <div class="question-box">${prob.q}</div> 538 ${imgHTML} 539 <div id="action-area-${prob.id}"><button class="reveal-btn" onclick="BradleyHub.revealSolution('${prob.id}', ${isAudit})">Show Model Answer</button></div> 540 <div id="sol-${prob.id}" class="step-container" style="display:none;"> 541 <h3 style="text-align:left; color: var(--brand-purple);">Model Solution</h3> 542 ${prob.steps.map(s => `<div class="step" style="display:block;"><span class="step-text">Step</span>${s}</div>`).join('')} 543 <div class="bradley-insight-box insight-${prob.bradley_insight.type}"> 544 <span class="insight-title">The Head Teacher's Eye</span> 545 ${prob.bradley_insight.content} 546 </div> 547 ${!isAudit ? `<div style="display:flex; gap:10px; margin-top:20px;"><button class="btn btn-purple" style="flex:1;" onclick="BradleyHub.serveArena('${this.state.currentGroup}')">Next Question</button><button class="btn" style="flex:1; background: var(--text-muted); color: white !important;" onclick="BradleyHub.renderMenu()">Change Area</button></div>` : ''} 548 <a href="${link}" target="_blank" class="btn-buy" style="display:block; text-align:center; margin-top:20px; background: var(--brand-green); color: white !important;">${bText}</a> 549 </div>`; 550 return card; 551 }, 552 553 extractFinalAnswer(finalStep) { 554 if (!finalStep) return ""; 555 let answerPart = finalStep.split("Final Answer:")[1]; 556 if (!answerPart) return ""; 557 answerPart = answerPart.replace(/<[^>]*>/g, ""); 558 answerPart = answerPart.replace(/\s+/g, " ").trim(); 559 return answerPart; 560 }, 561 562 generateDistractors(correct, prob) { 563 const distractors = []; 564 if (correct.includes("<") || correct.includes(">") || correct.includes("\\le") || correct.includes("\\ge")) { 565 let d1 = correct.replace(/(<|>|\\le|\\ge)/g, match => { 566 if (match === '<') return '>'; 567 if (match === '>') return '<'; 568 if (match === '\\le') return '\\ge'; 569 if (match === '\\ge') return '\\le'; 570 return match; 571 }); 572 let d2 = correct.replace(/<|\\le/g, "="); 573 let d3 = correct.replace(/-?\d+/g, m => (parseInt(m) + 1).toString()); 574 return [...new Set([d1, d2, d3])]; 575 } 576 577 const numRegex = /-?\d+(\.\d+)?/g; 578 const matches = correct.match(numRegex); 579 if (!matches) { 580 return ["Not enough information", "The opposite of the correct answer", "None of the parts are correct"]; 581 } 582 583 const tweakNumber = (valStr, strategy) => { 584 let num = parseFloat(valStr); 585 let isDecimal = valStr.includes('.'); 586 let decPlaces = isDecimal ? valStr.split('.')[1].length : 0; 587 588 let newVal; 589 if (strategy === 'high') newVal = num > 20 ? num * 1.1 : num + (isDecimal ? 0.5 : 2); 590 else if (strategy === 'low') { 591 newVal = num > 20 ? num * 0.9 : num - (isDecimal ? 0.5 : 2); 592 if (newVal < 0 && num > 0) newVal = num / 2; 593 } 594 else if (strategy === 'slip') newVal = num + (isDecimal ? 1.5 : 10); 595 596 return isDecimal ? newVal.toFixed(decPlaces) : Math.round(newVal).toString(); 597 }; 598 599 let d1 = correct.replace(numRegex, m => tweakNumber(m, 'high')); 600 let d2 = correct.replace(numRegex, m => tweakNumber(m, 'low')); 601 let d3 = correct.replace(numRegex, m => tweakNumber(m, 'slip')); 602 603 if (d1 === correct) d1 += " (approx)"; 604 if (d2 === correct || d2 === d1) d2 = "None of the parts match"; 605 if (d3 === correct || d3 === d2) d3 = "Cannot be determined"; 606 607 distractors.push(d1, d2, d3); 608 return [...new Set(distractors)]; 609 }, 610 611 checkAnswer(probId, isCorrect, feedbackHTML) { 612 const feedbackBox = document.getElementById(`feedback-${probId}`); 613 const optionsBox = document.getElementById(`options-${probId}`); 614 if (!feedbackBox) return; 615 616 feedbackBox.style.display = "block"; 617 let justLeveledUp = false;
618 let rankName = ""; 619 620 const getScore = () => this.state.correctIds.filter(id => (this.state.tier === 'gcse' ? id.startsWith('002') : id.startsWith('003'))).length; 621 622 if (isCorrect) { 623 if (!this.state.correctIds.includes(probId)) { 624 this.state.correctIds.push(probId); 625 localStorage.setItem('bradley_correct_ids', JSON.stringify(this.state.correctIds)); 626 627 let newScore = getScore(); 628 if (newScore === 5) { justLeveledUp = true; rankName = "Grade 5 Challenger"; } 629 else if (newScore === 15) { justLeveledUp = true; rankName = "Grade 6 Scholar"; } 630 else if (newScore === 30) { justLeveledUp = true; rankName = "Grade 7-8 Master"; } 631 else if (newScore === 50) { justLeveledUp = true; rankName = "Grade 9 Elite"; } 632 } 633 634 let currentScore = getScore(); 635 let extraMessage = `<br><div style="margin-top: 10px; font-size: 0.95rem; color: #047857;">â <strong>Current Score:</strong> ${currentScore} correct answers!</div>`; 636 if (justLeveledUp) { 637 extraMessage += `<div style="margin-top: 5px; font-size: 1.15rem; color: #059669; font-weight: bold; text-transform: uppercase;">ð LEVEL UP! You are now a ${rankName}! ð</div>`; 638 } 639 640 feedbackBox.style.background = "#d1fae5"; 641 feedbackBox.style.color = "#065f46"; 642 feedbackBox.style.border = "1px solid #34d399"; 643 feedbackBox.innerHTML = feedbackHTML + extraMessage; 644 645 } else { 646 let currentScore = getScore(); 647 let extraMessage = `<br><div style="margin-top: 10px; font-size: 0.95rem; color: #991b1b;">â <strong>Current Score:</strong> ${currentScore} correct answers. Keep going!</div>`; 648 649 feedbackBox.style.background = "#fee2e2"; 650 feedbackBox.style.color = "#991b1b"; 651 feedbackBox.style.border = "1px solid #f87171"; 652 feedbackBox.innerHTML = feedbackHTML + extraMessage; 653 } 654 655 if (optionsBox) optionsBox.style.display = "none"; 656 this.revealSolution(probId, false); 657 }, 658 659 // --- INTERACTIVE CARD GENERATOR --- 660 createInteractiveCard(prob, link, btnText) { 661 const card = document.createElement('div'); 662 card.className = 'daily-widget'; 663 664 const finalStep = prob.steps[prob.steps.length - 1]; 665 const correct = this.extractFinalAnswer(finalStep); 666 667 let optionsObj = []; 668 optionsObj.push({ 669 text: correct, 670 isCorrect: true, 671 feedback: `<strong>Correct!</strong> Outstanding work. Here is the fully worked solution:` 672 }); 673 674 if (prob.wrong_options && prob.wrong_options.length > 0) { 675 prob.wrong_options.forEach(wo => { 676 optionsObj.push({ 677 text: wo.ans, 678 isCorrect: false, 679 feedback: `<strong>Not quite!</strong> ${wo.feedback}` 680 }); 681 }); 682 } else { 683 const distractors = this.generateDistractors(correct, prob); 684 distractors.forEach(d => { 685 optionsObj.push({ 686 text: d, 687 isCorrect: false, 688 feedback: `<strong>Not quite.</strong> Don't worry, review the model answer below to see where you went wrong!` 689 }); 690 }); 691 } 692 693 for (let i = optionsObj.length - 1; i > 0; i--) { 694 const j = Math.floor(Math.random() * (i + 1)); 695 [optionsObj[i], optionsObj[j]] = [optionsObj[j], optionsObj[i]]; 696 } 697 698 optionsObj.push({ 699 text: "None of the above", 700 isCorrect: false, 701 feedback: `<strong>Not quite!</strong> It looks like you need some extra help with this topic. Perhaps you should consider purchasing the worksheet below for targeted help.` 702 }); 703 704 let imgHTML = ''; 705 if (prob.img === "true") { 706 const d = new Date(prob.date); 707 const yyyy = d.getFullYear(); 708 const mm = String(d.getMonth() + 1).padStart(2, '0'); 709 const folderName = `${yyyy}-${mm}`; 710 const dd = String(d.getDate()).padStart(2, '0'); 711 const t = this.state.tier === 'gcse' ? 'g' : 'i'; 712 imgHTML = `<img src="images/${folderName}/${t}_${dd}.png" class="question-img" style="margin: 20px auto; display: block;">`; 713 } 714 715 const tierDirectory = this.worksheetDirectory[this.state.tier] || {}; 716 const specificLink = tierDirectory[prob.subtopic]; 717 718 let finalLink = specificLink ? specificLink : (this.state.currentGroup === 'all' ? prob.payhip_link : link); 719 let finalText = specificLink ? `Need practice? Get the ${prob.subtopic} Worksheet` : (this.state.currentGroup === 'all' ? prob.button_text : btnText); 720 721 if (!finalLink || finalLink === "undefined") finalLink = "https://bradleysmaths.co.uk"; 722 if (!finalText || finalText === "undefined") finalText = "Get the Complete Revision Pack"; 723 724 card.innerHTML = `
725 <span class="widget-header">${prob.date.toUpperCase()} | ${prob.topic} | Grade ${prob.difficulty}</span> 726 <div class="question-box">${prob.q}</div> 727 ${imgHTML} 728 729 <div id="action-area-${prob.id}" style="text-align: center; margin-top: 20px;"> 730 <button class="reveal-btn" onclick="BradleyHub.revealOptions('${prob.id}')">I'm Ready: Reveal Answer Options</button> 731 </div> 732 733 <div id="options-${prob.id}" class="mcq-options" style="display:none; margin-top: 20px;"> 734 <p style="margin-bottom: 15px; font-weight: bold; color: var(--brand-purple); text-align: center;">Select your answer:</p> 735 <div style="display: flex; flex-direction: column; gap: 8px; align-items: center;"> 736 ${optionsObj.map(opt => { 737 const safeFeedback = opt.feedback.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, """); 738 return ` 739 <button class="mcq-btn" style="width: 100%; max-width: 400px; padding: 12px; font-size: 1rem; border: 1px solid var(--brand-purple); border-radius: 6px; background: white; cursor: pointer;" 740 onclick="BradleyHub.checkAnswer('${prob.id}', ${opt.isCorrect}, '${safeFeedback}')"> 741 ${opt.text} 742 </button>` 743 }).join('')} 744 </div> 745 </div> 746 747 <div id="feedback-${prob.id}" class="feedback-box" style="display:none; margin-top: 15px; text-align: center; padding: 10px; border-radius: 5px;"></div> 748 749 <div id="sol-${prob.id}" class="step-container" style="display:none;"> 750 <h3 style="text-align:left; color: var(--brand-purple);">Model Solution</h3> 751 ${prob.steps.map(s => `<div class="step" style="display:block !important; visibility:visible !important; opacity:1 !important; height:auto !important; margin-bottom:12px;"><span class="step-text" style="font-weight:bold; color:var(--brand-purple); margin-right:8px;">Step</span>${s}</div>`).join('')} 752 753 <div class="bradley-insight-box insight-${prob.bradley_insight.type}" style="margin-top: 20px;"> 754 <span class="insight-title">The Head Teacher's Eye</span> 755 ${prob.bradley_insight.content} 756 </div> 757 758 <div style="display:flex; gap:10px; margin-top:20px;"> 759 <button class="btn btn-purple" style="flex:1;" onclick="BradleyHub.serveArena('${this.state.currentGroup}')">Next Question</button> 760 <button class="btn" style="flex:1; background: var(--text-muted); color: white !important;" onclick="BradleyHub.renderMenu()">Change Area</button> 761 </div> 762 763 <a href="${finalLink}" target="_blank" class="btn-buy" style="display:block; text-align:center; margin-top:20px; background: var(--brand-green); color: white !important;">${finalText}</a> 764 </div> 765 `; 766 return card; 767 }, 768 769 revealOptions(id) { 770 const opts = document.getElementById(`options-${id}`); 771 const act = document.getElementById(`action-area-${id}`); 772 if (opts) opts.style.display = 'block'; 773 if (act) act.style.display = 'none'; 774 }, 775 776 revealSolution(id, isAudit) { 777 const sol = document.getElementById(`sol-${id}`); 778 const act = document.getElementById(`action-area-${id}`); 779 if (sol) sol.style.display = 'block'; 780 if (act) act.style.display = 'none'; 781 782 if (!isAudit && !this.state.seenIds.includes(id)) { 783 this.state.seenIds.push(id); 784 localStorage.setItem('bradley_seen_ids', JSON.stringify(this.state.seenIds)); 785 if (!this.state.isPremium) { 786 this.state.freeQuestionsLeft--; 787 localStorage.setItem('bradley_free_left', this.state.freeQuestionsLeft.toString()); 788 } 789 } 790 if (window.MathJax) MathJax.typesetPromise(); 791 }, 792 793 renderAuditList() { 794 const container = document.getElementById('hub-container'); 795 if (!container) return; 796 container.innerHTML = ''; 797 this.state.masterVault.sort((a, b) => new Date(b.date) - new Date(a.date)); 798 this.state.masterVault.forEach(prob => container.appendChild(this.createProblemCard(prob, true))); 799 if (window.MathJax) MathJax.typesetPromise(); 800 }, 801 802 loadSpecificMonth(mm) { 803 const picker = document.getElementById('month-picker'); 804 if (picker && picker.value !== mm) { 805 picker.value = mm; 806 } 807 this.init('audit', this.state.tier); 808 }, 809 810 switchAuditTier(newTier) { 811 this.state.tier = newTier; 812 localStorage.setItem('bradley_tier', newTier); 813 this.init('audit', newTier); 814 } 815 //this comment should force a change in github 816};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.