Skip to content

Web application for newly-accepted learners to build their foundations with JavaScript syntax and core skills.

Notifications You must be signed in to change notification settings

nashville-software-school/foundations-course

Repository files navigation

JavaScript Foundations Course

A comprehensive, interactive learning platform for JavaScript fundamentals, designed with a progressive chapter-based structure to build skills from the ground up.

Table of Contents

Purpose and Overview

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.

Structure: Sections and Chapters

The course is organized into logical sections, each containing multiple chapters.

Sections

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

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.

Testing Chapter Exercises

Current Approach: String Matching

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

Recommended Approach: Function-Based Testing

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"
}

How to Implement Function-Based Tests

  1. Identify the testable variable or result from the student's code
  2. Create a Function constructor that combines:
    • The student's code
    • A return statement for the variable/result you want to test
  3. Execute the function to retrieve the actual value
  4. Test the value against expected results

Example: Testing a Sum Function

// 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"
}

Example: Testing Array Manipulation

// 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"
}

Benefits of Function-Based Testing

  • 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

Deployment Process

This application uses GitHub Actions to automatically deploy to GitHub Pages when changes are pushed to the main branch.

How the Deployment Works

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:

  1. Triggers automatically when code is pushed to the main branch or manually via workflow_dispatch
  2. Sets up the environment by checking out the code and setting up Node.js
  3. Creates environment variables from GitHub repository secrets
  4. Builds the application using Vite
  5. Deploys to GitHub Pages using GitHub's deployment actions

Environment Variables for Deployment

The GitHub Actions workflow creates a .env file at build time with the following variables:

  • VITE_OAUTH_CLIENT_ID: The GitHub OAuth application client ID
  • VITE_OAUTH_CLIENT_SECRET: The GitHub OAuth application client secret
  • VITE_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.

Static Site Considerations

Because GitHub Pages only serves static files, the application uses:

  1. A separate auth proxy server to handle GitHub OAuth token exchange
  2. Client-side routing with React Router
  3. A custom 404.html page to handle direct navigation to routes
  4. Environment variables injected at build time

OAuth Authentication

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.

OAuth Flow Overview

The GitHub OAuth flow works as follows:

  1. User initiates login: Clicks the "Sign in with GitHub" button
  2. Redirect to GitHub: User is redirected to GitHub's authorization page
  3. User authorizes the app: Grants permission to the application
  4. GitHub redirects back: Returns to the application with an authorization code
  5. Code exchange: The application exchanges the code for an access token via proxy server
  6. Fetch user data: The token is used to retrieve the user's GitHub profile
  7. Session created: User is authenticated in the application

Implementation Components

1. Client-Side Authentication Context

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...
}

2. Auth Callback Component

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...
    }
}

3. Auth Proxy Server

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.

Local Development

Local OAuth CORS Proxy

  1. Clone the auth-proxy repo into its own directory.
  2. Run npm install.
  3. 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.
  4. Run npm run start to start the simple CORS Proxy.

Foundations Course

To set up the project for local development:

  1. Clone the repository:

    git clone https://github.com/your-username/foundations-course.git
    cd foundations-course
  2. Install dependencies:

    npm install
  3. 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
    
  4. Start the development server:

    npm run dev
  5. 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/

Contributing

Contributions to the JavaScript Foundations Course are welcome! Here's how you can contribute:

Adding or Modifying Chapters

  1. 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 */]
      }
    }
  2. Import and add the chapter in src/chapters/index.js

Modifying Tests

When creating tests for chapter exercises, use the Function-based testing approach described in the Testing Chapter Exercises section for more robust validation.

Pull Request Process

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

About

Web application for newly-accepted learners to build their foundations with JavaScript syntax and core skills.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •