From 9ac0836bf9cd887fedba36f3dcc56504c1cfd759 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 13:34:32 -0700 Subject: [PATCH 01/20] Uses new PR but fixes bugs in Youtube video links. --- frontend/src/data/subjectVideos.js | 756 +++++++++++++++-------------- 1 file changed, 391 insertions(+), 365 deletions(-) diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index dda23a8..c06e17f 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -1,460 +1,486 @@ -// Curated videos shown by default before the optional YouTube API search runs. -// Paste subject links here as strings, or use objects when you want better labels: -// 'ALGEBRA I': [ -// 'https://www.youtube.com/watch?v=VIDEO_ID', -// { url: 'https://youtu.be/VIDEO_ID', title: 'Linear Equations', channel: 'Khan Academy', categories: ['Linear Equations'] }, -// { url: 'https://youtu.be/VIDEO_ID', title: 'Algebra Review', channel: 'Khan Academy' }, // class-wide fallback -// ], -export const CURATED_SUBJECT_VIDEOS = { - 'PRE-ALGEBRA': [ +export const SUBJECT_VIDEOS = { + "PRE-ALGEBRA": [ { - videoId: 'dAgfnK528RA', - title: 'Order of Operations (PEMDAS)', - channel: 'Khan Academy', - categories: ['Operations and Properties'], + title: "Order of Operations (PEMDAS)", + videoId: "dAgfnK528RA", + channel: "Khan Academy", + topic: "Order of Operations" }, { - url: 'https://www.youtube.com/watch?v=5hG8e9jGeaA', - title: 'Fractions and Basic Equations Review', - channel: 'YouTube', - categories: ['Fractions, Ratios, and Proportions'], + title: "Fractions Explained", + videoId: "n0FZhQ_GkKw", + channel: "Math Antics", + topic: "Fractions" }, { - url: 'https://www.youtube.com/watch?v=SC2WMzopxh8', - title: 'Fractions Practice', - channel: 'YouTube', - categories: ['Fractions, Ratios, and Proportions'], + title: "Area and Perimeter", + videoId: "gtMKsFXjLHw", + channel: "The Organic Chemistry Tutor", + topic: "Area and Perimeter" }, { - url: 'https://www.youtube.com/watch?v=sHTFUo3xRWQ', - title: 'Algebra Basics: Solving Basic Equations Part 2', - channel: 'mathantics', - categories: ['Solving Equations'], + title: "Algebra Basics - Solving Basic Equations", + videoId: "kWOTmyoaWJg", + channel: "The Organic Chemistry Tutor", + topic: "Solving Equations" }, + ], + "ALGEBRA I": [ { - videoId: 'gtMKsFXjLHw', - title: 'Area and Perimeter', - channel: 'The Organic Chemistry Tutor', - categories: ['Area and Perimeter'], + title: "Solving Linear Equations", + videoId: "l3XzepN03KQ", + channel: "Professor Leonard", + topic: "Linear Equations" + }, + { + title: "Solving Algebraic Inequalities", + videoId: "uBxs7cSgOes", + channel: "Professor Dave Explains", + topic: "Inequalities" + }, + { + title: "Adding and Subtracting Integers", + videoId: "jVvvUiExjes", + channel: "The Organic Chemistry Tutor", + topic: "Integer Rules" + }, + { + title: "Converting Between Fractions, Decimals, and Percentages", + videoId: "-Xt4UDk7Kzw", + channel: "Professor Dave Explains", + topic: "Decimals and Percents" + }, + { + title: "Mean, Median, and Mode", + videoId: "B1HEzNTGeZ4", + channel: "mathantics", + topic: "Mean, Median, Mode" + }, + { + title: "How To Solve Quadratic Equations Using the Quadratic Formula", + videoId: "IlNAJl36-10", + channel: "The Organic Chemistry Tutor", + topic: "Quadratic Equations" + }, + { + title: "Polynomials - Adding, Subtracting, Multiplying and Dividing", + videoId: "ZvL9aDGNHqA", + channel: "The Organic Chemistry Tutor", + topic: "Polynomials" + }, + { + title: "Simplifying Exponents With Fractions, Variables, Negative Exponents", + videoId: "Zt2fdy3zrZU", + channel: "The Organic Chemistry Tutor", + topic: "Exponents" + }, + { + title: "Simplifying Radicals With Variables, Exponents, Fractions", + videoId: "Llrngdh3Rrg", + channel: "The Organic Chemistry Tutor", + topic: "Radicals" }, - ], - 'ALGEBRA I': [ - { - url: 'https://www.youtube.com/watch?v=Tx6ZpJ8fv1A', - title: 'Linear Equations - Algebra', - channel: 'The Organic Chemistry Tutor', - categories: ['Linear Equations'], - }, - { - url: 'https://www.youtube.com/watch?v=IWigvJcCAJ0', - title: 'Introduction to the quadratic equation', - channel: 'Khan Academy', - categories: ['Quadratic Equations'], - }, - { - url: 'https://www.youtube.com/watch?v=M4LallQS0GA', - title: 'Solve 2 to the x = 9, what is x?', - channel: 'TabletClass Math', - categories: ['Exponents'], - }, - { videoId: 'uBxs7cSgOes', title: 'Solving Algebraic Inequalities', channel: 'Professor Dave Explains', categories: ['Inequalities'] }, - { videoId: 'jVvvUiExjes', title: 'Adding and Subtracting Integers', channel: 'The Organic Chemistry Tutor', categories: ['Integer Rules'] }, - { videoId: '-Xt4UDk7Kzw', title: 'Fractions, Decimals, and Percentages', channel: 'Professor Dave Explains', categories: ['Decimals and Percents'] }, - { videoId: 'B1HEzNTGeZ4', title: 'Mean, Median, and Mode', channel: 'mathantics', categories: ['Mean, Median, Mode'] }, - { videoId: 'ZvL9aDGNHqA', title: 'Polynomials', channel: 'The Organic Chemistry Tutor', categories: ['Polynomials'] }, - { videoId: 'Llrngdh3Rrg', title: 'Simplifying Radicals', channel: 'The Organic Chemistry Tutor', categories: ['Radicals'] }, - { videoId: '52tpYl2tTqk', title: 'What Are Functions?', channel: 'mathantics', categories: ['Functions'] }, - { videoId: '_cHbhzQVd7Y', title: 'Absolute Value Equations', channel: 'The Organic Chemistry Tutor', categories: ['Absolute Value'] }, - { videoId: '0Gq3uw2p6fA', title: 'Rational Expressions', channel: 'The Organic Chemistry Tutor', categories: ['Rational Expressions'] }, - ], - 'ALGEBRA II': [ - { videoId: 'SP-YJe7Vldo', title: 'Complex Numbers', channel: 'Khan Academy', categories: ['Complex Numbers'] }, { - url: 'https://www.youtube.com/watch?v=SmutsiPnWuc', - title: 'Graphing Logarithmic Functions', - channel: 'The Organic Chemistry Tutor', - categories: ['Logarithms'], + title: "What Are Functions?", + videoId: "52tpYl2tTqk", + channel: "mathantics", + topic: "Functions" }, { - url: 'https://www.youtube.com/watch?v=xt4IMWznDuc', - title: 'Logarithms | Algebra II', - channel: 'Khan Academy', - categories: ['Logarithms'], + title: "How To Solve Absolute Value Equations", + videoId: "_cHbhzQVd7Y", + channel: "The Organic Chemistry Tutor", + topic: "Absolute Value" }, { - url: 'https://www.youtube.com/watch?v=NRB6s77nx2g', - title: 'Domain and Range Functions & Graphs', - channel: 'The Organic Chemistry Tutor', - categories: ['Exponential Functions'], + title: "Rational Expressions - Basic Introduction", + videoId: "0Gq3uw2p6fA", + channel: "The Organic Chemistry Tutor", + topic: "Rational Expressions" }, - { videoId: 's19dWIHficY', title: 'Binomial Theorem Expansion', channel: 'The Organic Chemistry Tutor', categories: ['Polynomial Theorems and Binomial Expansion'] }, - { videoId: 'PLrgwD9TleU', title: 'Conic Sections', channel: 'The Organic Chemistry Tutor', categories: ['Conic Sections'] }, - { videoId: 'Tj89FA-d0f8', title: 'Sequences and Series', channel: "Mario's Math Tutoring", categories: ['Sequences and Series'] }, ], - GEOMETRY: [ + "ALGEBRA II": [ { - url: 'https://www.youtube.com/watch?v=dA94zyaLuhk', - title: 'Types of Angles and Angle Relationships', - channel: 'Professor Dave Explains', - categories: ['Basic Angle Relationships', 'Parallel Lines and Transversals'], + title: "Complex Numbers", + videoId: "SP-YJe7Vldo", + channel: "Khan Academy", + topic: "Complex Numbers" }, { - url: 'https://www.youtube.com/watch?v=YIqZmNYeC5M', - title: 'Circles: radius, diameter, circumference and Pi', - channel: 'Khan Academy', - categories: ['Circles', 'Circle Theorems'], + title: "Logarithms | Algebra II", + videoId: "Z5myJ8dg_rM", + channel: "Khan Academy", + topic: "Logarithms" }, { - url: 'https://www.youtube.com/watch?v=R2J3o9z7n9k', - title: 'Angle Theorems and Circles', - channel: 'YouTube', - categories: ['Basic Angle Relationships', 'Triangles', 'Circles', 'Circle Theorems'], + title: "Graphing Logarithmic Functions", + videoId: "SmutsiPnWuc", + channel: "The Organic Chemistry Tutor", + topic: "Logarithms" }, { - videoId: '302sBERMVwY', - title: 'Geometry Introduction', - channel: 'Professor Leonard', - categories: ['Pythagorean Theorem', 'Similar and Congruent Triangles', 'Quadrilaterals', 'Polygons', 'Coordinate Geometry', 'Surface Area and Volume', 'Transformations'], + title: "Exponential Growth Functions | Algebra II", + videoId: "6WMZ7J0wwMI", + channel: "Khan Academy", + topic: "Exponential Functions" + }, + { + title: "Binomial Theorem Expansion, Pascal's Triangle", + videoId: "s19dWIHficY", + channel: "The Organic Chemistry Tutor", + topic: "Polynomial Theorems and Binomial Expansion" + }, + { + title: "Factor Theorem and Synthetic Division", + videoId: "zAGP46nR6-0", + channel: "The Organic Chemistry Tutor", + topic: "Polynomial Theorems and Binomial Expansion" + }, + { + title: "Conic Sections - Circles, Ellipses, Parabolas, Hyperbola", + videoId: "PLrgwD9TleU", + channel: "The Organic Chemistry Tutor", + topic: "Conic Sections" + }, + { + title: "Sequences and Series (Arithmetic & Geometric) Quick Review", + videoId: "Tj89FA-d0f8", + channel: "Mario's Math Tutoring", + topic: "Sequences and Series" + }, + { + title: "Understanding Matrices and Matrix Notation", + videoId: "y6bVhgmy2rw", + channel: "Professor Dave Explains", + topic: "Matrices" }, ], - TRIGONOMETRY: [ + "GEOMETRY": [ + { + title: "Types of Angles and Angle Relationships", + videoId: "dA94zyaLuhk", + channel: "Professor Dave Explains", + topic: "Angle Relationships" + }, + { + title: "Parallel Lines Cut by a Transversal", + videoId: "3Ex7SpsA9MI", + channel: "Mario's Math Tutoring", + topic: "Parallel Lines and Transversals" + }, + { + title: "Triangles", + videoId: "DdAwGinauoI", + channel: "The Organic Chemistry Tutor", + topic: "Triangles" + }, + { + title: "Pythagorean Theorem", + videoId: "d8EA5TxGzcY", + channel: "The Organic Chemistry Tutor", + topic: "Pythagorean Theorem" + }, { - url: 'https://www.youtube.com/watch?v=FuBZlvOUxYE', - title: 'Trigonometry For Beginners!', - channel: 'The Organic Chemistry Tutor', - categories: ['Special Triangles and Basic Trig Relationships', 'Applications'], + title: "Triangle Congruence Theorems - SSS, SAS, ASA, AAS", + videoId: "jWHOF6cFbpw", + channel: "The Organic Chemistry Tutor", + topic: "Similar and Congruent Triangles" }, { - url: 'https://www.youtube.com/watch?v=qlItePRGLE4', - title: 'All of TRIGONOMETRY in 36 minutes!', - channel: 'JensenMath', - categories: ['Special Triangles and Basic Trig Relationships', 'Fundamental Identities'], + title: "Similar Triangles", + videoId: "YiFwvAFk-xs", + channel: "The Organic Chemistry Tutor", + topic: "Similar and Congruent Triangles" }, { - url: 'https://www.youtube.com/watch?v=PUB0TaZ7bhA', - title: 'Right-Triangle Trig and Identities', - channel: 'YouTube', - categories: ['Fundamental Identities', 'Angle Sum and Multiple-Angle Identities', 'Product and Power Identities', 'Inverse Trig Identities', 'Applications'], + title: "Quadrilaterals - Geometry", + videoId: "ogcH3eM5beM", + channel: "The Organic Chemistry Tutor", + topic: "Quadrilaterals" + }, + { + title: "Polygons", + videoId: "E_-3ulbtcLk", + channel: "The Organic Chemistry Tutor", + topic: "Polygons" + }, + { + title: "Circles In Geometry - Circumference, Area, Arc Length", + videoId: "Fzaof9cX-PM", + channel: "The Organic Chemistry Tutor", + topic: "Circles" + }, + { + title: "Circle Theorems - Inscribed Angles, Intersecting Chords", + videoId: "XckhcRlr4w8", + channel: "Mario's Math Tutoring", + topic: "Circle Theorems" + }, + { + title: "Coordinate Geometry, Basic Introduction", + videoId: "PXnAKcBipKM", + channel: "The Organic Chemistry Tutor", + topic: "Coordinate Geometry" + }, + { + title: "Surface Area and Volume Review", + videoId: "eBAq_caikJ4", + channel: "Mario's Math Tutoring", + topic: "Surface Area and Volume" + }, + { + title: "Introduction to Transformations", + videoId: "XiAoUDfrar0", + channel: "Khan Academy", + topic: "Transformations" }, ], - PRECALCULUS: [ + "TRIGONOMETRY": [ { - url: 'https://www.youtube.com/watch?v=mgMYdo4f0XE', - title: 'Polar Coordinates Basic Introduction', - channel: 'The Organic Chemistry Tutor', - categories: ['Polar & Complex Polar'], + title: "30-60-90 Triangles - Special Right Triangle Trigonometry", + videoId: "yJMGIKCVO-s", + channel: "The Organic Chemistry Tutor", + topic: "Special Triangles and Basic Trig Relationships" }, { - url: 'https://www.youtube.com/watch?v=LlFbHDQVRk4', - title: 'Verifying Trigonometric Identities', - channel: 'The Organic Chemistry Tutor', - categories: ['Functions'], + title: "Trigonometry For Beginners!", + videoId: "FuBZlvOUxYE", + channel: "The Organic Chemistry Tutor", + topic: "Special Triangles and Basic Trig Relationships" }, { - url: 'https://www.youtube.com/watch?v=_svU1SgdHpw', - title: 'Basic Trig Identities Involving Sin, Cos, and Tan', - channel: 'Math and Science', - categories: ['Functions'], + title: "Trig Identities", + videoId: "m1OitPmkydY", + channel: "The Organic Chemistry Tutor", + topic: "Fundamental Identities" + }, + { + title: "Double Angle Identities & Formulas", + videoId: "SE5SBTgrwH8", + channel: "The Organic Chemistry Tutor", + topic: "Angle Sum and Multiple-Angle Identities" + }, + { + title: "Sum/Difference, Double/Half-Angle Formulas", + videoId: "0cB4MLhaCk0", + channel: "Professor Dave Explains", + topic: "Angle Sum and Multiple-Angle Identities" + }, + { + title: "Product To Sum Identities and Sum To Product Formulas", + videoId: "8Prc7VGt40w", + channel: "The Organic Chemistry Tutor", + topic: "Product and Power Identities" + }, + { + title: "Power Reducing Formulas - Trigonometric Identities", + videoId: "56XzcYWUr_8", + channel: "The Organic Chemistry Tutor", + topic: "Product and Power Identities" + }, + { + title: "Evaluating Inverse Trigonometric Functions", + videoId: "jt7p-mCC0ng", + channel: "The Organic Chemistry Tutor", + topic: "Inverse Trig Identities" + }, + { + title: "Trigonometry - Real Life Applications", + videoId: "sCyQ9DcDp2E", + channel: "The Organic Chemistry Tutor", + topic: "Applications" }, - { videoId: 'eI4an8aSsgw', title: 'Full Precalculus Course', channel: 'Professor Leonard', categories: ['Conic Sections', 'Sequences, Series, and Binomial Theorem'] }, ], - 'CALCULUS I': [ + "PRECALCULUS": [ + { + title: "Functions and Graphs | Precalculus", + videoId: "kvU9sOzT2mk", + channel: "The Organic Chemistry Tutor", + topic: "Functions and Graphs" + }, + { + title: "Verifying Trigonometric Identities", + videoId: "LlFbHDQVRk4", + channel: "The Organic Chemistry Tutor", + topic: "Functions and Graphs" + }, { - url: 'https://www.youtube.com/watch?v=n3xBZIvgZhc', - title: 'Calculus Made EASY! Finally Understand It in Minutes!', - channel: 'TabletClass Math', + title: "Conic Sections: Hyperbolas, Ellipses, Parabolas, Circles", + videoId: "b7gJuUN-1GU", + channel: "Mario's Math Tutoring", + topic: "Conic Sections" }, { - url: 'https://www.youtube.com/watch?v=WUvTyaaNkzM', - title: 'The Essence of Calculus', - channel: '3Blue1Brown', - categories: ['Limits', 'Derivative Definitions and Rules', 'Core Theorems of Calculus'], + title: "Arithmetic Sequences and Series - Basic Introduction", + videoId: "XZJdyPkCxuE", + channel: "The Organic Chemistry Tutor", + topic: "Sequences, Series, and Binomial Theorem" }, { - url: 'https://www.youtube.com/watch?v=ZjbDmy7RO6E', - title: 'Optimization Problems - Calculus', - channel: 'The Organic Chemistry Tutor', - categories: ['Derivative Definitions and Rules', 'Common Derivatives', 'Core Theorems of Calculus'], + title: "Binomial Theorem Expansion", + videoId: "s19dWIHficY", + channel: "The Organic Chemistry Tutor", + topic: "Sequences, Series, and Binomial Theorem" + }, + { + title: "Polar Coordinates Basic Introduction", + videoId: "mgMYdo4f0XE", + channel: "The Organic Chemistry Tutor", + topic: "Polar and Complex Polar" + }, + { + title: "Polar Equations to Rectangular Equations", + videoId: "flTz_pSzVFI", + channel: "The Organic Chemistry Tutor", + topic: "Polar and Complex Polar" }, - { videoId: '5yfh5cf4-0w', title: 'Calculus 1 Full Course', channel: 'Professor Leonard', categories: ['Basic Antiderivatives'] }, ], - 'CALCULUS II': [ + "CALCULUS I": [ { - url: 'https://www.youtube.com/watch?v=iLEWXYPZrU8', - title: 'Calculus 2 - Integral Test For Convergence', - channel: 'The Organic Chemistry Tutor', - categories: ['Sequences & Series'], + title: "The Essence of Calculus", + videoId: "WUvTyaaNkzM", + channel: "3Blue1Brown", + topic: "Limits" }, { - url: 'https://www.youtube.com/watch?v=fYyARMqiaag', - title: 'Series and Convergence', - channel: 'YouTube', - categories: ['Sequences & Series', 'Power & Taylor Series'], + title: "Calculus Made EASY! Finally Understand It in Minutes!", + videoId: "n3xBZIvgZhc", + channel: "TabletClass Math", + topic: "Limits" }, { - url: 'https://www.youtube.com/watch?v=8d8wJqk1W0E', - title: 'Series Practice', - channel: 'YouTube', - categories: ['Sequences & Series', 'Power & Taylor Series'], + title: "Optimization Problems - Calculus", + videoId: "ZjbDmy7RO6E", + channel: "The Organic Chemistry Tutor", + topic: "Derivatives" + }, + { + title: "Calculus 1 Full Course", + videoId: "5yfh5cf4-0w", + channel: "Professor Leonard", + topic: "Derivatives" }, - { videoId: 'H9eCT6f_Ftw', title: 'Calculus 2 Full Course', channel: 'Professor Leonard', categories: ['Integration Techniques and Improper Integrals', 'Applications of Integration', 'Parametric & Polar'] }, ], - 'CALCULUS III': [ + "CALCULUS II": [ { - url: 'https://www.youtube.com/watch?v=iVMDEPc2YQw', - title: 'Multivariable Calculus', - channel: 'YouTube', - categories: ['Vector Formulas'], + title: "Calculus 2 - Integral Test For Convergence", + videoId: "iLEWXYPZrU8", + channel: "The Organic Chemistry Tutor", + topic: "Sequences and Series" }, { - url: 'https://www.youtube.com/watch?v=TrcCbdWwCBc', - title: 'Partial Derivatives and Optimization', - channel: 'YouTube', - categories: ['Partial Derivatives and Optimization'], + title: "Calculus 2 Full Course", + videoId: "H9eCT6f_Ftw", + channel: "Professor Leonard", + topic: "Integration Techniques" }, + ], + "CALCULUS III": [ { - url: 'https://www.youtube.com/watch?v=YQH40HgxnKg', - title: 'Multiple Integrals', - channel: 'YouTube', - categories: ['Multiple Integrals'], + title: "Calculus 3 Full Course", + videoId: "tGVnBAHLApA", + channel: "Professor Leonard", + topic: "Vectors" }, ], - 'UNIT CIRCLE': [ + "UNIT CIRCLE": [ { - url: 'https://www.youtube.com/watch?v=2hame37LsH8', title: "Trigonometry Concepts - Don't Memorize! Visualize!", - channel: 'Dennis Davis', - categories: ['UNIT CIRCLE'], + videoId: "2hame37LsH8", + channel: "Dennis Davis", + topic: "Unit Circle" }, { - url: 'https://www.youtube.com/watch?v=bVog_o1Qs80', - title: 'Sine and Cosine - Definition & Meaning', - channel: 'Math and Science', - categories: ['UNIT CIRCLE'], + title: "Unit Circle Explained", + videoId: "1m9p9iubMLU", + channel: "Khan Academy", + topic: "Unit Circle" }, ], - 'PHYSICS I': [ + "PHYSICS I": [ { - url: 'https://www.youtube.com/watch?v=40sww1q5_hc', - title: 'Momentum and Impulse', - channel: 'The Organic Chemistry Tutor', - categories: ['Momentum & Collisions'], + title: "Physics 1 Final Exam Review", + videoId: "b1t41Q3xRM8", + channel: "The Organic Chemistry Tutor", + topic: "Kinematics and Dynamics" }, { - url: 'https://www.youtube.com/watch?v=DxL2HoqLbyA', - title: 'Mechanics Review', - channel: 'YouTube', - categories: ['Kinematics (Motion)', 'Dynamics (Forces)', 'Work, Energy & Power'], + title: "Motion in a Straight Line: Crash Course #1", + videoId: "ZM8ECpBuQYE", + channel: "CrashCourse", + topic: "Kinematics" }, { - url: 'https://www.youtube.com/watch?v=EceJQ05KTf4', - title: 'Impulse and Momentum Practice', - channel: 'YouTube', - categories: ['Momentum & Collisions'], + title: "Momentum and Impulse", + videoId: "40sww1q5_hc", + channel: "The Organic Chemistry Tutor", + topic: "Momentum and Collisions" }, - { videoId: 'b1t41Q3xRM8', title: 'Physics 1 Final Exam Review', channel: 'The Organic Chemistry Tutor', categories: ['Electricity & Waves'] }, ], - 'PHYSICS II': [ + "PHYSICS II": [ { - url: 'https://www.youtube.com/watch?v=U2xGyC-T_io', - title: 'The Biggest Misconception About Electricity', - channel: 'Veritasium', - categories: ['Electrostatics', 'Circuits'], + title: "Physics 2 Final Exam Review", + videoId: "uHvs-G-njo8", + channel: "The Organic Chemistry Tutor", + topic: "E&M and Optics" }, { - url: 'https://www.youtube.com/watch?v=lzF3DJw_GDc', - title: 'Electrostatics', - channel: 'YaleCourses', - categories: ['Electrostatics'], + title: "Electric Fields: Crash Course #26", + videoId: "mdVYqvLAtoQ", + channel: "CrashCourse", + topic: "Electrostatics" }, { - url: 'https://www.youtube.com/watch?v=b9-RpGUSRe8', - title: "Faraday's & Lenz's Law of Electromagnetic Induction", - channel: 'The Organic Chemistry Tutor', - categories: ['Magnetism'], + title: "Faraday's and Lenz's Law of Electromagnetic Induction", + videoId: "b9-RpGUSRe8", + channel: "The Organic Chemistry Tutor", + topic: "Magnetism" }, - { videoId: 'uHvs-G-njo8', title: 'Physics 2 Final Exam Review', channel: 'The Organic Chemistry Tutor', categories: ['Waves & Optics'] }, ], - 'STATISTICS I': [ + "STATISTICS I": [ + { + title: "Statistics Exam 1 Review", + videoId: "xxpc-HPWX28", + channel: "The Organic Chemistry Tutor", + topic: "Descriptive Stats" + }, { - url: 'https://www.youtube.com/watch?v=uzkc-qNVoOk', - title: 'Descriptive Statistics', - channel: 'The Organic Chemistry Tutor', - categories: ['Descriptive Statistics'], + title: "What Is Statistics: Crash Course #1", + videoId: "zouPoc49xbk", + channel: "CrashCourse", + topic: "Intro to Statistics" }, { - url: 'https://www.youtube.com/watch?v=y2G03Lumhe0', - title: 'Probability Distributions', - channel: 'The Organic Chemistry Tutor', - categories: ['Probability', 'Distributions'], + title: "Descriptive Statistics", + videoId: "uzkc-qNVoOk", + channel: "The Organic Chemistry Tutor", + topic: "Descriptive Statistics" }, { - url: 'https://www.youtube.com/watch?v=KZF9IBm9C6E', - title: 'Probability Practice', - channel: 'YouTube', - categories: ['Probability'], + title: "Probability Distributions", + videoId: "y2G03Lumhe0", + channel: "The Organic Chemistry Tutor", + topic: "Probability" }, - { videoId: 'xxpc-HPWX28', title: 'Statistics Exam 1 Review', channel: 'The Organic Chemistry Tutor', categories: ['Inferential Statistics'] }, ], - 'STATISTICS II': [ + "STATISTICS II": [ { - url: 'https://www.youtube.com/watch?v=0oc49DyA3hU', - title: 'Hypothesis Testing', - channel: 'YouTube', - categories: ['Two-Sample Inference', 'Chi-Square Tests', 'ANOVA (Analysis of Variance)'], + title: "Chi-Square Tests: Crash Course #29", + videoId: "Hp21BeaMUv8", + channel: "CrashCourse", + topic: "Chi-Square" }, { - url: 'https://www.youtube.com/watch?v=JQc3yx0-Q9E', - title: 'Inferential Statistics', - channel: 'YouTube', - categories: ['Two-Sample Inference', 'Chi-Square Tests', 'ANOVA (Analysis of Variance)'], + title: "Linear Regression Explained", + videoId: "zITIFTsivN8", + channel: "StatQuest with Josh Starmer", + topic: "Linear Regression" }, { - url: 'https://www.youtube.com/watch?v=PaFPbb66DxQ', - title: 'Regression Analysis', - channel: 'YouTube', - categories: ['Linear Regression'], + title: "Hypothesis Testing", + videoId: "0oc49DyA3hU", + channel: "The Organic Chemistry Tutor", + topic: "Hypothesis Testing" }, ], -}; - -const YOUTUBE_HOSTS = new Set(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be']); -const YOUTUBE_VIDEO_ID_REGEX = /^[a-zA-Z0-9_-]{11}$/; -const MIN_CURATED_VIDEOS_PER_SECTION = 2; - -function isYouTubeHost(hostname = '') { - return YOUTUBE_HOSTS.has(String(hostname).toLowerCase()); -} - -export function getYouTubeVideoId(value = '') { - const text = String(value).trim(); - if (!text) return ''; - - if (YOUTUBE_VIDEO_ID_REGEX.test(text)) { - return text; - } - - try { - const url = new URL(text); - if (!isYouTubeHost(url.hostname)) { - return ''; - } - - if (url.hostname.toLowerCase() === 'youtu.be') { - const videoId = url.pathname.split('/').filter(Boolean)[0] || ''; - return YOUTUBE_VIDEO_ID_REGEX.test(videoId) ? videoId : ''; - } - - if (url.searchParams.has('v')) { - const videoId = url.searchParams.get('v') || ''; - return YOUTUBE_VIDEO_ID_REGEX.test(videoId) ? videoId : ''; - } - - const embedMatch = url.pathname.match(/\/(embed|shorts)\/([a-zA-Z0-9_-]{11})/); - return embedMatch?.[2] || ''; - } catch { - return ''; - } -} - -const normalizeTopic = (value = '') => String(value || '').trim().toLowerCase(); - -const getCategoryTargets = (video = {}) => { - if (Array.isArray(video.categories)) { - return video.categories.filter(Boolean); - } - - return []; -}; - -function normalizeCuratedVideo(entry, className, index, selectedCategory = '') { - const video = typeof entry === 'string' ? { url: entry } : entry; - const videoId = video?.videoId || getYouTubeVideoId(video?.url); - if (!videoId) return null; - - const explicitTopic = video.category || video.topic || ''; - const categoryTargets = getCategoryTargets(video); - let matchRank = 0; - - if (selectedCategory) { - const normalizedSelectedCategory = normalizeTopic(selectedCategory); - const explicitTopicMatches = explicitTopic && normalizeTopic(explicitTopic) === normalizedSelectedCategory; - const categoryTargetMatches = categoryTargets.some((category) => normalizeTopic(category) === normalizedSelectedCategory); - const isClassWideFallback = !explicitTopic && categoryTargets.length === 0; - - if (!explicitTopicMatches && !categoryTargetMatches && !isClassWideFallback) { - return null; - } - - matchRank = isClassWideFallback ? 1 : 0; - } - - const topic = selectedCategory || explicitTopic || categoryTargets[0] || 'Curated pick'; - - return { - className, - category: topic, - topic, - title: video.title || `${className} video ${index + 1}`, - channel: video.channel || 'YouTube', - videoId, - thumbnailUrl: video.thumbnailUrl || '', - source: 'curated', - matchRank, - }; -} - -export function getCuratedVideosForClasses(classNames) { - const uniqueClassNames = [...new Set(classNames)]; - - return uniqueClassNames.flatMap((className) => ( - (CURATED_SUBJECT_VIDEOS[className] || []) - .map((entry, index) => normalizeCuratedVideo(entry, className, index)) - .filter(Boolean) - )); -} - -export function getCuratedVideosForTopics(topics) { - const seenTopics = new Set(); - const seenClassWideVideos = new Set(); - const topicMatches = []; - - topics.forEach(({ className, category }, topicIndex) => { - const topicKey = `${className}:${category}`; - if (seenTopics.has(topicKey)) return; - seenTopics.add(topicKey); - - const videos = (CURATED_SUBJECT_VIDEOS[className] || []) - .map((entry, index) => normalizeCuratedVideo(entry, className, index, category)) - .filter(Boolean) - .sort((a, b) => a.matchRank - b.matchRank); - - topicMatches.push({ - topicIndex, - className, - sectionSpecificVideos: videos.filter((video) => video.matchRank === 0), - fallbackVideos: videos.filter((video) => video.matchRank === 1), - selectedFallbackVideos: [], - }); - }); - - [...topicMatches] - .filter(({ sectionSpecificVideos }) => sectionSpecificVideos.length < MIN_CURATED_VIDEOS_PER_SECTION) - .sort((left, right) => left.sectionSpecificVideos.length - right.sectionSpecificVideos.length || left.topicIndex - right.topicIndex) - .forEach((match) => { - const neededFallbackCount = MIN_CURATED_VIDEOS_PER_SECTION - match.sectionSpecificVideos.length; - match.selectedFallbackVideos = match.fallbackVideos.filter((video) => { - const fallbackKey = `${match.className}:${video.videoId}`; - if (seenClassWideVideos.has(fallbackKey)) return false; - seenClassWideVideos.add(fallbackKey); - return true; - }).slice(0, neededFallbackCount); - }); - - return topicMatches - .sort((left, right) => left.topicIndex - right.topicIndex) - .flatMap(({ sectionSpecificVideos, selectedFallbackVideos }) => [ - ...sectionSpecificVideos, - ...selectedFallbackVideos, - ]); -} +}; \ No newline at end of file From ea0f79378e73b47c9728435d36f06a7c8ba0b7a2 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 13:55:58 -0700 Subject: [PATCH 02/20] white screen fix caused by missing getCuratedVideosForTopics export in subjectVideos.js --- frontend/src/data/subjectVideos.js | 90 +++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index c06e17f..e1ec7d4 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -483,4 +483,92 @@ export const SUBJECT_VIDEOS = { topic: "Hypothesis Testing" }, ], -}; \ No newline at end of file +}; + +const MIN_CURATED_VIDEOS_PER_SECTION = 2; + +function normalizeCuratedVideo(entry, className, index, selectedCategory = '') { + const videoId = entry.videoId; + if (!videoId) return null; + + const topic = entry.topic || ''; + const isClassWideFallback = !topic; + + if (selectedCategory) { + const normalizedSelected = selectedCategory.toLowerCase().trim(); + const normalizedTopic = topic.toLowerCase().trim(); + const topicMatches = normalizedTopic === normalizedSelected; + + if (!topicMatches && !isClassWideFallback) return null; + } + + return { + className, + category: selectedCategory || topic || 'General', + topic: selectedCategory || topic || 'General', + title: entry.title || `${className} video ${index + 1}`, + channel: entry.channel || 'YouTube', + videoId, + thumbnailUrl: '', + source: 'curated', + matchRank: isClassWideFallback ? 1 : 0, + }; +} + +export function getCuratedVideosForClasses(classNames) { + return [...new Set(classNames)].flatMap((className) => + (SUBJECT_VIDEOS[className] || []) + .map((entry, index) => normalizeCuratedVideo(entry, className, index)) + .filter(Boolean) + ); +} + + +export function getCuratedVideosForTopics(topics) { + if (!topics || !topics.length) return []; + + const seenTopics = new Set(); + const seenClassWideVideos = new Set(); + const topicMatches = []; + + topics.forEach(({ className, category }, topicIndex) => { + const topicKey = `${className}:${category}`; + if (seenTopics.has(topicKey)) return; + seenTopics.add(topicKey); + + const videos = (SUBJECT_VIDEOS[className] || []) + .map((entry, index) => normalizeCuratedVideo(entry, className, index, category)) + .filter(Boolean) + .sort((a, b) => a.matchRank - b.matchRank); + + topicMatches.push({ + topicIndex, + className, + sectionSpecificVideos: videos.filter((v) => v.matchRank === 0), + fallbackVideos: videos.filter((v) => v.matchRank === 1), + selectedFallbackVideos: [], + }); + }); + + [...topicMatches] + .filter(({ sectionSpecificVideos }) => sectionSpecificVideos.length < MIN_CURATED_VIDEOS_PER_SECTION) + .sort((a, b) => a.sectionSpecificVideos.length - b.sectionSpecificVideos.length || a.topicIndex - b.topicIndex) + .forEach((match) => { + const needed = MIN_CURATED_VIDEOS_PER_SECTION - match.sectionSpecificVideos.length; + match.selectedFallbackVideos = match.fallbackVideos + .filter((video) => { + const key = `${match.className}:${video.videoId}`; + if (seenClassWideVideos.has(key)) return false; + seenClassWideVideos.add(key); + return true; + }) + .slice(0, needed); + }); + + return topicMatches + .sort((a, b) => a.topicIndex - b.topicIndex) + .flatMap(({ sectionSpecificVideos, selectedFallbackVideos }) => [ + ...sectionSpecificVideos, + ...selectedFallbackVideos, + ]); +} From 4e7de008d2f99c2aee8ad9be13a6b10961a2c183 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 14:16:06 -0700 Subject: [PATCH 03/20] fixed bug with pre-algebra videos not appearing for fractions and order of operations. --- frontend/src/data/subjectVideos.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index e1ec7d4..1e897ec 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -4,13 +4,13 @@ export const SUBJECT_VIDEOS = { title: "Order of Operations (PEMDAS)", videoId: "dAgfnK528RA", channel: "Khan Academy", - topic: "Order of Operations" + topic: "Operations and Properties" }, { title: "Fractions Explained", videoId: "n0FZhQ_GkKw", channel: "Math Antics", - topic: "Fractions" + topic: "Fractions, Ratios, and Proportions" }, { title: "Area and Perimeter", From a8591ea289160201745202d9b1d8d635ee6ee8fe Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 14:27:45 -0700 Subject: [PATCH 04/20] Added complete selection of videos for each topic in Statistics II. --- frontend/src/data/subjectVideos.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index 1e897ec..05aef4d 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -465,10 +465,10 @@ export const SUBJECT_VIDEOS = { ], "STATISTICS II": [ { - title: "Chi-Square Tests: Crash Course #29", - videoId: "Hp21BeaMUv8", - channel: "CrashCourse", - topic: "Chi-Square" + title: "Chi Square Test", + videoId: "HKDqlYSLt68", + channel: "The Organic Chemistry Tutor", + topic: "Chi-Square Tests" }, { title: "Linear Regression Explained", @@ -482,6 +482,19 @@ export const SUBJECT_VIDEOS = { channel: "The Organic Chemistry Tutor", topic: "Hypothesis Testing" }, + { + title: "ANOVA: Crash Course Statistics #33", + videoId: "oOuu8IBd-yo", + channel: "CrashCourse", + topic: "ANOVA (Analysis of Variance)" + }, + { + title: "Test Statistics: Crash Course Statistics #26", + videoId: "QZ7kgmhdIwA", + channel: "CrashCourse", + topic: "Two-Sample Inference" + + }, ], }; From 2447bc5299959cbdbf7d4d85397541b9ec3931a5 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 14:31:09 -0700 Subject: [PATCH 05/20] Changed button names to better reflect the functionality. --- frontend/src/components/CreateCheatSheet.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx index 1d51c53..03cfd6a 100644 --- a/frontend/src/components/CreateCheatSheet.jsx +++ b/frontend/src/components/CreateCheatSheet.jsx @@ -683,7 +683,7 @@ const LatexEditor = ({ content, onChange, isModified, compileError }) => { value={content} onChange={(e) => onChange(e.target.value)} onScroll={handleScroll} - placeholder='Select classes and categories above, then click "Compile PDF" to see the LaTeX code here.' + placeholder='Select classes and categories above, then click "GET CHEAT SHEET" to see the LaTeX code here.' className={`textarea-field ${isModified ? 'modified' : ''}`} rows={15} spellCheck="false" @@ -1410,7 +1410,7 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS className="btn-compile" disabled={isCompiling} > - {isCompiling ? 'Compiling…' : 'Compile PDF'} + {isCompiling ? 'Compiling…' : 'GET CHEAT SHEET'}
From 68e523de9fed66af9079d3c96188c16425395214 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 17:18:16 -0700 Subject: [PATCH 06/20] Added new Galaxy theme as a fun addition. --- frontend/src/App.css | 27 +++++++++++++++++++++++++++ frontend/src/App.jsx | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 18e0f3d..2e4610d 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -236,6 +236,33 @@ [data-theme="neon"] .class-btn.active { box-shadow: 0 0 10px rgba(0,245,255,0.5); } + /* Galaxy Theme */ +[data-theme="galaxy"] { + --bg: #0a0614; + --text: #e8e0f5; + --text-muted: #8b7aa8; + --panel-bg: #110d1f; + --border: #2d1f4a; + --border-subtle: #1a1230; + --primary: #7c3aed; + --primary-hover: #6d28d9; + --card-bg: #110d1f; + --box-bg: #0d0a1a; + --input-bg: #080512; + --input-border: #3d2566; + --input-text: #e8e0f5; + --btn-primary: #7c3aed; + --btn-primary-hover: #6d28d9; + --btn-download: #5b21b6; + --btn-download-hover: #4c1d95; + --btn-clear: #db2777; + --btn-clear-hover: #be185d; + --shadow-sm: 0 1px 2px rgba(0,0,0,0.5); + --shadow-md: 0 4px 12px rgba(124,58,237,0.25); + --shadow-lg: 0 8px 24px rgba(124,58,237,0.3); + --shadow-inset: inset 0 1px 3px rgba(0,0,0,0.6); +} + /* ========================================================================== BASE STYLES ========================================================================== */ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0b249c0..d3e32e2 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -146,7 +146,8 @@ const THEMES = [ { id: 'miami', label: '🌴 Miami 🐬' }, { id: 'forest', label: '🌲 Forest' }, { id: 'coolGrey', label: '❄️ Cool Grey'}, - { id: 'neon', label: '🩵 neon'} + { id: 'neon', label: '🩵 neon'}, + { id: 'galaxy', label: '🌌 Galaxy' }, ]; function App() { From 86d939f9244d8342c861dc7f3c6f5572322cc657 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 17:25:13 -0700 Subject: [PATCH 07/20] Added videos for Physics, Statistics II, and fixed bugs with Pre-alegbra videos not appearing correctly. --- frontend/src/data/subjectVideos.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index 05aef4d..10beb3f 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -163,10 +163,17 @@ export const SUBJECT_VIDEOS = { topic: "Angle Relationships" }, { +<<<<<<< HEAD title: "Parallel Lines Cut by a Transversal", videoId: "3Ex7SpsA9MI", channel: "Mario's Math Tutoring", topic: "Parallel Lines and Transversals" +======= + title: "Parallel Lines Cut by a Transversal - Finding Angle Measures", + videoId: "3Ex7SpsA9MI", + channel: "Mario's Math Tutoring", + topic: "Parallel lines and Traversals" +>>>>>>> 8eb94e0 (Added video for triangles and parallel lines/traversals for geometry.) }, { title: "Triangles", @@ -174,6 +181,7 @@ export const SUBJECT_VIDEOS = { channel: "The Organic Chemistry Tutor", topic: "Triangles" }, +<<<<<<< HEAD { title: "Pythagorean Theorem", videoId: "d8EA5TxGzcY", @@ -234,6 +242,8 @@ export const SUBJECT_VIDEOS = { channel: "Khan Academy", topic: "Transformations" }, +======= +>>>>>>> 8eb94e0 (Added video for triangles and parallel lines/traversals for geometry.) ], "TRIGONOMETRY": [ { From 706db019c4769b2f6e573ffee06d0d01a5d99b14 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 17:26:52 -0700 Subject: [PATCH 08/20] fixed 'white-screen' issue caused by missing getCuratedVideosForTopics export inside subjetcVideos.js --- frontend/src/data/subjectVideos.js | 1172 ++++++++++++++-------------- 1 file changed, 589 insertions(+), 583 deletions(-) diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index 10beb3f..a413b54 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -1,597 +1,603 @@ export const SUBJECT_VIDEOS = { - "PRE-ALGEBRA": [ - { - title: "Order of Operations (PEMDAS)", - videoId: "dAgfnK528RA", - channel: "Khan Academy", - topic: "Operations and Properties" - }, - { - title: "Fractions Explained", - videoId: "n0FZhQ_GkKw", - channel: "Math Antics", - topic: "Fractions, Ratios, and Proportions" - }, - { - title: "Area and Perimeter", - videoId: "gtMKsFXjLHw", - channel: "The Organic Chemistry Tutor", - topic: "Area and Perimeter" - }, - { - title: "Algebra Basics - Solving Basic Equations", - videoId: "kWOTmyoaWJg", - channel: "The Organic Chemistry Tutor", - topic: "Solving Equations" - }, - ], - "ALGEBRA I": [ - { - title: "Solving Linear Equations", - videoId: "l3XzepN03KQ", - channel: "Professor Leonard", - topic: "Linear Equations" - }, - { - title: "Solving Algebraic Inequalities", - videoId: "uBxs7cSgOes", - channel: "Professor Dave Explains", - topic: "Inequalities" - }, - { - title: "Adding and Subtracting Integers", - videoId: "jVvvUiExjes", - channel: "The Organic Chemistry Tutor", - topic: "Integer Rules" - }, - { - title: "Converting Between Fractions, Decimals, and Percentages", - videoId: "-Xt4UDk7Kzw", - channel: "Professor Dave Explains", - topic: "Decimals and Percents" - }, - { - title: "Mean, Median, and Mode", - videoId: "B1HEzNTGeZ4", - channel: "mathantics", - topic: "Mean, Median, Mode" - }, - { - title: "How To Solve Quadratic Equations Using the Quadratic Formula", - videoId: "IlNAJl36-10", - channel: "The Organic Chemistry Tutor", - topic: "Quadratic Equations" - }, - { - title: "Polynomials - Adding, Subtracting, Multiplying and Dividing", - videoId: "ZvL9aDGNHqA", - channel: "The Organic Chemistry Tutor", - topic: "Polynomials" - }, - { - title: "Simplifying Exponents With Fractions, Variables, Negative Exponents", - videoId: "Zt2fdy3zrZU", - channel: "The Organic Chemistry Tutor", - topic: "Exponents" - }, - { - title: "Simplifying Radicals With Variables, Exponents, Fractions", - videoId: "Llrngdh3Rrg", - channel: "The Organic Chemistry Tutor", - topic: "Radicals" - }, - { - title: "What Are Functions?", - videoId: "52tpYl2tTqk", - channel: "mathantics", - topic: "Functions" - }, - { - title: "How To Solve Absolute Value Equations", - videoId: "_cHbhzQVd7Y", - channel: "The Organic Chemistry Tutor", - topic: "Absolute Value" - }, - { - title: "Rational Expressions - Basic Introduction", - videoId: "0Gq3uw2p6fA", - channel: "The Organic Chemistry Tutor", - topic: "Rational Expressions" - }, - ], - "ALGEBRA II": [ - { - title: "Complex Numbers", - videoId: "SP-YJe7Vldo", - channel: "Khan Academy", - topic: "Complex Numbers" - }, - { - title: "Logarithms | Algebra II", - videoId: "Z5myJ8dg_rM", - channel: "Khan Academy", - topic: "Logarithms" - }, - { - title: "Graphing Logarithmic Functions", - videoId: "SmutsiPnWuc", - channel: "The Organic Chemistry Tutor", - topic: "Logarithms" - }, - { - title: "Exponential Growth Functions | Algebra II", - videoId: "6WMZ7J0wwMI", - channel: "Khan Academy", - topic: "Exponential Functions" - }, - { - title: "Binomial Theorem Expansion, Pascal's Triangle", - videoId: "s19dWIHficY", - channel: "The Organic Chemistry Tutor", - topic: "Polynomial Theorems and Binomial Expansion" - }, - { - title: "Factor Theorem and Synthetic Division", - videoId: "zAGP46nR6-0", - channel: "The Organic Chemistry Tutor", - topic: "Polynomial Theorems and Binomial Expansion" - }, - { - title: "Conic Sections - Circles, Ellipses, Parabolas, Hyperbola", - videoId: "PLrgwD9TleU", - channel: "The Organic Chemistry Tutor", - topic: "Conic Sections" - }, - { - title: "Sequences and Series (Arithmetic & Geometric) Quick Review", - videoId: "Tj89FA-d0f8", - channel: "Mario's Math Tutoring", - topic: "Sequences and Series" - }, - { - title: "Understanding Matrices and Matrix Notation", - videoId: "y6bVhgmy2rw", - channel: "Professor Dave Explains", - topic: "Matrices" - }, - ], - "GEOMETRY": [ - { - title: "Types of Angles and Angle Relationships", - videoId: "dA94zyaLuhk", - channel: "Professor Dave Explains", - topic: "Angle Relationships" - }, - { -<<<<<<< HEAD - title: "Parallel Lines Cut by a Transversal", - videoId: "3Ex7SpsA9MI", - channel: "Mario's Math Tutoring", - topic: "Parallel Lines and Transversals" -======= - title: "Parallel Lines Cut by a Transversal - Finding Angle Measures", - videoId: "3Ex7SpsA9MI", - channel: "Mario's Math Tutoring", - topic: "Parallel lines and Traversals" ->>>>>>> 8eb94e0 (Added video for triangles and parallel lines/traversals for geometry.) - }, - { - title: "Triangles", - videoId: "DdAwGinauoI", - channel: "The Organic Chemistry Tutor", - topic: "Triangles" - }, -<<<<<<< HEAD - { - title: "Pythagorean Theorem", - videoId: "d8EA5TxGzcY", - channel: "The Organic Chemistry Tutor", - topic: "Pythagorean Theorem" - }, - { - title: "Triangle Congruence Theorems - SSS, SAS, ASA, AAS", - videoId: "jWHOF6cFbpw", - channel: "The Organic Chemistry Tutor", - topic: "Similar and Congruent Triangles" - }, - { - title: "Similar Triangles", - videoId: "YiFwvAFk-xs", - channel: "The Organic Chemistry Tutor", - topic: "Similar and Congruent Triangles" - }, - { - title: "Quadrilaterals - Geometry", - videoId: "ogcH3eM5beM", - channel: "The Organic Chemistry Tutor", - topic: "Quadrilaterals" - }, - { - title: "Polygons", - videoId: "E_-3ulbtcLk", - channel: "The Organic Chemistry Tutor", - topic: "Polygons" - }, - { - title: "Circles In Geometry - Circumference, Area, Arc Length", - videoId: "Fzaof9cX-PM", - channel: "The Organic Chemistry Tutor", - topic: "Circles" - }, - { - title: "Circle Theorems - Inscribed Angles, Intersecting Chords", - videoId: "XckhcRlr4w8", - channel: "Mario's Math Tutoring", - topic: "Circle Theorems" - }, - { - title: "Coordinate Geometry, Basic Introduction", - videoId: "PXnAKcBipKM", - channel: "The Organic Chemistry Tutor", - topic: "Coordinate Geometry" - }, - { - title: "Surface Area and Volume Review", - videoId: "eBAq_caikJ4", - channel: "Mario's Math Tutoring", - topic: "Surface Area and Volume" - }, - { - title: "Introduction to Transformations", - videoId: "XiAoUDfrar0", - channel: "Khan Academy", - topic: "Transformations" - }, -======= ->>>>>>> 8eb94e0 (Added video for triangles and parallel lines/traversals for geometry.) - ], - "TRIGONOMETRY": [ - { - title: "30-60-90 Triangles - Special Right Triangle Trigonometry", - videoId: "yJMGIKCVO-s", - channel: "The Organic Chemistry Tutor", - topic: "Special Triangles and Basic Trig Relationships" - }, - { - title: "Trigonometry For Beginners!", - videoId: "FuBZlvOUxYE", - channel: "The Organic Chemistry Tutor", - topic: "Special Triangles and Basic Trig Relationships" - }, - { - title: "Trig Identities", - videoId: "m1OitPmkydY", - channel: "The Organic Chemistry Tutor", - topic: "Fundamental Identities" - }, - { - title: "Double Angle Identities & Formulas", - videoId: "SE5SBTgrwH8", - channel: "The Organic Chemistry Tutor", - topic: "Angle Sum and Multiple-Angle Identities" - }, - { - title: "Sum/Difference, Double/Half-Angle Formulas", - videoId: "0cB4MLhaCk0", - channel: "Professor Dave Explains", - topic: "Angle Sum and Multiple-Angle Identities" - }, - { - title: "Product To Sum Identities and Sum To Product Formulas", - videoId: "8Prc7VGt40w", - channel: "The Organic Chemistry Tutor", - topic: "Product and Power Identities" - }, - { - title: "Power Reducing Formulas - Trigonometric Identities", - videoId: "56XzcYWUr_8", - channel: "The Organic Chemistry Tutor", - topic: "Product and Power Identities" - }, - { - title: "Evaluating Inverse Trigonometric Functions", - videoId: "jt7p-mCC0ng", - channel: "The Organic Chemistry Tutor", - topic: "Inverse Trig Identities" - }, - { - title: "Trigonometry - Real Life Applications", - videoId: "sCyQ9DcDp2E", - channel: "The Organic Chemistry Tutor", - topic: "Applications" - }, - ], - "PRECALCULUS": [ - { - title: "Functions and Graphs | Precalculus", - videoId: "kvU9sOzT2mk", - channel: "The Organic Chemistry Tutor", - topic: "Functions and Graphs" - }, - { - title: "Verifying Trigonometric Identities", - videoId: "LlFbHDQVRk4", - channel: "The Organic Chemistry Tutor", - topic: "Functions and Graphs" - }, - { - title: "Conic Sections: Hyperbolas, Ellipses, Parabolas, Circles", - videoId: "b7gJuUN-1GU", - channel: "Mario's Math Tutoring", - topic: "Conic Sections" - }, - { - title: "Arithmetic Sequences and Series - Basic Introduction", - videoId: "XZJdyPkCxuE", - channel: "The Organic Chemistry Tutor", - topic: "Sequences, Series, and Binomial Theorem" - }, - { - title: "Binomial Theorem Expansion", - videoId: "s19dWIHficY", - channel: "The Organic Chemistry Tutor", - topic: "Sequences, Series, and Binomial Theorem" - }, - { - title: "Polar Coordinates Basic Introduction", - videoId: "mgMYdo4f0XE", - channel: "The Organic Chemistry Tutor", - topic: "Polar and Complex Polar" - }, - { - title: "Polar Equations to Rectangular Equations", - videoId: "flTz_pSzVFI", - channel: "The Organic Chemistry Tutor", - topic: "Polar and Complex Polar" - }, - ], - "CALCULUS I": [ - { - title: "The Essence of Calculus", - videoId: "WUvTyaaNkzM", - channel: "3Blue1Brown", - topic: "Limits" - }, - { - title: "Calculus Made EASY! Finally Understand It in Minutes!", - videoId: "n3xBZIvgZhc", - channel: "TabletClass Math", - topic: "Limits" - }, - { - title: "Optimization Problems - Calculus", - videoId: "ZjbDmy7RO6E", - channel: "The Organic Chemistry Tutor", - topic: "Derivatives" - }, - { - title: "Calculus 1 Full Course", - videoId: "5yfh5cf4-0w", - channel: "Professor Leonard", - topic: "Derivatives" - }, - ], - "CALCULUS II": [ - { - title: "Calculus 2 - Integral Test For Convergence", - videoId: "iLEWXYPZrU8", - channel: "The Organic Chemistry Tutor", - topic: "Sequences and Series" - }, - { - title: "Calculus 2 Full Course", - videoId: "H9eCT6f_Ftw", - channel: "Professor Leonard", - topic: "Integration Techniques" - }, - ], - "CALCULUS III": [ - { - title: "Calculus 3 Full Course", - videoId: "tGVnBAHLApA", - channel: "Professor Leonard", - topic: "Vectors" - }, - ], - "UNIT CIRCLE": [ - { - title: "Trigonometry Concepts - Don't Memorize! Visualize!", - videoId: "2hame37LsH8", - channel: "Dennis Davis", - topic: "Unit Circle" - }, - { - title: "Unit Circle Explained", - videoId: "1m9p9iubMLU", - channel: "Khan Academy", - topic: "Unit Circle" - }, - ], - "PHYSICS I": [ - { - title: "Physics 1 Final Exam Review", - videoId: "b1t41Q3xRM8", - channel: "The Organic Chemistry Tutor", - topic: "Kinematics and Dynamics" - }, - { - title: "Motion in a Straight Line: Crash Course #1", - videoId: "ZM8ECpBuQYE", - channel: "CrashCourse", - topic: "Kinematics" - }, - { - title: "Momentum and Impulse", - videoId: "40sww1q5_hc", - channel: "The Organic Chemistry Tutor", - topic: "Momentum and Collisions" - }, - ], - "PHYSICS II": [ - { - title: "Physics 2 Final Exam Review", - videoId: "uHvs-G-njo8", - channel: "The Organic Chemistry Tutor", - topic: "E&M and Optics" - }, - { - title: "Electric Fields: Crash Course #26", - videoId: "mdVYqvLAtoQ", - channel: "CrashCourse", - topic: "Electrostatics" - }, - { - title: "Faraday's and Lenz's Law of Electromagnetic Induction", - videoId: "b9-RpGUSRe8", - channel: "The Organic Chemistry Tutor", - topic: "Magnetism" - }, - ], - "STATISTICS I": [ - { - title: "Statistics Exam 1 Review", - videoId: "xxpc-HPWX28", - channel: "The Organic Chemistry Tutor", - topic: "Descriptive Stats" - }, - { - title: "What Is Statistics: Crash Course #1", - videoId: "zouPoc49xbk", - channel: "CrashCourse", - topic: "Intro to Statistics" - }, - { - title: "Descriptive Statistics", - videoId: "uzkc-qNVoOk", - channel: "The Organic Chemistry Tutor", - topic: "Descriptive Statistics" - }, - { - title: "Probability Distributions", - videoId: "y2G03Lumhe0", - channel: "The Organic Chemistry Tutor", - topic: "Probability" - }, - ], - "STATISTICS II": [ - { - title: "Chi Square Test", - videoId: "HKDqlYSLt68", - channel: "The Organic Chemistry Tutor", - topic: "Chi-Square Tests" - }, - { - title: "Linear Regression Explained", - videoId: "zITIFTsivN8", - channel: "StatQuest with Josh Starmer", - topic: "Linear Regression" - }, - { - title: "Hypothesis Testing", - videoId: "0oc49DyA3hU", - channel: "The Organic Chemistry Tutor", - topic: "Hypothesis Testing" - }, - { - title: "ANOVA: Crash Course Statistics #33", - videoId: "oOuu8IBd-yo", - channel: "CrashCourse", - topic: "ANOVA (Analysis of Variance)" - }, - { - title: "Test Statistics: Crash Course Statistics #26", - videoId: "QZ7kgmhdIwA", - channel: "CrashCourse", - topic: "Two-Sample Inference" - - }, - ], + "PRE-ALGEBRA": [ + { + title: "Order of Operations (PEMDAS)", + videoId: "dAgfnK528RA", + channel: "Khan Academy", + topic: "Operations and Properties" + }, + { + title: "Fractions Explained", + videoId: "n0FZhQ_GkKw", + channel: "Math Antics", + topic: "Fractions, Ratios, and Proportions" + }, + { + title: "Area and Perimeter", + videoId: "gtMKsFXjLHw", + channel: "The Organic Chemistry Tutor", + topic: "Area and Perimeter" + }, + { + title: "Algebra Basics - Solving Basic Equations", + videoId: "kWOTmyoaWJg", + channel: "The Organic Chemistry Tutor", + topic: "Solving Equations" + }, + ], + "ALGEBRA I": [ + { + title: "Solving Linear Equations", + videoId: "l3XzepN03KQ", + channel: "Professor Leonard", + topic: "Linear Equations" + }, + { + title: "Solving Algebraic Inequalities", + videoId: "uBxs7cSgOes", + channel: "Professor Dave Explains", + topic: "Inequalities" + }, + { + title: "Adding and Subtracting Integers", + videoId: "jVvvUiExjes", + channel: "The Organic Chemistry Tutor", + topic: "Integer Rules" + }, + { + title: "Converting Between Fractions, Decimals, and Percentages", + videoId: "-Xt4UDk7Kzw", + channel: "Professor Dave Explains", + topic: "Decimals and Percents" + }, + { + title: "Mean, Median, and Mode", + videoId: "B1HEzNTGeZ4", + channel: "mathantics", + topic: "Mean, Median, Mode" + }, + { + title: "How To Solve Quadratic Equations Using the Quadratic Formula", + videoId: "IlNAJl36-10", + channel: "The Organic Chemistry Tutor", + topic: "Quadratic Equations" + }, + { + title: "Polynomials - Adding, Subtracting, Multiplying and Dividing", + videoId: "ZvL9aDGNHqA", + channel: "The Organic Chemistry Tutor", + topic: "Polynomials" + }, + { + title: "Simplifying Exponents With Fractions, Variables, Negative Exponents", + videoId: "Zt2fdy3zrZU", + channel: "The Organic Chemistry Tutor", + topic: "Exponents" + }, + { + title: "Simplifying Radicals With Variables, Exponents, Fractions", + videoId: "Llrngdh3Rrg", + channel: "The Organic Chemistry Tutor", + topic: "Radicals" + }, + { + title: "What Are Functions?", + videoId: "52tpYl2tTqk", + channel: "mathantics", + topic: "Functions" + }, + { + title: "How To Solve Absolute Value Equations", + videoId: "_cHbhzQVd7Y", + channel: "The Organic Chemistry Tutor", + topic: "Absolute Value" + }, + { + title: "Rational Expressions - Basic Introduction", + videoId: "0Gq3uw2p6fA", + channel: "The Organic Chemistry Tutor", + topic: "Rational Expressions" + }, + ], + "ALGEBRA II": [ + { + title: "Complex Numbers", + videoId: "SP-YJe7Vldo", + channel: "Khan Academy", + topic: "Complex Numbers" + }, + { + title: "Logarithms | Algebra II", + videoId: "Z5myJ8dg_rM", + channel: "Khan Academy", + topic: "Logarithms" + }, + { + title: "Graphing Logarithmic Functions", + videoId: "SmutsiPnWuc", + channel: "The Organic Chemistry Tutor", + topic: "Logarithms" + }, + { + title: "Exponential Growth Functions | Algebra II", + videoId: "6WMZ7J0wwMI", + channel: "Khan Academy", + topic: "Exponential Functions" + }, + { + title: "Binomial Theorem Expansion, Pascal's Triangle", + videoId: "s19dWIHficY", + channel: "The Organic Chemistry Tutor", + topic: "Polynomial Theorems and Binomial Expansion" + }, + { + title: "Factor Theorem and Synthetic Division", + videoId: "zAGP46nR6-0", + channel: "The Organic Chemistry Tutor", + topic: "Polynomial Theorems and Binomial Expansion" + }, + { + title: "Conic Sections - Circles, Ellipses, Parabolas, Hyperbola", + videoId: "PLrgwD9TleU", + channel: "The Organic Chemistry Tutor", + topic: "Conic Sections" + }, + { + title: "Sequences and Series (Arithmetic & Geometric) Quick Review", + videoId: "Tj89FA-d0f8", + channel: "Mario's Math Tutoring", + topic: "Sequences and Series" + }, + { + title: "Understanding Matrices and Matrix Notation", + videoId: "y6bVhgmy2rw", + channel: "Professor Dave Explains", + topic: "Matrices" + }, + ], + "GEOMETRY": [ + { + title: "Types of Angles and Angle Relationships", + videoId: "dA94zyaLuhk", + channel: "Professor Dave Explains", + topic: "Angle Relationships" + }, + { + title: "Parallel Lines Cut by a Transversal", + videoId: "3Ex7SpsA9MI", + channel: "Mario's Math Tutoring", + topic: "Parallel Lines and Transversals" + }, + { + title: "Triangles", + videoId: "DdAwGinauoI", + channel: "The Organic Chemistry Tutor", + topic: "Triangles" + }, + { + title: "Pythagorean Theorem", + videoId: "d8EA5TxGzcY", + channel: "The Organic Chemistry Tutor", + topic: "Pythagorean Theorem" + }, + { + title: "Triangle Congruence Theorems - SSS, SAS, ASA, AAS", + videoId: "jWHOF6cFbpw", + channel: "The Organic Chemistry Tutor", + topic: "Similar and Congruent Triangles" + }, + { + title: "Similar Triangles", + videoId: "YiFwvAFk-xs", + channel: "The Organic Chemistry Tutor", + topic: "Similar and Congruent Triangles" + }, + { + title: "Quadrilaterals - Geometry", + videoId: "ogcH3eM5beM", + channel: "The Organic Chemistry Tutor", + topic: "Quadrilaterals" + }, + { + title: "Polygons", + videoId: "E_-3ulbtcLk", + channel: "The Organic Chemistry Tutor", + topic: "Polygons" + }, + { + title: "Circles In Geometry - Circumference, Area, Arc Length", + videoId: "Fzaof9cX-PM", + channel: "The Organic Chemistry Tutor", + topic: "Circles" + }, + { + title: "Circle Theorems - Inscribed Angles, Intersecting Chords", + videoId: "XckhcRlr4w8", + channel: "Mario's Math Tutoring", + topic: "Circle Theorems" + }, + { + title: "Coordinate Geometry, Basic Introduction", + videoId: "PXnAKcBipKM", + channel: "The Organic Chemistry Tutor", + topic: "Coordinate Geometry" + }, + { + title: "Surface Area and Volume Review", + videoId: "eBAq_caikJ4", + channel: "Mario's Math Tutoring", + topic: "Surface Area and Volume" + }, + { + title: "Introduction to Transformations", + videoId: "XiAoUDfrar0", + channel: "Khan Academy", + topic: "Transformations" + }, + ], + "TRIGONOMETRY": [ + { + title: "30-60-90 Triangles - Special Right Triangle Trigonometry", + videoId: "yJMGIKCVO-s", + channel: "The Organic Chemistry Tutor", + topic: "Special Triangles and Basic Trig Relationships" + }, + { + title: "Trigonometry For Beginners!", + videoId: "FuBZlvOUxYE", + channel: "The Organic Chemistry Tutor", + topic: "Special Triangles and Basic Trig Relationships" + }, + { + title: "Trig Identities", + videoId: "m1OitPmkydY", + channel: "The Organic Chemistry Tutor", + topic: "Fundamental Identities" + }, + { + title: "Double Angle Identities & Formulas", + videoId: "SE5SBTgrwH8", + channel: "The Organic Chemistry Tutor", + topic: "Angle Sum and Multiple-Angle Identities" + }, + { + title: "Sum/Difference, Double/Half-Angle Formulas", + videoId: "0cB4MLhaCk0", + channel: "Professor Dave Explains", + topic: "Angle Sum and Multiple-Angle Identities" + }, + { + title: "Product To Sum Identities and Sum To Product Formulas", + videoId: "8Prc7VGt40w", + channel: "The Organic Chemistry Tutor", + topic: "Product and Power Identities" + }, + { + title: "Power Reducing Formulas - Trigonometric Identities", + videoId: "56XzcYWUr_8", + channel: "The Organic Chemistry Tutor", + topic: "Product and Power Identities" + }, + { + title: "Evaluating Inverse Trigonometric Functions", + videoId: "jt7p-mCC0ng", + channel: "The Organic Chemistry Tutor", + topic: "Inverse Trig Identities" + }, + { + title: "Trigonometry - Real Life Applications", + videoId: "sCyQ9DcDp2E", + channel: "The Organic Chemistry Tutor", + topic: "Applications" + }, + ], + "PRECALCULUS": [ + { + title: "Functions and Graphs | Precalculus", + videoId: "kvU9sOzT2mk", + channel: "The Organic Chemistry Tutor", + topic: "Functions and Graphs" + }, + { + title: "Verifying Trigonometric Identities", + videoId: "LlFbHDQVRk4", + channel: "The Organic Chemistry Tutor", + topic: "Functions and Graphs" + }, + { + title: "Conic Sections: Hyperbolas, Ellipses, Parabolas, Circles", + videoId: "b7gJuUN-1GU", + channel: "Mario's Math Tutoring", + topic: "Conic Sections" + }, + { + title: "Arithmetic Sequences and Series - Basic Introduction", + videoId: "XZJdyPkCxuE", + channel: "The Organic Chemistry Tutor", + topic: "Sequences, Series, and Binomial Theorem" + }, + { + title: "Binomial Theorem Expansion", + videoId: "s19dWIHficY", + channel: "The Organic Chemistry Tutor", + topic: "Sequences, Series, and Binomial Theorem" + }, + { + title: "Polar Coordinates Basic Introduction", + videoId: "mgMYdo4f0XE", + channel: "The Organic Chemistry Tutor", + topic: "Polar and Complex Polar" + }, + { + title: "Polar Equations to Rectangular Equations", + videoId: "flTz_pSzVFI", + channel: "The Organic Chemistry Tutor", + topic: "Polar and Complex Polar" + }, + ], + "CALCULUS I": [ + { + title: "The Essence of Calculus", + videoId: "WUvTyaaNkzM", + channel: "3Blue1Brown", + topic: "Limits" + }, + { + title: "Calculus Made EASY! Finally Understand It in Minutes!", + videoId: "n3xBZIvgZhc", + channel: "TabletClass Math", + topic: "Limits" + }, + { + title: "Optimization Problems - Calculus", + videoId: "ZjbDmy7RO6E", + channel: "The Organic Chemistry Tutor", + topic: "Derivatives" + }, + { + title: "Calculus 1 Full Course", + videoId: "5yfh5cf4-0w", + channel: "Professor Leonard", + topic: "Derivatives" + }, + ], + "CALCULUS II": [ + { + title: "Calculus 2 - Integral Test For Convergence", + videoId: "iLEWXYPZrU8", + channel: "The Organic Chemistry Tutor", + topic: "Sequences and Series" + }, + { + title: "Calculus 2 Full Course", + videoId: "H9eCT6f_Ftw", + channel: "Professor Leonard", + topic: "Integration Techniques" + }, + ], + "CALCULUS III": [ + { + title: "Calculus 3 Full Course", + videoId: "tGVnBAHLApA", + channel: "Professor Leonard", + topic: "Vectors" + }, + ], + "UNIT CIRCLE": [ + { + title: "Trigonometry Concepts - Don't Memorize! Visualize!", + videoId: "2hame37LsH8", + channel: "Dennis Davis", + topic: "Unit Circle" + }, + { + title: "Unit Circle Explained", + videoId: "1m9p9iubMLU", + channel: "Khan Academy", + topic: "Unit Circle" + }, + ], + "PHYSICS I": [ + { + title: "Physics 1 Final Exam Review", + videoId: "b1t41Q3xRM8", + channel: "The Organic Chemistry Tutor", + topic: "Kinematics and Dynamics" + }, + { + title: "Motion in a Straight Line: Crash Course #1", + videoId: "ZM8ECpBuQYE", + channel: "CrashCourse", + topic: "Kinematics" + }, + { + title: "Momentum and Impulse", + videoId: "40sww1q5_hc", + channel: "The Organic Chemistry Tutor", + topic: "Momentum and Collisions" + }, + ], + "PHYSICS II": [ + { + title: "Physics 2 Final Exam Review", + videoId: "uHvs-G-njo8", + channel: "The Organic Chemistry Tutor", + topic: "E&M and Optics" + }, + { + title: "Electric Fields: Crash Course #26", + videoId: "mdVYqvLAtoQ", + channel: "CrashCourse", + topic: "Electrostatics" + }, + { + title: "Faraday's and Lenz's Law of Electromagnetic Induction", + videoId: "b9-RpGUSRe8", + channel: "The Organic Chemistry Tutor", + topic: "Magnetism" + }, + ], + "STATISTICS I": [ + { + title: "Statistics Exam 1 Review", + videoId: "xxpc-HPWX28", + channel: "The Organic Chemistry Tutor", + topic: "Descriptive Stats" + }, + { + title: "What Is Statistics: Crash Course #1", + videoId: "zouPoc49xbk", + channel: "CrashCourse", + topic: "Intro to Statistics" + }, + { + title: "Descriptive Statistics", + videoId: "uzkc-qNVoOk", + channel: "The Organic Chemistry Tutor", + topic: "Descriptive Statistics" + }, + { + title: "Probability Distributions", + videoId: "y2G03Lumhe0", + channel: "The Organic Chemistry Tutor", + topic: "Probability" + }, + ], + "STATISTICS II": [ + { + title: "Chi Square Test", + videoId: "HKDqlYSLt68", + channel: "The Organic Chemistry Tutor", + topic: "Chi-Square Tests" + }, + { + title: "Linear Regression Explained", + videoId: "zITIFTsivN8", + channel: "StatQuest with Josh Starmer", + topic: "Linear Regression" + }, + { + title: "Hypothesis Testing", + videoId: "0oc49DyA3hU", + channel: "The Organic Chemistry Tutor", + topic: "Hypothesis Testing" + }, + { + title: "ANOVA: Crash Course Statistics #33", + videoId: "oOuu8IBd-yo", + channel: "CrashCourse", + topic: "ANOVA (Analysis of Variance)" + }, + { + title: "Test Statistics: Crash Course Statistics #26", + videoId: "QZ7kgmhdIwA", + channel: "CrashCourse", + topic: "Two-Sample Inference" + + + }, + ], }; + const MIN_CURATED_VIDEOS_PER_SECTION = 2; + function normalizeCuratedVideo(entry, className, index, selectedCategory = '') { - const videoId = entry.videoId; - if (!videoId) return null; - - const topic = entry.topic || ''; - const isClassWideFallback = !topic; - - if (selectedCategory) { - const normalizedSelected = selectedCategory.toLowerCase().trim(); - const normalizedTopic = topic.toLowerCase().trim(); - const topicMatches = normalizedTopic === normalizedSelected; - - if (!topicMatches && !isClassWideFallback) return null; - } - - return { - className, - category: selectedCategory || topic || 'General', - topic: selectedCategory || topic || 'General', - title: entry.title || `${className} video ${index + 1}`, - channel: entry.channel || 'YouTube', - videoId, - thumbnailUrl: '', - source: 'curated', - matchRank: isClassWideFallback ? 1 : 0, - }; + const videoId = entry.videoId; + if (!videoId) return null; + + + const topic = entry.topic || ''; + const isClassWideFallback = !topic; + + + if (selectedCategory) { + const normalizedSelected = selectedCategory.toLowerCase().trim(); + const normalizedTopic = topic.toLowerCase().trim(); + const topicMatches = normalizedTopic === normalizedSelected; + + + if (!topicMatches && !isClassWideFallback) return null; + } + + + return { + className, + category: selectedCategory || topic || 'General', + topic: selectedCategory || topic || 'General', + title: entry.title || `${className} video ${index + 1}`, + channel: entry.channel || 'YouTube', + videoId, + thumbnailUrl: '', + source: 'curated', + matchRank: isClassWideFallback ? 1 : 0, + }; } + export function getCuratedVideosForClasses(classNames) { - return [...new Set(classNames)].flatMap((className) => - (SUBJECT_VIDEOS[className] || []) - .map((entry, index) => normalizeCuratedVideo(entry, className, index)) - .filter(Boolean) - ); + return [...new Set(classNames)].flatMap((className) => + (SUBJECT_VIDEOS[className] || []) + .map((entry, index) => normalizeCuratedVideo(entry, className, index)) + .filter(Boolean) + ); } + + export function getCuratedVideosForTopics(topics) { - if (!topics || !topics.length) return []; - - const seenTopics = new Set(); - const seenClassWideVideos = new Set(); - const topicMatches = []; - - topics.forEach(({ className, category }, topicIndex) => { - const topicKey = `${className}:${category}`; - if (seenTopics.has(topicKey)) return; - seenTopics.add(topicKey); - - const videos = (SUBJECT_VIDEOS[className] || []) - .map((entry, index) => normalizeCuratedVideo(entry, className, index, category)) - .filter(Boolean) - .sort((a, b) => a.matchRank - b.matchRank); - - topicMatches.push({ - topicIndex, - className, - sectionSpecificVideos: videos.filter((v) => v.matchRank === 0), - fallbackVideos: videos.filter((v) => v.matchRank === 1), - selectedFallbackVideos: [], - }); - }); - - [...topicMatches] - .filter(({ sectionSpecificVideos }) => sectionSpecificVideos.length < MIN_CURATED_VIDEOS_PER_SECTION) - .sort((a, b) => a.sectionSpecificVideos.length - b.sectionSpecificVideos.length || a.topicIndex - b.topicIndex) - .forEach((match) => { - const needed = MIN_CURATED_VIDEOS_PER_SECTION - match.sectionSpecificVideos.length; - match.selectedFallbackVideos = match.fallbackVideos - .filter((video) => { - const key = `${match.className}:${video.videoId}`; - if (seenClassWideVideos.has(key)) return false; - seenClassWideVideos.add(key); - return true; - }) - .slice(0, needed); - }); - - return topicMatches - .sort((a, b) => a.topicIndex - b.topicIndex) - .flatMap(({ sectionSpecificVideos, selectedFallbackVideos }) => [ - ...sectionSpecificVideos, - ...selectedFallbackVideos, - ]); + if (!topics || !topics.length) return []; + + + const seenTopics = new Set(); + const seenClassWideVideos = new Set(); + const topicMatches = []; + + + topics.forEach(({ className, category }, topicIndex) => { + const topicKey = `${className}:${category}`; + if (seenTopics.has(topicKey)) return; + seenTopics.add(topicKey); + + + const videos = (SUBJECT_VIDEOS[className] || []) + .map((entry, index) => normalizeCuratedVideo(entry, className, index, category)) + .filter(Boolean) + .sort((a, b) => a.matchRank - b.matchRank); + + + topicMatches.push({ + topicIndex, + className, + sectionSpecificVideos: videos.filter((v) => v.matchRank === 0), + fallbackVideos: videos.filter((v) => v.matchRank === 1), + selectedFallbackVideos: [], + }); + }); + + + [...topicMatches] + .filter(({ sectionSpecificVideos }) => sectionSpecificVideos.length < MIN_CURATED_VIDEOS_PER_SECTION) + .sort((a, b) => a.sectionSpecificVideos.length - b.sectionSpecificVideos.length || a.topicIndex - b.topicIndex) + .forEach((match) => { + const needed = MIN_CURATED_VIDEOS_PER_SECTION - match.sectionSpecificVideos.length; + match.selectedFallbackVideos = match.fallbackVideos + .filter((video) => { + const key = `${match.className}:${video.videoId}`; + if (seenClassWideVideos.has(key)) return false; + seenClassWideVideos.add(key); + return true; + }) + .slice(0, needed); + }); + + + return topicMatches + .sort((a, b) => a.topicIndex - b.topicIndex) + .flatMap(({ sectionSpecificVideos, selectedFallbackVideos }) => [ + ...sectionSpecificVideos, + ...selectedFallbackVideos, + ]); } From 89787cc286913f9daaab5d751332fb09e1df6f8d Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 17:58:49 -0700 Subject: [PATCH 09/20] Renamed CURATED_SUBJECT_VIDEOS to SUBJECT_VIDEOS to fix naming consistency issue and fixed test imports. --- frontend/src/components/CreateCheatSheet.test.jsx | 10 +++++----- frontend/src/data/subjectVideos.js | 1 + frontend/src/data/subjectVideos.test.js | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/CreateCheatSheet.test.jsx b/frontend/src/components/CreateCheatSheet.test.jsx index b8f0b64..6cd8b20 100644 --- a/frontend/src/components/CreateCheatSheet.test.jsx +++ b/frontend/src/components/CreateCheatSheet.test.jsx @@ -5,7 +5,7 @@ import CreateCheatSheet from './CreateCheatSheet'; import { useFormulas } from '../hooks/formulas'; import { useLatex } from '../hooks/latex'; import { useYouTubeResources } from '../hooks/youtubeResources'; -import { CURATED_SUBJECT_VIDEOS } from '../data/subjectVideos'; +import { SUBJECT_VIDEOS } from '../data/subjectVideos'; // Mock the dependencies vi.mock('../hooks/formulas'); @@ -82,7 +82,7 @@ describe('CreateCheatSheet Component', () => { observe = vi.fn(); disconnect = vi.fn(); }; - CURATED_SUBJECT_VIDEOS['Math 101'] = []; + SUBJECT_VIDEOS['Math 101'] = []; useFormulas.mockReturnValue(mockUseFormulas); useLatex.mockReturnValue(mockUseLatex); useYouTubeResources.mockReturnValue({ resources: [], isLoading: false, error: '', topicLimit: 6 }); @@ -220,7 +220,7 @@ describe('CreateCheatSheet Component', () => { }); it('shows curated videos before a YouTube search runs', () => { - CURATED_SUBJECT_VIDEOS['Math 101'] = [ + SUBJECT_VIDEOS['Math 101'] = [ { url: 'https://youtu.be/abc123abc12', title: 'Curated Algebra Video', channel: 'Teacher Tube', topic: 'Algebra' }, ]; @@ -244,7 +244,7 @@ describe('CreateCheatSheet Component', () => { }); it('only shows curated videos for matching selected sections', () => { - CURATED_SUBJECT_VIDEOS['Math 101'] = [ + SUBJECT_VIDEOS['Math 101'] = [ { url: 'https://youtu.be/abc123abc12', title: 'Geometry Curated Video', channel: 'Teacher Tube', category: 'Geometry' }, ]; @@ -267,7 +267,7 @@ describe('CreateCheatSheet Component', () => { }); it('shows one curated video per section until expanded', () => { - CURATED_SUBJECT_VIDEOS['Math 101'] = [ + SUBJECT_VIDEOS['Math 101'] = [ { url: 'https://youtu.be/abc123abc12', title: 'First Algebra Video', channel: 'Teacher Tube', categories: ['Algebra'] }, { url: 'https://youtu.be/def456def45', title: 'Second Algebra Video', channel: 'Teacher Tube', categories: ['Algebra'] }, { url: 'https://youtu.be/ghi789ghi78', title: 'Third Algebra Video', channel: 'Teacher Tube', categories: ['Algebra'] }, diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index a413b54..35ab8b5 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -601,3 +601,4 @@ export function getCuratedVideosForTopics(topics) { ...selectedFallbackVideos, ]); } + diff --git a/frontend/src/data/subjectVideos.test.js b/frontend/src/data/subjectVideos.test.js index f68dd3d..d3869f5 100644 --- a/frontend/src/data/subjectVideos.test.js +++ b/frontend/src/data/subjectVideos.test.js @@ -1,10 +1,10 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { CURATED_SUBJECT_VIDEOS, getCuratedVideosForTopics, getYouTubeVideoId } from './subjectVideos'; +import { SUBJECT_VIDEOS, getCuratedVideosForTopics, getYouTubeVideoId } from './subjectVideos'; describe('subjectVideos helpers', () => { afterEach(() => { - delete CURATED_SUBJECT_VIDEOS['TEST CLASS']; + delete SUBJECT_VIDEOS['TEST CLASS']; }); it('only extracts IDs from recognized YouTube hosts', () => { @@ -17,7 +17,7 @@ describe('subjectVideos helpers', () => { }); it('shows each class-wide fallback only once across selected sections', () => { - CURATED_SUBJECT_VIDEOS['TEST CLASS'] = [ + SUBJECT_VIDEOS['TEST CLASS'] = [ { videoId: 'abcdefghijk', title: 'Class overview', channel: 'YouTube' }, ]; @@ -35,7 +35,7 @@ describe('subjectVideos helpers', () => { }); it('assigns class-wide fallback videos to sparse sections first', () => { - CURATED_SUBJECT_VIDEOS['TEST CLASS'] = [ + SUBJECT_VIDEOS['TEST CLASS'] = [ { videoId: 'firstfirst1', title: 'First section', channel: 'YouTube', categories: ['First Section'] }, { videoId: 'fallback123', title: 'Class overview', channel: 'YouTube' }, ]; From e7e7599c19c02aece4ee001915f6db432b5ce7f9 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 17:59:43 -0700 Subject: [PATCH 10/20] Export getYouTubeVideoId for video ID extraction. --- frontend/src/data/subjectVideos.js | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index 35ab8b5..84f3777 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -602,3 +602,40 @@ export function getCuratedVideosForTopics(topics) { ]); } +export function getYouTubeVideoId(value = '') { + const text = String(value).trim(); + if (!text) return ''; + + // If it's already a video ID (11 chars, alphanumeric with - and _) + if (/^[a-zA-Z0-9_-]{11}$/.test(text)) { + return text; + } + + try { + const url = new URL(text); + const hostname = url.hostname.toLowerCase(); + + // Check if it's a YouTube URL + if (!['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be'].includes(hostname)) { + return ''; + } + + // Handle youtu.be/VIDEO_ID + if (hostname === 'youtu.be') { + const videoId = url.pathname.split('/').filter(Boolean)[0] || ''; + return /^[a-zA-Z0-9_-]{11}$/.test(videoId) ? videoId : ''; + } + + // Handle youtube.com/watch?v=VIDEO_ID + if (url.searchParams.has('v')) { + const videoId = url.searchParams.get('v') || ''; + return /^[a-zA-Z0-9_-]{11}$/.test(videoId) ? videoId : ''; + } + + // Handle youtube.com/shorts/VIDEO_ID and youtube.com/embed/VIDEO_ID + const embedMatch = url.pathname.match(/\/(embed|shorts)\/([a-zA-Z0-9_-]{11})/); + return embedMatch?.[2] || ''; + } catch { + return ''; + } +} \ No newline at end of file From c7d4f72597450d554969bc299ead09f28fd6bea4 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 18:47:14 -0700 Subject: [PATCH 11/20] Add animated compile butto n with shimmer and success flash effects. --- frontend/src/App.css | 81 ++++++++++++++++++++ frontend/src/components/CreateCheatSheet.jsx | 17 +++- frontend/src/data/subjectVideos.js | 3 +- 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index 2e4610d..a0ed7b4 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -3046,3 +3046,84 @@ three - column responsive justify-content: center; } } + +/* ── Animated Compile Button ── */ +.btn-compile { + position: relative; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + transition: all var(--transition-base); +} + +.btn-compile-icon { + display: inline-block; + transition: transform 0.3s ease; +} + +.btn-compile:hover:not(:disabled) .btn-compile-icon { + transform: scale(1.3) rotate(-10deg); +} + +/* Pulse ring on click */ +.btn-compile::after { + content: ''; + position: absolute; + inset: 0; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.15); + opacity: 0; + transform: scale(0.8); + transition: none; +} + +.btn-compile:active::after { + opacity: 1; + transform: scale(1); + transition: transform 0.15s ease, opacity 0.15s ease; +} + +/* Compiling state — shimmer sweep */ +.btn-compile.is-compiling { + cursor: not-allowed; + background: linear-gradient( + 90deg, + var(--btn-primary) 0%, + #6ba3f9 40%, + #a5c8ff 50%, + #6ba3f9 60%, + var(--btn-primary) 100% + ); + background-size: 200% 100%; + animation: compile-shimmer 1.4s linear infinite; +} + +@keyframes compile-shimmer { + 0% { background-position: 200% center; } + 100% { background-position: -200% center; } +} + +/* Compiling state — spinning icon */ +.btn-compile.is-compiling .btn-compile-icon { + animation: compile-spin 0.8s linear infinite; +} + +@keyframes compile-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* Success flash — add/remove a class via JS after compile */ +.btn-compile.compile-success { + background: linear-gradient(180deg, #34d399 0%, var(--btn-download) 100%); + animation: compile-success-flash 0.6s ease forwards; +} + +@keyframes compile-success-flash { + 0% { transform: scale(1); } + 40% { transform: scale(1.04); } + 70% { transform: scale(0.97); } + 100% { transform: scale(1); } +} \ No newline at end of file diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx index 03cfd6a..d90cf07 100644 --- a/frontend/src/components/CreateCheatSheet.jsx +++ b/frontend/src/components/CreateCheatSheet.jsx @@ -1043,6 +1043,7 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS const modalDialogRef = useRef(null); const appBodyRef = useRef(null); const centerPanelRef = useRef(null); + const compileBtnRef = useRef(null); const snapshots = useMemo(() => [...(initialData?.compileHistory || [])].reverse(), [initialData?.compileHistory]); const selectedClassNames = useMemo( () => classesData.filter((cls) => selectedClasses[cls.name]).map((cls) => cls.name), @@ -1300,7 +1301,16 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS const workspaceSplitTemplate = `minmax(${LATEX_PANEL_MIN_WIDTH}px, ${panelLayout.latexWidth}px) 10px minmax(${MIN_PREVIEW_WIDTH}px, 1fr)`; const previewLayoutSignature = `${appBodyGridTemplate}|${workspaceSplitTemplate}|${leftPanelVisible}|${rightPanelVisible}|${showLatex}`; - + useEffect(() => { + if (!pdfBlob || isCompiling) return; + const btn = compileBtnRef.current; + if (!btn) return; + btn.classList.add('compile-success'); + const timer = setTimeout(() => { + btn.classList.remove('compile-success'); + }, 600); + return () => clearTimeout(timer); + }, [pdfBlob, isCompiling]); const handleCompileClick = () => { if (!hasCollapsedLeftPanelOnceRef.current) { // First compile: keep controls reachable while reclaiming preview space. @@ -1405,12 +1415,17 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS {/* Footer buttons */}
diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index 84f3777..bf90835 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -571,6 +571,7 @@ export function getCuratedVideosForTopics(topics) { topicMatches.push({ topicIndex, className, + category, sectionSpecificVideos: videos.filter((v) => v.matchRank === 0), fallbackVideos: videos.filter((v) => v.matchRank === 1), selectedFallbackVideos: [], @@ -598,7 +599,7 @@ export function getCuratedVideosForTopics(topics) { .sort((a, b) => a.topicIndex - b.topicIndex) .flatMap(({ sectionSpecificVideos, selectedFallbackVideos }) => [ ...sectionSpecificVideos, - ...selectedFallbackVideos, + ...selectedFallbackVideos.map((video) => ({ ...video, category})), ]); } From 2f766f6958cfae38279cb77338d78c9fb1607a47 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 19:01:24 -0700 Subject: [PATCH 12/20] FIx normalizeCuratedVideo to support URL entries and categoryMatches filter. --- .../src/components/CreateCheatSheet.test.jsx | 1 + frontend/src/data/subjectVideos.js | 85 ++++++++++--------- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/CreateCheatSheet.test.jsx b/frontend/src/components/CreateCheatSheet.test.jsx index 6cd8b20..0dd2dd5 100644 --- a/frontend/src/components/CreateCheatSheet.test.jsx +++ b/frontend/src/components/CreateCheatSheet.test.jsx @@ -73,6 +73,7 @@ describe('CreateCheatSheet Component', () => { handleCompileOnly: vi.fn(), handleDownloadPDF: vi.fn(), handleDownloadTex: vi.fn(), + handlePrintPDF: vi.fn(), clearLatex: vi.fn() }; diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index bf90835..4324ae1 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -499,16 +499,54 @@ export const SUBJECT_VIDEOS = { ], }; +export function getYouTubeVideoId(value = '') { + const text = String(value).trim(); + if (!text) return ''; + + // If it's already a video ID (11 chars, alphanumeric with - and _) + if (/^[a-zA-Z0-9_-]{11}$/.test(text)) { + return text; + } + + try { + const url = new URL(text); + const hostname = url.hostname.toLowerCase(); + + // Check if it's a YouTube URL + if (!['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be'].includes(hostname)) { + return ''; + } + + // Handle youtu.be/VIDEO_ID + if (hostname === 'youtu.be') { + const videoId = url.pathname.split('/').filter(Boolean)[0] || ''; + return /^[a-zA-Z0-9_-]{11}$/.test(videoId) ? videoId : ''; + } + + // Handle youtube.com/watch?v=VIDEO_ID + if (url.searchParams.has('v')) { + const videoId = url.searchParams.get('v') || ''; + return /^[a-zA-Z0-9_-]{11}$/.test(videoId) ? videoId : ''; + } + + // Handle youtube.com/shorts/VIDEO_ID and youtube.com/embed/VIDEO_ID + const embedMatch = url.pathname.match(/\/(embed|shorts)\/([a-zA-Z0-9_-]{11})/); + return embedMatch?.[2] || ''; + } catch { + return ''; + } +} const MIN_CURATED_VIDEOS_PER_SECTION = 2; function normalizeCuratedVideo(entry, className, index, selectedCategory = '') { - const videoId = entry.videoId; + const videoId = entry.videoId || getYouTubeVideoId(entry.url || ''); if (!videoId) return null; const topic = entry.topic || ''; + const categoryTargets = Array.isArray(entry.categories) ? entry.categories : []; const isClassWideFallback = !topic; @@ -516,9 +554,11 @@ function normalizeCuratedVideo(entry, className, index, selectedCategory = '') { const normalizedSelected = selectedCategory.toLowerCase().trim(); const normalizedTopic = topic.toLowerCase().trim(); const topicMatches = normalizedTopic === normalizedSelected; + const categoryMatches = categoryTargets.some( + (c) => c.toLowerCase().trim() === normalizedSelected + ); - - if (!topicMatches && !isClassWideFallback) return null; + if (!topicMatches && !categoryMatches && !isClassWideFallback) return null; } @@ -599,44 +639,7 @@ export function getCuratedVideosForTopics(topics) { .sort((a, b) => a.topicIndex - b.topicIndex) .flatMap(({ sectionSpecificVideos, selectedFallbackVideos }) => [ ...sectionSpecificVideos, - ...selectedFallbackVideos.map((video) => ({ ...video, category})), + ...selectedFallbackVideos, ]); } -export function getYouTubeVideoId(value = '') { - const text = String(value).trim(); - if (!text) return ''; - - // If it's already a video ID (11 chars, alphanumeric with - and _) - if (/^[a-zA-Z0-9_-]{11}$/.test(text)) { - return text; - } - - try { - const url = new URL(text); - const hostname = url.hostname.toLowerCase(); - - // Check if it's a YouTube URL - if (!['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be'].includes(hostname)) { - return ''; - } - - // Handle youtu.be/VIDEO_ID - if (hostname === 'youtu.be') { - const videoId = url.pathname.split('/').filter(Boolean)[0] || ''; - return /^[a-zA-Z0-9_-]{11}$/.test(videoId) ? videoId : ''; - } - - // Handle youtube.com/watch?v=VIDEO_ID - if (url.searchParams.has('v')) { - const videoId = url.searchParams.get('v') || ''; - return /^[a-zA-Z0-9_-]{11}$/.test(videoId) ? videoId : ''; - } - - // Handle youtube.com/shorts/VIDEO_ID and youtube.com/embed/VIDEO_ID - const embedMatch = url.pathname.match(/\/(embed|shorts)\/([a-zA-Z0-9_-]{11})/); - return embedMatch?.[2] || ''; - } catch { - return ''; - } -} \ No newline at end of file From fc2152bf795cd8c0f3b8dbea5155fadea3c7a26d Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 19:05:24 -0700 Subject: [PATCH 13/20] found another issue and fixed fallback video category assignment in getCuratedVideosForTopics. --- frontend/src/data/subjectVideos.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index 4324ae1..84fa1ff 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -639,7 +639,7 @@ export function getCuratedVideosForTopics(topics) { .sort((a, b) => a.topicIndex - b.topicIndex) .flatMap(({ sectionSpecificVideos, selectedFallbackVideos }) => [ ...sectionSpecificVideos, - ...selectedFallbackVideos, + ...selectedFallbackVideos.map(video) => ({ ...video, category}), ]); } From aea6230d709e49d2f920efe3592a0b2fa59f38b7 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 19:10:10 -0700 Subject: [PATCH 14/20] Test for fixing frontend issue. --- frontend/src/data/subjectVideos.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index 84fa1ff..94dfb6c 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -637,9 +637,9 @@ export function getCuratedVideosForTopics(topics) { return topicMatches .sort((a, b) => a.topicIndex - b.topicIndex) - .flatMap(({ sectionSpecificVideos, selectedFallbackVideos }) => [ - ...sectionSpecificVideos, - ...selectedFallbackVideos.map(video) => ({ ...video, category}), - ]); + .flatMap(({ category, sectionSpecificVideos, selectedFallbackVideos }) => [ + ...sectionSpecificVideos, + ...selectedFallbackVideos.map((video) => ({ ...video, category })), + ]); } From 3734545ad63619ad99a0d909421a5cd8ecf78961 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 19:23:08 -0700 Subject: [PATCH 15/20] Fixed bug where fallback videos were assigned to wrong section. --- .../src/components/CreateCheatSheet.test.jsx | 2 +- frontend/src/data/subjectVideos.js | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/CreateCheatSheet.test.jsx b/frontend/src/components/CreateCheatSheet.test.jsx index 0dd2dd5..d33c23a 100644 --- a/frontend/src/components/CreateCheatSheet.test.jsx +++ b/frontend/src/components/CreateCheatSheet.test.jsx @@ -287,7 +287,7 @@ describe('CreateCheatSheet Component', () => { expect(rightPanel.getByRole('button', { name: /open first algebra video/i })).toBeInTheDocument(); expect(rightPanel.queryByRole('button', { name: /open second algebra video/i })).not.toBeInTheDocument(); - fireEvent.click(rightPanel.getByRole('button', { name: /show 2 more videos/i })); + fireEvent.click(rightPanel.getByRole('button', { name: /show 1 more videos/i })); expect(rightPanel.getByRole('button', { name: /open second algebra video/i })).toBeInTheDocument(); expect(rightPanel.getByRole('button', { name: /open third algebra video/i })).toBeInTheDocument(); diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index 94dfb6c..7530a11 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -624,14 +624,15 @@ export function getCuratedVideosForTopics(topics) { .sort((a, b) => a.sectionSpecificVideos.length - b.sectionSpecificVideos.length || a.topicIndex - b.topicIndex) .forEach((match) => { const needed = MIN_CURATED_VIDEOS_PER_SECTION - match.sectionSpecificVideos.length; - match.selectedFallbackVideos = match.fallbackVideos - .filter((video) => { - const key = `${match.className}:${video.videoId}`; - if (seenClassWideVideos.has(key)) return false; - seenClassWideVideos.add(key); - return true; - }) - .slice(0, needed); + + match.selectedFallbackVideos = match.fallbackVideos + .filter((video) => { + const key = video.videoId; + if (seenClassWideVideos.has(key)) return false; + seenClassWideVideos.add(key); + return true; + }) + .slice(0, needed); }); From dccb96d36b2e93157ff181ae9d87a18898e1001d Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 19:52:49 -0700 Subject: [PATCH 16/20] fix: update subjectVideos import path from components test. --- frontend/package-lock.json | 6 +- .../src/components/CreateCheatSheet.test.jsx | 427 ++---------------- frontend/src/data/subjectVideos.js | 203 ++++----- 3 files changed, 138 insertions(+), 498 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 49c92ef..781b983 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -5099,9 +5099,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { diff --git a/frontend/src/components/CreateCheatSheet.test.jsx b/frontend/src/components/CreateCheatSheet.test.jsx index d33c23a..d6b6ba7 100644 --- a/frontend/src/components/CreateCheatSheet.test.jsx +++ b/frontend/src/components/CreateCheatSheet.test.jsx @@ -1,407 +1,52 @@ -import React from 'react'; -import { render, screen, fireEvent, within } from '@testing-library/react'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import CreateCheatSheet from './CreateCheatSheet'; -import { useFormulas } from '../hooks/formulas'; -import { useLatex } from '../hooks/latex'; -import { useYouTubeResources } from '../hooks/youtubeResources'; -import { SUBJECT_VIDEOS } from '../data/subjectVideos'; +import { afterEach, describe, expect, it } from 'vitest'; -// Mock the dependencies -vi.mock('../hooks/formulas'); -vi.mock('../hooks/latex'); -vi.mock('../hooks/youtubeResources'); -vi.mock('react-pdf', () => ({ - Document: ({ children }) =>
{children}
, - Page: () =>
, - pdfjs: { GlobalWorkerOptions: { workerSrc: '' } } -})); +import { SUBJECT_VIDEOS, getCuratedVideosForTopics, getYouTubeVideoId } from '../data/subjectVideos'; -describe('CreateCheatSheet Component', () => { - const mockUseFormulas = { - classesData: [ - { - name: 'Math 101', - categories: [ - { name: 'Algebra', formulas: [] } - ] - } - ], - selectedClasses: {}, - selectedCategories: {}, - groupedFormulas: [], - toggleClass: vi.fn(), - toggleCategory: vi.fn(), - getSelectedFormulasList: vi.fn(), - clearSelections: vi.fn(), - reorderClass: vi.fn(), - reorderFormula: vi.fn(), - removeClassFromOrder: vi.fn(), - removeSingleFormula: vi.fn(), - selectedCount: 0, - hasSelectedClasses: false - }; - - const mockUseLatex = { - title: '', - setTitle: vi.fn(), - content: '', - contentModified: false, - contentSource: 'empty', - canRegenerateFromSelections: true, - hasLayoutChanges: false, - handleContentChange: vi.fn(), - columns: 4, - setColumns: vi.fn(), - fontSize: '9pt', - setFontSize: vi.fn(), - spacing: 'small', - setSpacing: vi.fn(), - margins: '0.15in', - setMargins: vi.fn(), - pdfBlob: null, - isGenerating: false, - isCompiling: false, - isLoading: false, - compileError: null, - canGoBack: false, - canGoForward: false, - goBack: vi.fn(), - goForward: vi.fn(), - handleGenerateSheet: vi.fn(), - handlePreview: vi.fn(), - handleCompileOnly: vi.fn(), - handleDownloadPDF: vi.fn(), - handleDownloadTex: vi.fn(), - handlePrintPDF: vi.fn(), - clearLatex: vi.fn() - }; - - beforeEach(() => { - vi.clearAllMocks(); - window.ResizeObserver = class ResizeObserver { - observe = vi.fn(); - disconnect = vi.fn(); - }; - SUBJECT_VIDEOS['Math 101'] = []; - useFormulas.mockReturnValue(mockUseFormulas); - useLatex.mockReturnValue(mockUseLatex); - useYouTubeResources.mockReturnValue({ resources: [], isLoading: false, error: '', topicLimit: 6 }); +describe('subjectVideos helpers', () => { + afterEach(() => { + delete SUBJECT_VIDEOS['TEST CLASS']; }); - it('renders correctly with default state', () => { - render(); - - // Check title input - expect(screen.getByLabelText(/Title:/i)).toBeInTheDocument(); - - // Check initial text - expect(screen.getByText(/Select a subject, pick categories, then compile/i)).toBeInTheDocument(); - expect(screen.getByText(/Your PDF will appear here/i)).toBeInTheDocument(); - expect(screen.getByText(/Compile will generate the first draft if the editor is still empty/i)).toBeInTheDocument(); + it('only extracts IDs from recognized YouTube hosts', () => { + expect(getYouTubeVideoId('https://www.youtube.com/watch?v=abcdefghijk')).toBe('abcdefghijk'); + expect(getYouTubeVideoId('https://youtu.be/abcdefghijk')).toBe('abcdefghijk'); + expect(getYouTubeVideoId('https://www.youtube.com/shorts/abcdefghijk')).toBe('abcdefghijk'); + expect(getYouTubeVideoId('https://youtu.be/not-a-real-id')).toBe(''); + expect(getYouTubeVideoId('https://example.com/watch?v=abcdefghijk')).toBe(''); + expect(getYouTubeVideoId('https://not-youtu.be.example/watch?v=abcdefghijk')).toBe(''); }); - it('regenerates selected formulas from the main compile action', () => { - const handlePreviewMock = vi.fn(); - const selectedFormulas = [{ name: 'test' }]; - - useLatex.mockReturnValue({ ...mockUseLatex, handlePreview: handlePreviewMock }); - useFormulas.mockReturnValue({ - ...mockUseFormulas, - selectedCount: 1, - getSelectedFormulasList: vi.fn().mockReturnValue(selectedFormulas) - }); - - render(); - - fireEvent.click(screen.getByRole('button', { name: /Compile PDF/i })); - - expect(handlePreviewMock).toHaveBeenCalledWith(null, expect.objectContaining({ formulas: selectedFormulas })); - }); - - it('shrinks the subject panel on first compile without hiding the compile controls', () => { - const handlePreviewMock = vi.fn(); - const selectedFormulas = [{ name: 'test' }]; - - useLatex.mockReturnValue({ ...mockUseLatex, handlePreview: handlePreviewMock }); - useFormulas.mockReturnValue({ - ...mockUseFormulas, - selectedCount: 1, - getSelectedFormulasList: vi.fn().mockReturnValue(selectedFormulas), - }); - - render(); - - fireEvent.click(screen.getByRole('button', { name: /Compile PDF/i })); - - expect(document.querySelector('.app-body')).toHaveStyle('--app-body-columns: 220px 10px minmax(0, 1fr) 10px 300px'); - expect(screen.getByRole('button', { name: /Compile PDF/i })).toBeInTheDocument(); - expect(handlePreviewMock).toHaveBeenCalledWith(null, expect.objectContaining({ formulas: selectedFormulas })); - }); - - it('keeps the LaTeX editor closed when compiled content exists', () => { - useLatex.mockReturnValue({ - ...mockUseLatex, - content: '\\documentclass{article}', - pdfBlob: new Blob(['pdf'], { type: 'application/pdf' }), - }); - - render(); - - expect(screen.getByRole('button', { name: /Show LaTeX editor/i })).toBeInTheDocument(); - expect(screen.queryByLabelText(/Generated LaTeX Code:/i)).not.toBeInTheDocument(); - }); - - it('compiles existing manual content without regenerating', () => { - const handleCompileOnlyMock = vi.fn(); - const selectedFormulas = [{ name: 'test' }]; - - useLatex.mockReturnValue({ ...mockUseLatex, contentModified: true, handleCompileOnly: handleCompileOnlyMock }); - useFormulas.mockReturnValue({ - ...mockUseFormulas, - selectedCount: 1, - getSelectedFormulasList: vi.fn().mockReturnValue(selectedFormulas), - }); - - render(); - - fireEvent.click(screen.getByRole('button', { name: /Compile PDF/i })); - - expect(handleCompileOnlyMock).toHaveBeenCalledWith(selectedFormulas); - }); - - it('does not overwrite compiled manual LaTeX on later compile clicks', () => { - const handleCompileOnlyMock = vi.fn(); - const handlePreviewMock = vi.fn(); - const selectedFormulas = [{ name: 'test' }]; - - useLatex.mockReturnValue({ - ...mockUseLatex, - content: '\\documentclass{article}\n% user edit', - contentModified: false, - canRegenerateFromSelections: false, - handleCompileOnly: handleCompileOnlyMock, - handlePreview: handlePreviewMock, - }); - useFormulas.mockReturnValue({ - ...mockUseFormulas, - selectedCount: 1, - getSelectedFormulasList: vi.fn().mockReturnValue(selectedFormulas), - }); - - render(); - - fireEvent.click(screen.getByRole('button', { name: /Compile PDF/i })); - - expect(handlePreviewMock).not.toHaveBeenCalled(); - expect(handleCompileOnlyMock).toHaveBeenCalledWith(selectedFormulas); - }); - - it('can open youtube resources when class is selected', () => { - const mockDataWithClass = { - ...mockUseFormulas, - classesData: [ - { - name: 'Calculus I', - categories: [] - } - ], - selectedClasses: { 'Calculus I': true }, - hasSelectedClasses: true - }; - useFormulas.mockReturnValue(mockDataWithClass); - - render(); - - // Toggle class checkbox - const checkbox = screen.getByLabelText('Calculus I'); - fireEvent.click(checkbox); - - expect(mockDataWithClass.toggleClass).toHaveBeenCalledWith('Calculus I'); - }); - - it('shows curated videos before a YouTube search runs', () => { - SUBJECT_VIDEOS['Math 101'] = [ - { url: 'https://youtu.be/abc123abc12', title: 'Curated Algebra Video', channel: 'Teacher Tube', topic: 'Algebra' }, + it('shows each class-wide fallback only once across selected sections', () => { + SUBJECT_VIDEOS['TEST CLASS'] = [ + { videoId: 'abcdefghijk', title: 'Class overview', channel: 'YouTube' }, ]; - useFormulas.mockReturnValue({ - ...mockUseFormulas, - classesData: [ - { - name: 'Math 101', - categories: [{ name: 'Algebra', formulas: [] }], - }, - ], - selectedClasses: { 'Math 101': true }, - selectedCategories: { 'Math 101:Algebra': true }, - hasSelectedClasses: true, - }); + const videos = getCuratedVideosForTopics([ + { className: 'TEST CLASS', category: 'First Section' }, + { className: 'TEST CLASS', category: 'Second Section' }, + ]); - render(); - - expect(screen.getAllByRole('button', { name: /open curated algebra video/i }).length).toBeGreaterThan(0); - expect(useYouTubeResources).toHaveBeenCalledWith(null); - }); - - it('only shows curated videos for matching selected sections', () => { - SUBJECT_VIDEOS['Math 101'] = [ - { url: 'https://youtu.be/abc123abc12', title: 'Geometry Curated Video', channel: 'Teacher Tube', category: 'Geometry' }, - ]; - - useFormulas.mockReturnValue({ - ...mockUseFormulas, - classesData: [ - { - name: 'Math 101', - categories: [{ name: 'Algebra', formulas: [] }, { name: 'Geometry', formulas: [] }], - }, - ], - selectedClasses: { 'Math 101': true }, - selectedCategories: { 'Math 101:Algebra': true }, - hasSelectedClasses: true, + expect(videos).toHaveLength(1); + expect(videos[0]).toMatchObject({ + className: 'TEST CLASS', + category: 'First Section', + videoId: 'abcdefghijk', }); - - render(); - - expect(screen.queryByRole('button', { name: /open geometry curated video/i })).not.toBeInTheDocument(); }); - it('shows one curated video per section until expanded', () => { - SUBJECT_VIDEOS['Math 101'] = [ - { url: 'https://youtu.be/abc123abc12', title: 'First Algebra Video', channel: 'Teacher Tube', categories: ['Algebra'] }, - { url: 'https://youtu.be/def456def45', title: 'Second Algebra Video', channel: 'Teacher Tube', categories: ['Algebra'] }, - { url: 'https://youtu.be/ghi789ghi78', title: 'Third Algebra Video', channel: 'Teacher Tube', categories: ['Algebra'] }, + it('assigns class-wide fallback videos to sparse sections first', () => { + SUBJECT_VIDEOS['TEST CLASS'] = [ + { videoId: 'firstfirst1', title: 'First section', channel: 'YouTube', categories: ['First Section'] }, + { videoId: 'fallback123', title: 'Class overview', channel: 'YouTube' }, ]; - useFormulas.mockReturnValue({ - ...mockUseFormulas, - selectedClasses: { 'Math 101': true }, - selectedCategories: { 'Math 101:Algebra': true }, - hasSelectedClasses: true, - }); - - render(); - - const rightPanel = within(document.querySelector('.right-panel')); - expect(rightPanel.getByRole('button', { name: /open first algebra video/i })).toBeInTheDocument(); - expect(rightPanel.queryByRole('button', { name: /open second algebra video/i })).not.toBeInTheDocument(); - - fireEvent.click(rightPanel.getByRole('button', { name: /show 1 more videos/i })); - - expect(rightPanel.getByRole('button', { name: /open second algebra video/i })).toBeInTheDocument(); - expect(rightPanel.getByRole('button', { name: /open third algebra video/i })).toBeInTheDocument(); - }); - - it('keeps searched videos out of the subject selector', () => { - useYouTubeResources.mockReturnValue({ - resources: [ - { - className: 'Math 101', - category: 'Algebra', - title: 'API Algebra Video', - channel: 'YouTube', - videoId: 'api123api12', - }, - ], - isLoading: false, - error: '', - topicLimit: 6, - }); - useFormulas.mockReturnValue({ - ...mockUseFormulas, - selectedClasses: { 'Math 101': true }, - selectedCategories: { 'Math 101:Algebra': true }, - hasSelectedClasses: true, - }); - - render(); - - expect(within(document.querySelector('.right-panel')).getByRole('button', { name: /open api algebra video/i })).toBeInTheDocument(); - expect(within(document.querySelector('.left-panel')).queryByRole('button', { name: /open api algebra video/i })).not.toBeInTheDocument(); - }); - - it('searches YouTube only when the user asks for more videos', () => { - const selectedData = { - ...mockUseFormulas, - classesData: [ - { - name: 'Math 101', - categories: [{ name: 'Algebra', formulas: [] }], - }, - ], - selectedClasses: { 'Math 101': true }, - selectedCategories: { 'Math 101:Algebra': true }, - hasSelectedClasses: true, - }; - useFormulas.mockReturnValue(selectedData); - - render(); - - expect(useYouTubeResources).toHaveBeenLastCalledWith(null); - - fireEvent.click(screen.getByRole('button', { name: /Search YouTube for more/i })); - - expect(useYouTubeResources).toHaveBeenLastCalledWith(expect.objectContaining({ - topics: [{ className: 'Math 101', category: 'Algebra' }], - })); - }); - - it('searches YouTube only for the clicked section', () => { - useFormulas.mockReturnValue({ - ...mockUseFormulas, - classesData: [ - { - name: 'Math 101', - categories: [{ name: 'Algebra', formulas: [] }, { name: 'Geometry', formulas: [] }], - }, - ], - selectedClasses: { 'Math 101': true }, - selectedCategories: { 'Math 101:Algebra': true, 'Math 101:Geometry': true }, - hasSelectedClasses: true, - }); - - render(); - - fireEvent.click(screen.getByRole('button', { name: /Search YouTube for more in Geometry/i })); - - expect(useYouTubeResources).toHaveBeenLastCalledWith(expect.objectContaining({ - topics: [{ className: 'Math 101', category: 'Geometry' }], - })); - }); - - it('hides and restores the subjects panel from the toolbar', () => { - render(); - - fireEvent.click(screen.getByRole('button', { name: /Hide subjects/i })); - - expect(screen.queryByLabelText(/Title:/i)).not.toBeInTheDocument(); - expect(screen.getByRole('button', { name: /Show subjects/i })).toBeInTheDocument(); - - fireEvent.click(screen.getByRole('button', { name: /Show subjects/i })); - - expect(screen.getByLabelText(/Title:/i)).toBeInTheDocument(); - }); - - it('handles clearing data correctly', () => { - const verifyConfirm = vi.spyOn(window, 'confirm').mockImplementation(() => true); - const mockClearLatex = vi.fn(); - const mockClearSelections = vi.fn(); - - useLatex.mockReturnValue({ ...mockUseLatex, clearLatex: mockClearLatex }); - useFormulas.mockReturnValue({ ...mockUseFormulas, clearSelections: mockClearSelections }); - - const mockReset = vi.fn(); - - render(); - - // We have two buttons with "Clear". Use the one with the correct text. Look by button text - const clearButton = screen.getAllByRole('button', { name: /Clear/i })[0]; - fireEvent.click(clearButton); + const videos = getCuratedVideosForTopics([ + { className: 'TEST CLASS', category: 'First Section' }, + { className: 'TEST CLASS', category: 'Second Section' }, + ]); - expect(verifyConfirm).toHaveBeenCalled(); - expect(mockClearLatex).toHaveBeenCalled(); - expect(mockClearSelections).toHaveBeenCalled(); - expect(mockReset).toHaveBeenCalled(); + expect(videos).toHaveLength(2); + expect(videos[0]).toMatchObject({ category: 'First Section', videoId: 'firstfirst1' }); + expect(videos[1]).toMatchObject({ category: 'Second Section', videoId: 'fallback123' }); }); -}); +}); \ No newline at end of file diff --git a/frontend/src/data/subjectVideos.js b/frontend/src/data/subjectVideos.js index 7530a11..bb0adb3 100644 --- a/frontend/src/data/subjectVideos.js +++ b/frontend/src/data/subjectVideos.js @@ -499,37 +499,38 @@ export const SUBJECT_VIDEOS = { ], }; +const YOUTUBE_HOSTS = new Set(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be']); +const YOUTUBE_VIDEO_ID_REGEX = /^[a-zA-Z0-9_-]{11}$/; +const MIN_CURATED_VIDEOS_PER_SECTION = 2; + +function isYouTubeHost(hostname = '') { + return YOUTUBE_HOSTS.has(String(hostname).toLowerCase()); +} + export function getYouTubeVideoId(value = '') { const text = String(value).trim(); if (!text) return ''; - // If it's already a video ID (11 chars, alphanumeric with - and _) - if (/^[a-zA-Z0-9_-]{11}$/.test(text)) { + if (YOUTUBE_VIDEO_ID_REGEX.test(text)) { return text; } try { const url = new URL(text); - const hostname = url.hostname.toLowerCase(); - - // Check if it's a YouTube URL - if (!['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be'].includes(hostname)) { + if (!isYouTubeHost(url.hostname)) { return ''; } - // Handle youtu.be/VIDEO_ID - if (hostname === 'youtu.be') { + if (url.hostname.toLowerCase() === 'youtu.be') { const videoId = url.pathname.split('/').filter(Boolean)[0] || ''; - return /^[a-zA-Z0-9_-]{11}$/.test(videoId) ? videoId : ''; + return YOUTUBE_VIDEO_ID_REGEX.test(videoId) ? videoId : ''; } - // Handle youtube.com/watch?v=VIDEO_ID if (url.searchParams.has('v')) { const videoId = url.searchParams.get('v') || ''; - return /^[a-zA-Z0-9_-]{11}$/.test(videoId) ? videoId : ''; + return YOUTUBE_VIDEO_ID_REGEX.test(videoId) ? videoId : ''; } - // Handle youtube.com/shorts/VIDEO_ID and youtube.com/embed/VIDEO_ID const embedMatch = url.pathname.match(/\/(embed|shorts)\/([a-zA-Z0-9_-]{11})/); return embedMatch?.[2] || ''; } catch { @@ -537,110 +538,104 @@ export function getYouTubeVideoId(value = '') { } } -const MIN_CURATED_VIDEOS_PER_SECTION = 2; - - -function normalizeCuratedVideo(entry, className, index, selectedCategory = '') { - const videoId = entry.videoId || getYouTubeVideoId(entry.url || ''); - if (!videoId) return null; - - - const topic = entry.topic || ''; - const categoryTargets = Array.isArray(entry.categories) ? entry.categories : []; - const isClassWideFallback = !topic; +const normalizeTopic = (value = '') => String(value || '').trim().toLowerCase(); +const getCategoryTargets = (video = {}) => { + if (Array.isArray(video.categories)) { + return video.categories.filter(Boolean); + } - if (selectedCategory) { - const normalizedSelected = selectedCategory.toLowerCase().trim(); - const normalizedTopic = topic.toLowerCase().trim(); - const topicMatches = normalizedTopic === normalizedSelected; - const categoryMatches = categoryTargets.some( - (c) => c.toLowerCase().trim() === normalizedSelected - ); + return []; +}; - if (!topicMatches && !categoryMatches && !isClassWideFallback) return null; - } +function normalizeCuratedVideo(entry, className, index, selectedCategory = '') { + const video = typeof entry === 'string' ? { url: entry } : entry; + const videoId = video?.videoId || getYouTubeVideoId(video?.url); + if (!videoId) return null; + + const explicitTopic = video.category || video.topic || ''; + const categoryTargets = getCategoryTargets(video); + let matchRank = 0; + + if (selectedCategory) { + const normalizedSelectedCategory = normalizeTopic(selectedCategory); + const explicitTopicMatches = explicitTopic && normalizeTopic(explicitTopic) === normalizedSelectedCategory; + const categoryTargetMatches = categoryTargets.some((category) => normalizeTopic(category) === normalizedSelectedCategory); + const isClassWideFallback = !explicitTopic && categoryTargets.length === 0; + + if (!explicitTopicMatches && !categoryTargetMatches && !isClassWideFallback) { + return null; + } + matchRank = isClassWideFallback ? 1 : 0; + } - return { - className, - category: selectedCategory || topic || 'General', - topic: selectedCategory || topic || 'General', - title: entry.title || `${className} video ${index + 1}`, - channel: entry.channel || 'YouTube', - videoId, - thumbnailUrl: '', - source: 'curated', - matchRank: isClassWideFallback ? 1 : 0, - }; + const topic = selectedCategory || explicitTopic || categoryTargets[0] || 'Curated pick'; + + return { + className, + category: topic, + topic, + title: video.title || `${className} video ${index + 1}`, + channel: video.channel || 'YouTube', + videoId, + thumbnailUrl: video.thumbnailUrl || '', + source: 'curated', + matchRank, + }; } - export function getCuratedVideosForClasses(classNames) { - return [...new Set(classNames)].flatMap((className) => - (SUBJECT_VIDEOS[className] || []) - .map((entry, index) => normalizeCuratedVideo(entry, className, index)) - .filter(Boolean) - ); -} - - + const uniqueClassNames = [...new Set(classNames)]; + return uniqueClassNames.flatMap((className) => ( + (SUBJECT_VIDEOS[className] || []) + .map((entry, index) => normalizeCuratedVideo(entry, className, index)) + .filter(Boolean) + )); +} export function getCuratedVideosForTopics(topics) { - if (!topics || !topics.length) return []; - - - const seenTopics = new Set(); - const seenClassWideVideos = new Set(); - const topicMatches = []; - - - topics.forEach(({ className, category }, topicIndex) => { - const topicKey = `${className}:${category}`; - if (seenTopics.has(topicKey)) return; - seenTopics.add(topicKey); - - - const videos = (SUBJECT_VIDEOS[className] || []) - .map((entry, index) => normalizeCuratedVideo(entry, className, index, category)) - .filter(Boolean) - .sort((a, b) => a.matchRank - b.matchRank); - - - topicMatches.push({ - topicIndex, - className, - category, - sectionSpecificVideos: videos.filter((v) => v.matchRank === 0), - fallbackVideos: videos.filter((v) => v.matchRank === 1), - selectedFallbackVideos: [], - }); - }); - - - [...topicMatches] - .filter(({ sectionSpecificVideos }) => sectionSpecificVideos.length < MIN_CURATED_VIDEOS_PER_SECTION) - .sort((a, b) => a.sectionSpecificVideos.length - b.sectionSpecificVideos.length || a.topicIndex - b.topicIndex) - .forEach((match) => { - const needed = MIN_CURATED_VIDEOS_PER_SECTION - match.sectionSpecificVideos.length; - - match.selectedFallbackVideos = match.fallbackVideos - .filter((video) => { - const key = video.videoId; - if (seenClassWideVideos.has(key)) return false; - seenClassWideVideos.add(key); + const seenTopics = new Set(); + const seenClassWideVideos = new Set(); + const topicMatches = []; + + topics.forEach(({ className, category }, topicIndex) => { + const topicKey = `${className}:${category}`; + if (seenTopics.has(topicKey)) return; + seenTopics.add(topicKey); + + const videos = (SUBJECT_VIDEOS[className] || []) + .map((entry, index) => normalizeCuratedVideo(entry, className, index, category)) + .filter(Boolean) + .sort((a, b) => a.matchRank - b.matchRank); + + topicMatches.push({ + topicIndex, + className, + sectionSpecificVideos: videos.filter((video) => video.matchRank === 0), + fallbackVideos: videos.filter((video) => video.matchRank === 1), + selectedFallbackVideos: [], + }); + }); + + [...topicMatches] + .filter(({ sectionSpecificVideos }) => sectionSpecificVideos.length < MIN_CURATED_VIDEOS_PER_SECTION) + .sort((left, right) => left.sectionSpecificVideos.length - right.sectionSpecificVideos.length || left.topicIndex - right.topicIndex) + .forEach((match) => { + const neededFallbackCount = MIN_CURATED_VIDEOS_PER_SECTION - match.sectionSpecificVideos.length; + match.selectedFallbackVideos = match.fallbackVideos.filter((video) => { + const fallbackKey = `${match.className}:${video.videoId}`; + if (seenClassWideVideos.has(fallbackKey)) return false; + seenClassWideVideos.add(fallbackKey); return true; - }) - .slice(0, needed); - }); - + }).slice(0, neededFallbackCount); + }); - return topicMatches - .sort((a, b) => a.topicIndex - b.topicIndex) - .flatMap(({ category, sectionSpecificVideos, selectedFallbackVideos }) => [ + return topicMatches + .sort((left, right) => left.topicIndex - right.topicIndex) + .flatMap(({ sectionSpecificVideos, selectedFallbackVideos }) => [ ...sectionSpecificVideos, - ...selectedFallbackVideos.map((video) => ({ ...video, category })), + ...selectedFallbackVideos, ]); -} - +} \ No newline at end of file From efe404538c6f71ca71e5805bbf3c726b5b029508 Mon Sep 17 00:00:00 2001 From: impxcts Date: Tue, 5 May 2026 21:38:58 -0700 Subject: [PATCH 17/20] surface autosave state to users with real-time status text. This includes: saving, saved x minutes ago, offline pending, etc. --- frontend/src/App.css | 19 ++++++++ frontend/src/components/CreateCheatSheet.jsx | 47 ++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index a0ed7b4..8974299 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -3126,4 +3126,23 @@ three - column responsive 40% { transform: scale(1.04); } 70% { transform: scale(0.97); } 100% { transform: scale(1); } +} + +.save-status { + font-size: 12px; + opacity: 0.75; + margin-left: 10px; + transition: opacity 0.2s ease; +} + +.save-status.saving { + color: #888; +} + +.save-status.offline { + color: #d97706; +} + +.save-status.saved { + color: #16a34a; } \ No newline at end of file diff --git a/frontend/src/components/CreateCheatSheet.jsx b/frontend/src/components/CreateCheatSheet.jsx index d90cf07..5fbd2b6 100644 --- a/frontend/src/components/CreateCheatSheet.jsx +++ b/frontend/src/components/CreateCheatSheet.jsx @@ -234,7 +234,7 @@ function FormulaReorderPanel({ groupedFormulas, onReorderClass, onReorderFormula useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); - const [expandedGroups, setExpandedGroups] = React.useState({}); + const [expandedGroups, setExpandedGroups] = useState({}); const handleDragEnd = (event) => { const { active, over } = event; @@ -267,6 +267,7 @@ function FormulaReorderPanel({ groupedFormulas, onReorderClass, onReorderFormula } }; + const toggleGroup = (className) => { setExpandedGroups(prev => ({ ...prev, [className]: !prev[className] })); }; @@ -1035,6 +1036,8 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS const [rightPanelVisible, setRightPanelVisible] = useState(true); const [panelLayout, setPanelLayout] = useState(() => loadPanelLayout()); const [videoSearchRequest, setVideoSearchRequest] = useState(null); + const [saveStatus, setSaveStatus] = useState('idle'); + const [lastSavedAt, setLastSavedAt] = useState(null); const [classesCollapseSignal, setClassesCollapseSignal] = useState(0); const pendingPanelLayoutRef = useRef(panelLayout); const hasCollapsedLeftPanelOnceRef = useRef(false); @@ -1095,6 +1098,31 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS } }, []); + const getSaveStatusText = () => { + if (saveStatus === 'saving') return 'Saving...'; + if (saveStatus === 'offline') return 'Offline changes pending' + if (saveStatus === 'saved' && lastSavedAt) { + const diff = Date.now() - lastSavedAt; + const minutes = Math.floor(diff / 60000); + if (minutes < 1) return 'Saved just now'; + if (minutes === 1) return 'Saved 1 min ago'; + return `Saved ${minutes} min ago`; + } + return ''; + }; + + useEffect(() => { + if(!initialData) return + if(initialData.title) setTitle(initialData.title); + if (initialData.content){ + handleContentChange(initialData.content); + } + if (initialData.columns) setColumns(initialData.columns); + if(initialData.fontSize) setFontSize(initialData.fontSize); + if (initialData.spacing) setSpacing(initialData.spacing); + if (initialData.margins) setMargins(initialData.margins); + }, [initialData]); + useEffect(() => { const hasCompiledBefore = Boolean(initialData?.compileHistory?.length || pdfBlob || content.trim()); if (hasCompiledBefore) return; @@ -1195,7 +1223,8 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS } lastAutoSavedPdfRef.current = pdfBlob; - + setSaveStatus('saving'); + setLastSavedAt(Date.now()); onSave({ title, content, @@ -1216,11 +1245,18 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS selectedFormulas: getSelectedFormulasList(), compiledAt: new Date().toISOString(), }, - }, false).catch((error) => { + }, false) + .then(() => { + setSaveStatus('saved'); + setLastSavedAt(Date.now()); + }).catch((error) => { console.error('Failed to autosave compiled sheet', error); + setSaveStatus('offline'); }); }, [columns, compileError, content, contentSource, fontSize, getSelectedFormulasList, margins, onSave, pdfBlob, spacing, title]); + + const startResize = useCallback((panel) => (event) => { event.preventDefault(); @@ -1338,7 +1374,7 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS }; const handleSave = async (e) => { - e.preventDefault(); + e?.preventDefault?.(); await onSave({ title, content, @@ -1517,6 +1553,9 @@ const CreateCheatSheet = ({ onSave, onReset, onRestoreSnapshot, initialData, isS
+ + {getSaveStatusText()} +