A comprehensive, interactive learning platform for JavaScript fundamentals, designed with a progressive chapter-based structure to build skills from the ground up.
- Purpose and Overview
- Structure: Sections and Chapters
- Testing Chapter Exercises
- Deployment Process
- OAuth Authentication
- Local Development
- Contributing
The JavaScript Foundations Course is an interactive learning platform designed to teach JavaScript fundamentals through structured, hands-on exercises. This application provides:
- Organized Learning Path: A progressive curriculum that builds skills step-by-step
- Interactive Exercises: Code exercises with real-time feedback
- Authentication: GitHub integration for tracking progress
- Modern Interface: Clean, distraction-free learning environment
The platform guides learners from basic JavaScript concepts through more advanced topics like functions, arrays, objects, and modules, with each concept building on previous knowledge.
The course is organized into logical sections, each containing multiple chapters.
Sections represent major topic areas in JavaScript:
// From src/sections/index.js
export const sections = [
{
id: 'getting-started',
title: 'Getting Started',
description: 'Essential setup steps to begin your learning journey'
},
{
id: 'variables-and-values',
title: 'Variables and Values',
description: 'Understanding data storage and manipulation in JavaScript'
},
// more sections...
]
Each section groups related chapters together and provides a high-level organization of the curriculum.
Chapters are individual lessons within a section:
// Example chapter object
{
id: 'arrays-intro',
title: 'Introduction',
path: '/arrays-intro',
sectionId: 'arrays',
previousChapterId: null,
nextChapterId: 'arrays-indices',
content: `## Arrays (a.k.a. collections of things)...`,
exercise: {
starterCode: `// Example starter code...`,
solution: `// Example solution...`,
tests: [/* tests */]
}
}
Each chapter includes:
- Content: Markdown-formatted instructional material
- Exercise: Interactive coding challenge with starter code
- Solution: Reference implementation for the exercise
- Tests: Automated tests to validate the learner's solution
- Navigation: Links to previous and next chapters
Chapters are organized in a sequence, with each chapter building on knowledge from previous ones. The application dynamically loads and displays chapters as the user navigates through the course.
Currently, the application uses string matching to test student code:
// Example of current string-matching test
{
name: "Array Creation",
test: (code) => code.includes('["Banana", "Orange", "Apple", "Watermelon", "Blueberry"]'),
message: "Make sure you've included all fruits in the array in the correct order"
}
While this approach is simple, it has limitations:
- It doesn't test actual functionality
- It's sensitive to formatting and whitespace
- It may falsely pass or fail based on string matches that don't reflect code behavior
A more robust approach is to use the Function constructor to evaluate the student's code and test the actual functionality:
// Example of Function-based test
{
name: "Array Creation",
test: (code) => {
try {
// Combine the student code with test code that returns the variable we want to check
const testFunction = new Function(`
${code}
return fruits;
`);
// Execute the function and get the actual value
const result = testFunction();
// Test the value for correctness
return Array.isArray(result) &&
result.length === 5 &&
result[0] === "Banana" &&
result[1] === "Orange" &&
result[2] === "Apple" &&
result[3] === "Watermelon" &&
result[4] === "Blueberry";
} catch (error) {
console.error("Test error:", error);
return false;
}
},
message: "Make sure you've created an array with all the fruits in the correct order"
}
- Identify the testable variable or result from the student's code
- Create a Function constructor that combines:
- The student's code
- A return statement for the variable/result you want to test
- Execute the function to retrieve the actual value
- Test the value against expected results
// Chapter exercise: Implement a function that adds two numbers
{
name: "Sum Function",
test: (code) => {
try {
const testFunction = new Function(`
${code}
// Test with various inputs
return {
test1: sum(5, 3),
test2: sum(0, 0),
test3: sum(-1, 1)
};
`);
const results = testFunction();
return results.test1 === 8 &&
results.test2 === 0 &&
results.test3 === 0;
} catch (error) {
return false;
}
},
message: "Your sum function should correctly add two numbers"
}
// Chapter exercise: Filter even numbers from an array
{
name: "Even Numbers Filter",
test: (code) => {
try {
const testFunction = new Function(`
${code}
return filterEvenNumbers([1, 2, 3, 4, 5, 6]);
`);
const result = testFunction();
return Array.isArray(result) &&
result.length === 3 &&
result.every(num => num % 2 === 0);
} catch (error) {
return false;
}
},
message: "Your function should return an array with only even numbers"
}
- Tests actual code behavior rather than syntax
- Allows multiple test cases within a single test
- Doesn't depend on specific formatting or code style
- Provides more accurate feedback to students
- Allows testing complex logic and edge cases
This application uses GitHub Actions to automatically deploy to GitHub Pages when changes are pushed to the main branch.
The deployment process is defined in .github/workflows/deploy.yml
:
name: Deploy to GitHub Pages
on:
push:
branches: ["main"]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
# Checkout code, setup Node, etc.
- name: Create env file
run: |
echo "VITE_OAUTH_CLIENT_ID=${{ secrets.OAUTH_CLIENT_ID }}" > .env
echo "VITE_OAUTH_CLIENT_SECRET=${{ secrets.OAUTH_CLIENT_SECRET }}" >> .env
echo "VITE_PROXY_DOMAIN=https://authproxy.nss.team" >> .env
# Install dependencies, build, deploy
The workflow does the following:
- Triggers automatically when code is pushed to the main branch or manually via workflow_dispatch
- Sets up the environment by checking out the code and setting up Node.js
- Creates environment variables from GitHub repository secrets
- Builds the application using Vite
- Deploys to GitHub Pages using GitHub's deployment actions
The GitHub Actions workflow creates a .env
file at build time with the following variables:
VITE_OAUTH_CLIENT_ID
: The GitHub OAuth application client IDVITE_OAUTH_CLIENT_SECRET
: The GitHub OAuth application client secretVITE_PROXY_DOMAIN
: The domain where the auth proxy server is hosted
These variables are stored as secrets in the GitHub repository settings and are only accessible during the build process, ensuring secure handling of sensitive credentials.
Because GitHub Pages only serves static files, the application uses:
- A separate auth proxy server to handle GitHub OAuth token exchange
- Client-side routing with React Router
- A custom 404.html page to handle direct navigation to routes
- Environment variables injected at build time
This application uses GitHub OAuth for authentication, allowing users to sign in with their GitHub accounts. This provides a seamless authentication experience and enables tracking progress across sessions.
The GitHub OAuth flow works as follows:
- User initiates login: Clicks the "Sign in with GitHub" button
- Redirect to GitHub: User is redirected to GitHub's authorization page
- User authorizes the app: Grants permission to the application
- GitHub redirects back: Returns to the application with an authorization code
- Code exchange: The application exchanges the code for an access token via proxy server
- Fetch user data: The token is used to retrieve the user's GitHub profile
- Session created: User is authenticated in the application
The AuthContext.jsx
provides authentication state and methods:
// From src/context/AuthContext.jsx
export const AuthProvider = ({ children }) => {
// Authentication state
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [user, setUser] = useState(null)
// Login method redirects to GitHub OAuth
const login = () => {
const clientId = import.meta.env.VITE_OAUTH_CLIENT_ID
const redirectUri = `${window.location.origin}${basePath}/auth`
// Generate state parameter for security
const state = Math.random().toString(36).substring(2, 15)
sessionStorage.setItem('oauth_state', state)
// Redirect to GitHub authorization page
window.location.href = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=read:user&state=${state}`
}
// Other authentication methods...
}
The AuthCallback.jsx
component handles the OAuth redirect and token exchange:
// From src/components/AuthCallback.jsx
const handleAuth = async () => {
// Get code and state from URL params
const code = params.get('code')
const state = params.get('state')
// Verify state parameter
if (state !== storedState) {
setError('Security error: Invalid state parameter')
return
}
try {
// Get proxy domain from environment variables
const proxyDomain = import.meta.env.VITE_PROXY_DOMAIN
// Exchange code for token via proxy server
const response = await fetch(`${proxyDomain}/oauth/github/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
redirect_uri: `${window.location.origin}${basePath}/auth`
})
})
const data = await response.json()
// Store token and fetch user data
localStorage.setItem('github_token', data.access_token)
await fetchUserData(data.access_token)
} catch (error) {
// Handle errors...
}
}
Since GitHub Pages only supports static content, a separate server handles the OAuth token exchange:
- Runs on a separate domain (configured as VITE_PROXY_DOMAIN)
- Securely stores OAuth client secret
- Handles the GitHub API request to exchange authorization code for access token
- Returns the token to the client application
This architecture separates sensitive credentials from the client-side code while maintaining a seamless authentication experience.
- Clone the auth-proxy repo into its own directory.
- Run
npm install
. - Follow the instruction in that README to set up the local
.env
file with the Github OAuth app info and port number (e.g. 3003). Allow origin will be the local domain of the client app. - Run
npm run start
to start the simple CORS Proxy.
To set up the project for local development:
-
Clone the repository:
git clone https://github.com/your-username/foundations-course.git cd foundations-course
-
Install dependencies:
npm install
-
Create environment variables: Create a
.env.local
file in the project root:VITE_OAUTH_CLIENT_ID=your_github_oauth_client_id VITE_PROXY_DOMAIN=http://localhost:3003
-
Start the development server:
npm run dev
-
Start the auth proxy server (in a separate terminal):
cd path/to/auth-proxy npm install npm start
The application will be available at http://localhost:5173/foundations-course/
Contributions to the JavaScript Foundations Course are welcome! Here's how you can contribute:
-
Create a new chapter file in
src/chapters/
following the existing pattern:export const newChapter = { id: 'unique-id', title: 'Chapter Title', path: '/unique-id', sectionId: 'section-id', previousChapterId: 'previous-chapter', nextChapterId: 'next-chapter', content: `## Markdown Content...`, exercise: { starterCode: `// Starter code...`, solution: `// Solution...`, tests: [/* tests */] } }
-
Import and add the chapter in
src/chapters/index.js
When creating tests for chapter exercises, use the Function-based testing approach described in the Testing Chapter Exercises section for more robust validation.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request