This application counts down how many days there are until a specific date. It can calculate a date from the past, future, or even tell you that today is the date you are looking for. This code was made by setting up the constants for the event name, date, and the message. It then verifies that they entered a date, and then calculates the exact number of days to the date.
Go to M01 Exercise: Strings and DatesThis exercise is the game of Pig. The player clicks a roll button to roll a die and can choose to roll again or hold their turn. The first player to reach 50 points wins.
Go to M01 Exercise: PIGIn this exercise, we set up a way to swap images on a webpage using JavaScript. We also disabled the normal interaction so you can see it change on the page, rather than it bringing you to a separate page.
Go to M02 Exercise: Image SwapIn this exercise, I learned how Node.js modules work by splitting code into seperate files as we went. It was kind of cool actually seeing it build up over a few files.
It starts with m02_1.js which is the simplest version. There's two message variables (mess1 and mess2) and a function called parag that takes a string and logs it as a paragraph. Nothing crazy going on.
Then m02_2.js builds on that by putting both messages into an array and using a while loop to run through them. There's also a ternary at the end that checks how many statements ran and logs whether its a short or long statement.
Then m02_app.js, m02_func.js, and m02_messages.js are where the real point of this exercise comes in. Instead of having everything stuffed in one file, the messages got moved to m02_messages.js and exported as an array with module.exports. The parag function got moved to m02_func.js and exported the same way. Then m02_app.js pulls them both in with require and runs the same logic.
So we went from one file doing everything to three different files that all talk to each other through require and module.exports. Kind of like how in a game studio they don't have one guy doing the art AND the code AND the audio. They split it up. Same idea here I guess.
This is a pythagorean theorem calculator I built in Node. You run it in the terminal and pass two numbers for side A and side B, like node script.js 3 4. Way more efficient than doing it by hand on a test, which is exactly what I'm gonna remember during my next math final.
The script grabs the two numbers from the command line with process.argv and converts them from strings into actual numbers with parseFloat. There's also a quick isNaN check so the script doesn't fall apart if you try to pass something like "banana", "potato", or "The Massachusetts Insitute of Technology" instead of a number.
Then it does the actual math. It squares both sides with Math.pow, adds them together, and uses Math.sqrt to find side C. The .toFixed(2) at the end just rounds it to two decimal places so it doesnt look gross.
After it has the answer, it uses the fs module to write the result into a text file called result.txt, then reads it back with fs.readFileSync and logs it to the console. So you get the answer both saved as a file AND printed in the terminal.
Pretty much that's it. Pass two sides, get the hypotenuse, save it to a file. Math homework solver but make it Node.
In this exercise, we went ahead and made a form that lets you enter your information before taking you to a successful registration form after submission. The point of this exercise was to show us data validation and how we can filter data to be to our liking we receive from the user before using it.
Go to M03 Exercise: Data Validation
In this exercise, I went and set up a node.js package from scratch using npm. The whole point was to learn how packages actually get built. Pretty cool stuff.
I started by running npm init which generates the package.json file. That file is basically the brain of the whole project, it holds the name, version, description, scripts, and all the dependencies the project needs to run.
For dependencies I used two. The first was lodash, which is a JavaScript utility library that gives you a bunch of helper functions for working with arrays and objects and stuff. In my index.js I used _.intersection() which takes two arrays of flowers and finds the ones that appear in both. So if array01 has Poppy and Hyacinth in it, and array02 also has Poppy and Hyacinth, it returns those two. The second was sass, which is a CSS preprocessor that lets you write cleaner stylesheets with variables and nesting and other stuff that vanilla CSS doesn't have. It compiles down to a normal CSS file when you build it.
I also added nodemon as a dev dependency with npm i nodemon -D. Nodemon watches your files and auto restarts the server whenever you save changes, so you dont have to keep killing the server and running node index.js a thousand times manually.
In the package.json I set up a few scripts. npm start runs the index.js. npm run dev runs it with nodemon. npm run mysass compiles the scss into css. npm run build watches the scss and auto compiles whenever you save it.
The files in this one are index.js for the lodash logic, mystyle.scss for the sass styles, mystyle.css for the compiled css output, and index.html which just links to the css to actually show the styles working on a page.
In this exercise, I built a basic web server from scratch using Node and the http module. No frameworks, no shortcuts, just raw http handling. Felt like building a car out of cardboard but it works.
The main file is m04_index.js. It starts off by using fs.readFileSync to load all my files into memory. The html pages, the css file, and a banner image of a forest. Then it creates a server with http.createServer which listens for requests on port 8080.
Inside the server is a bunch of if/else statements that check what url the user is visiting. If they go to "/" they get the home page. If they go to "/about" they get the about page. If they request main.css or the banner image, it sends those back with the right content-type header so the browser knows what its dealing with. And if they go to anything that doesnt match, they get hit with a 404 page not found message.
The writeHead part matters a lot because it tells the browser what kind of file its getting back. text/html for html pages, text/css for stylesheets, image/x-png for the banner. Without that the browser would just be confused about what to do with the response.
I also added nodemon as a dev dependency again because manually restarting the server every time you change a file is mind numbing.
This was a cool exercise because it showed how a web server actually works at the lowest level before we move on to express which does all of this routing stuff for you automatically.
// importing http and fs modules
const http = require('http');
const {readFileSync} = require('fs');
// loading all files into memory
const indexpg = readFileSync('./indexbanner/index.html');
const aboutpg = readFileSync('./indexbanner/about.html');
const mainsty = readFileSync('./indexbanner/main.css');
const banimg = readFileSync('./indexbanner/banner_forest.png');
// creating the server
const server = http.createServer((req,res)=>{
const url = req.url;
// routing based on the url
if(url === '/'){
res.writeHead(200,{'content-type':'text/html'});
res.write(indexpg);
res.end();
} else if(url === '/main.css'){
res.writeHead(200,{'content-type':'text/css'});
res.write(mainsty);
res.end();
} else if(url === '/about'){
res.writeHead(200,{'content-type':'text/html'});
res.write(aboutpg);
res.end();
} else {
// 404 for anything else
res.writeHead(404,{'content-type':'text/html'});
res.write('<h1>Oops! Page not found.</h1>');
res.end();
}
});
// starts the server on port 8080
server.listen(8080);
In this exercise, I rebuilt the http server from the previous exercise but using express.js this time. Express handles a lot of the boring stuff for you so you dont have to write giant if/else trees for every single url like before.
Express is a framework that sits on top of node and makes routing and serving files way easier. I installed it through npm as a dependency.
The main file is AJohnson_app.js. It sets up an express server and uses app.get() to define routes. "/" serves the home page, "/about" serves the about page, and then there's a few api routes around a rock paper scissors theme.
The data for the rock paper scissors stuff lives in data.js which exports an array of objects. Each object has an id, name, strength, weakness, element, and description. The api routes like /api/rock and /api/paper use items.find() to look up the matching object by id and send it back as json.
There's also a /api/info route that returns the whole items array at once. And at the bottom there's a catch-all that sends a 404 if you go to a url that doesnt exist.
The big quality of life upgrade with express is express.static. It handles serving css and images automatically. So instead of writing seperate routes for every file like in the http exercise, you just point it at a folder and let express do the work. Easily worth the install.
// importing express and path modules, plus our data
const express = require('express');
const app = express();
const path = require('path');
const {items} = require('./data');
// serves static files like css and images
app.use(express.static('./indexbanner'));
// home page route
app.get('/',(req, res)=>{
res.status(200).sendFile(path.resolve(__dirname, 'indexbanner/index.html'));
});
// about page route
app.get('/about',(req,res)=> {
res.status(200).sendFile(path.resolve(__dirname, './indexbanner/about.html'));
});
// returns all items as json
app.get('/api/info',(req,res)=> {
res.status(200).json({items});
});
// individual item routes that find by id
app.get('/api/paper', (req,res)=>{
const singleItem = items.find((item)=>item.id === Number(1));
res.status(200).json(singleItem);
});
app.get('/api/rock', (req,res)=>{
const singleItem = items.find((item)=>item.id === Number(2));
res.status(200).json(singleItem);
});
app.get('/api/scissors', (req,res)=>{
const singleItem = items.find((item)=>item.id === Number(3));
res.status(200).json(singleItem);
});
app.get('/api/cleaver', (req,res)=>{
const singleItem = items.find((item)=>item.id === Number(4));
res.status(200).json(singleItem);
});
// 404 catch-all for anything that doesnt match
app.use((req,res)=>{
res.status(404).send('Resource Not Found');
});
// starts server on port 8080
app.listen(8080);
In this exercise, we learned how to set up a list that can store the tasks that the user puts into the text box. This way, you can keep your tasks stored long term and delete them when you no longer need them.
Go to M05 Exercise: Web Storage
In this exercise, I built on the express stuff from last time by adding dynamic routes and query parameters. Now I dont have to manually hardcode a route for every single item which is a huge improvment.
The home page is just a title and a link to the api. /api/items returns all the items but uses .map() to only send back the name, description, strength, and weakness instead of dumping every property. Keeps the response cleaner.
The cool upgrade here is /api/items/:itemID which uses a route parameter. Instead of writing seperate routes for rock, paper, scissors, and cleaver like I did last time, this one route handles all of them. Whatever number you put in the url gets grabbed with req.params and used to find the matching item by id. Way better.
Theres also a /query route that lets you search and filter the data. You can pass ?search=slicing to find items with "slicing" in the description, or ?limit=2 to only get the first two results, or both at the same time. If nothing matches what you searched for, it returns a message saying no items were found.
The data.js is the same rock paper scissors data from last time. Just an array of objects with id, name, strength, weakness, element, and description.
const express = require('express');
const app = express();
const {items} = require('./data');
// home page with link to the api
app.get('/', (req,res)=>{
res.send('<h1>Rock, Paper, Scissors Ring</h1><a href="/api/items">Available Items</a>');
});
// returns all items but only specific fields
app.get('/api/items', (req,res)=>{
const newListing = items.map((item)=>{
const {name, desc, strength, weakness} = item;
return {name, desc, strength, weakness}
})
res.json(newListing);
});
// dynamic route that finds an item by its id
app.get('/api/items/:itemID', (req, res)=>{
const {itemID} = req.params;
const singleItem = items.find((item)=>item.id === Number(itemID));
res.json(singleItem);
});
// query route with search and limit parameters
app.get('/query',(req,res)=>{
const {search, limit} = req.query;
let sorted = [...items];
if(search){
sorted = sorted.filter((item)=>{
return item.desc.match(search);
})
}
if(limit){
sorted = sorted.slice(0,Number(limit))
}
if(sorted.length < 1){
return res.status(200).json('No items matching your query were found.');
}
res.status(200).json(sorted);
});
// starts server on port 8080
app.listen(8080);
In this exercise, we linked to the pokeAPI and fetched the name, sprite, and type of a specific pokemon. We also fetched the moves and titles of the abilities. We displayed them for three different pokemons and they all have their unique names and abilities. Feel free to check all three out! The three are charizard, bulbasaur, and squirtle!
Go to M06 Exercise: API PageIn this exercise, I learned how to chain jQuery animations to scroll events so the page actually responds when you move around on it. The header has a big flower background with a title and intro text that fades out as soon as you scroll down past 100px. At the same time, a white bar shoots across the screen using .animate() to change its width from 0px to 100%. Then once you pass 200px, the main content fades in AND slides up at the same time using the queue: false option so they happen together instead of waiting in line. Scroll back to the top and it all reverses itself. There's also a little easter egg footer in the corner you can click on that pops open with a different message.
Go to M07 Exercise: Motion PageIn this exercise, I added two jQuery plugins to a basic page that started out pretty boring. The first is parallax.js from pixelcog, which makes background images stay in place while you scroll past them so the page feels like it has depth. I dropped in three parallax windows between the content sections and picked different images from the images folder for each one (a moorea beach, a night scene, and space). The second plugin is jQuery UI with a fully custom theme I built on Themeroller. I changed five things on the theme: the main font got bumped to bold at 1.3em, the corner radius went up to 15px, the headers got a gray background, the highlight got changed to a green with a black border and white text, and the modal overlay screen got set to a dark blue. Then I dropped in a datepicker widget tied to an arrival date field so you can pick a date from a calendar instead of typing it in. Heads up that the parallax effect doesn't work right on mobile, so phones just get static backgrounds.
Go to M07 Exercise: jQuery Plugins
In this exercise, I learned how the location and history browser objects work in javascript. They handle the navigation stuff that you would normally use anchor tags for.
The site starts at index.html which has two buttons. Enter takes you to the main page, Tutorial takes you through a three page walkthrough. Instead of using anchor tags, the buttons change the location object directly with location = "somepage.html" which gets the same result as clicking a link.
main.html has a pretty flower picture and a back button. It also prints out history.length so you can see how many pages are stacked up behind you. The back button uses history.back() which is the exact same thing your browsers back arrow does.
The tutorial pages are where it gets interesting. At first they used regular location changes which means every page you visit gets added to history. So if you click through the whole tutorial and then try to hit back a few times to get out, you have to walk all the way back through every page you visited first. Super annoying.
The fix is location.replace() instead of location =. Replace swaps out the current page in history rather than adding a new entry. So no matter how many times you go forward and back through the tutorial pages, your history only ever has the two entries it needs. Way cleaner.
So location is for navigating to pages and history is for tracking where you've been. Replace is for when you dont want to clog up the back button with stuff the user wont care about going back to.
In this exercise, I built a career path explorer for Butler that uses delayed animations to transition between pages. The home page has six career cards (video game developer, graphic designer, web designer, animator, social media specialist, level designer) that each link to one of three program pages. The animation chain is the cool part. When you click a card, it first plays a jello-horizontal animation that squishes and stretches the card, then 300ms later the whole main element starts a slide-out-top animation that flies it off the screen, and then 1000ms after the original click the page actually changes. The new page lands with a bounce-in-top animation so it feels like it's dropping in from above. Same thing happens with the nav links. The animations are from Animista and the layout uses Bootstrap 4.6 with Font Awesome icons. The whole point was using setTimeout to stagger the animations so they don't all fire at once.
Go to M08 Exercise: Delayed Animation
In this exercise, I learned how eventemitters work in node. Specifically how you can hook multiple listeners onto the same event and pass arguments through them.
The script imports the events module and creates a customEmitter. Then it binds two seperate listeners to the same event name 'mess'. When emit('mess', 6) fires, both listeners run one after the other.
The first listener takes radius as an argument and calculates the circumference of a circle (2 * pi * radius). I used .toFixed(2) to round the answer to two decimal places. There's also an if/else check with isNaN that catches anyone trying to pass something like "banana" instead of an actual number.
The second listener uses the readline module to ask the user a question in the terminal. It pops up "What?" and waits for input, then logs back whatever you typed. rl.close() is at the end so the script actually stops when you're done instead of just sitting there waiting on you forever like an awkward conversation.
So the order of operations is: you emit the event with a number, the first listener spits out the circumference, then the second listener asks you a question and prints your answer back. Two listeners. One event. One emit call.
//ajohnson
const eventEmitter = require('events');
const customEmitter = new eventEmitter();
const readline = require('readline');
const rl = readline.createInterface(process.stdin, process.stdout);
customEmitter.on('mess', (radius) => {
if (isNaN(radius)) {
console.log('Sorry, couldn\'t figure out the circumference. \nPlease enter a real number next time.');
} else {
let circRaw = 2 * Math.PI * radius;
let circ = circRaw.toFixed(2);
console.log(`The circumference is ${circ} units.`);
}
});
customEmitter.on('mess', ()=> {
rl.question('What?', (ans)=>{
console.log('Data received.');
console.log(`Your answer is ${ans}.`);
rl.close();
});
});
customEmitter.emit('mess', 6);
In this exercise, I got my first taste of TypeScript, which is basically JavaScript but with type checking. Browsers can't run .ts files directly so the package.json has a test script that runs "tsc index" to compile it down to a .js file first, then runs it with node.
The first part is just primitive types. TypeScript can figure out on its own that sport is a string and id is a number based on what you assign to it, but you can also be explicit and write let id:number if you want. If you try to assign a string to a number variable later, it yells at you with red squiggles before the code even runs. Honestly thats more helpful than waiting til it crashes mid run.
Then there's union types, which let a variable be more than one type. let confirmed: string | boolean means it can be either of those, but if you tried to set it to a number 7 it would throw an error. You can also do union stuff with arrays so each index sticks to its assigned type.
Reference types are next. When you make an object and pull a property out into another variable, that variable references the same memory spot instead of getting its own copy. The example checks if origValue.val01 equals refPoint and prints "They're equal" because they point to the same value.
Type aliases are actually pretty useful. You can build your own custom types out of the primitive ones. I made one called myAlias that allows string OR number. Then I made itemObj which is an object template with title (string), serial (myAlias so it can be either), and inventory (number). Then I made two items, one with a number serial and one with a string serial, and both pass the type check.
At the end there's a function called buy() that takes cart:number and item:string and returns a string. It checks how many you're trying to buy against the inventory of itemA or itemB, and either tells you how many are left or tells you off for trying to buy too many. Typing the parameters AND the return value is the whole reason TypeScript exists. You always know exactly what goes in and what comes out. No guesswork.
let sport = 'football';
let id = 5;
console.log(sport);
let confirmed: string | boolean = 'yes';
confirmed = '7';
let person = [false, 'John', 'Plumber'];
person[0] = true;
console.log(`${person[1]} is a ${person[2]}`);
let origValue = {val01: 1, val02: 2};
let refPoint = origValue.val02;
if (origValue.val01 == refPoint) {
console.log('They\'re equal.');
} else {
console.log('Not even close');
}
type myAlias = string | number;
type itemObj = {
title: string;
serial: myAlias;
inventory: number;
}
const itemA: itemObj = {
title: 'Box',
serial: 234,
inventory: 4
}
const itemB: itemObj = {
title: 'Bowl',
serial: '2er45',
inventory: 3
}
console.log(itemA.title);
function buy(cart:number, item:string):string {
let theMess = '';
if (item == 'a' || item == 'A') {
if (cart > itemA.inventory){
theMess = `You can't buy that many of the ${itemA.title}`;
} else {
theMess = `There are ${itemA.inventory-cart} of ${itemA.title}`;
}
} else {
if (cart > itemB.inventory) {
theMess = `You can't buy that many of the ${itemB.title}`;
} else {
theMess = `There are ${itemB.inventory-cart} of ${itemB.title}`;
}
}
return theMess;
}
console.log(buy(2, 'a'));
console.log(buy(5, 'b'));
In this exercise, I built my very first React app using Vite. It is just a simple page with a header, a hello message, the current date, and a footer, but the point was learning how to split everything into separate components and pass data between them with props. The Header and Footer both take a myTitle prop from App.jsx and use it instead of hardcoded text, so if I want to change the app name in one place, it updates everywhere. The Footer also auto-generates the copyright year using new Date().getFullYear() and the CurrentDate component formats today's date with toLocaleDateString(). Then I compiled it all with npm run build which spits out a dist folder that can actually be uploaded to a live site. This was where the whole "components are just lego pieces you snap together" idea finally clicked for me.
Go to M10 Exercise: My First React
In this exercise, I learned how React state works by building a playlist app. You can view songs, add new ones, mark favorites, and delete them, and the page updates instantly without a refresh because of useState. Pretty slick.
App.jsx is the main file where all the state lives. Theres two pieces of state, playlist (the actual array of songs) and showForm (a boolean that decides whether you see the Add Song button or the form). The initial songs array has a few of my picks loaded in by default. Espresso by Sabrina Carpenter, Electric Feel by MGMT, Come As You Are by Nirvana, and Dancing Queen by ABBA which is marked as a favorite because Mom likes it.
The add song logic was the part that took me the longest to wrap my head around. When you submit the form, it runs through the existing playlist with .reduce() to find the highest existing id, adds 1 to it, and assigns that to the new song. So no matter how much you add and delete, no two songs ever end up sharing an id. Neat little trick.
Delete works the opposite way using .filter(). When you click delete on a song, it takes that song's id and filters the array to keep only the songs whose ids dont match. Simple.
The Add button uses a ternary that swaps between showing the button OR the form depending on the showForm state. Clicking Add Song flips it to true and the form pops in. Cancel or submit flips it back to false. The SongForm has its own internal state for the title, artist, year, and favorite fields with onChange handlers tracking every keystroke. preventDefault() on submit stops the page from refreshing when you hit the button.
Props pass data everywhere. The app title gets passed to the Header and Footer, the songs array gets passed to the Playlist, the event handlers get passed wherever they need to fire. Data flows down, actions bubble up. This was the exercise where React finally clicked for me.
// import the useState hook
import { useState } from 'react';
// import the components for the app
import Header from './components/Header';
import Playlist from './components/Playlist';
import SongForm from './components/SongForm';
import Footer from './components/Footer';
import './App.css';
// app name to display in the header and footer
const appTitle = "Alex's Playlist";
// array of songs to display on load
const initalSongs = [
{ id: 1, title: "Espresso", artist: "Sabrina Carpenter", year: 2024 },
{ id: 2, title: "Electric Feel", artist: "MGMT", year: 2007 },
{ id: 3, title: "Come As You Are", artist: "Nirvana", year: 1991 },
{ id: 4, title: "Dancing Queen", artist: "ABBA", year: 1976, fave: true },
];
const App = () => {
// state to track the playlist and whether to show the add song form
const [playlist, setPlaylist] = useState(initalSongs);
const [showForm, setShowForm] = useState(false);
// event handlers to show/hide the form
const handleShowForm = () => setShowForm(true);
const handleHideForm = () => setShowForm(false);
// event handler to add a new song to the playlist
const handleAddSong = (newSong) => {
setPlaylist((prev) => {
const maxId = prev.reduce((max, song) => (song.id > max ? song.id : max), 0);
return [...prev, { ...newSong, id: maxId + 1 }];
});
setShowForm(false);
};
// event handler to delete a song from the playlist
const handleDeleteSong = (id) => {
setPlaylist((prev) => prev.filter((song) => song.id !== id));
};
return (
<div className="container">
<Header appName={appTitle}>
<p>Favorite songs marked with a star</p>
</Header>
<main className="main-content">
<Playlist songs={playlist} onDelete={handleDeleteSong} />
{showForm ? (
<SongForm onAdd={handleAddSong} onCancel={handleHideForm}/>
):(
<button type="button" onClick={handleShowForm}>Add Song</button>
)}
</main>
<Footer appName={appTitle}/>
</div>
);
};
export default App;
In this exercise, I took the playlist app from last week and made it way better. The add song form now floats in a modal instead of just sitting at the bottom of the page, and the songs dont wipe themselves every time you refresh the page anymore. Both fixes were done by writing custom hooks.
The first one is useDialog. It sits in a new hooks folder and basically wraps around the native HTML <dialog> element. It uses useRef to grab a reference to the actual dialog DOM node, then gives back two functions, openDialog() and closeDialog(), that call .showModal() and .close() on the ref. Then any component that wants a modal just imports useDialog, pulls out the ref and the two functions, and sticks the ref onto its dialog tag. Reusable.
Both App.jsx and Playlist.jsx use it. App uses it for the add song form, so clicking Add Song pops the modal open and submit or cancel closes it. Playlist uses it for the delete confirmation. When you click delete on a song, it sets that song into local state and opens the dialog. The modal shows "Delete [title] by [artist]?" and gives you confirm or cancel buttons. Confirm runs the delete handler, cancel nulls out the song state and closes the modal.
The second hook is useLocalStorage. It acts like useState but stores everything to localStorage in the background. When you initialize it, the lazy initializer function checks localStorage for the key first. If theres something there it parses it and uses it, if not it falls back to the initial value. I wrapped it in a try/catch because JSON.parse can blow up on garbage data and I dont want my whole app to crash from one bad entry. Then useEffect runs every time the value changes and writes the new state back to localStorage.
Swapping useState for useLocalStorage in App.jsx only took changing one line. const [playlist, setPlaylist] = useLocalStorage("songs", initialSongs). Everything else just works the same. Add a song, refresh the page, the song is still there. Delete one, refresh, its still gone. Custom hooks are basically write once use forever.
// useDialog.js
import { useRef } from "react";
export const useDialog = () => {
// ref to access dialog
const dialogRef = useRef(null);
// open function
const openDialog = () => dialogRef.current.showModal();
// close function
const closeDialog = () => dialogRef.current.close();
return { dialogRef, openDialog, closeDialog };
};
// useLocalStorage.js
import { useState, useEffect } from "react";
export const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Error writing localStorage key "${key}":`, error);
}
}, [key, value]);
return [value, setValue];
};
// App.jsx usage
import { useDialog } from './hooks/useDialog';
import { useLocalStorage } from './hooks/useLocalStorage';
const App = () => {
const [playlist, setPlaylist] = useLocalStorage("songs", initialSongs);
const { dialogRef, openDialog, closeDialog } = useDialog();
const handleAddSong = (newSong) => {
setPlaylist((prev) => {
const maxId = prev.reduce((max, s) => (s.id > max ? s.id : max), 0);
return [...prev, { ...newSong, id: maxId + 1 }];
});
closeDialog();
};
return (
<div className="container">
<button onClick={openDialog}>Add Song</button>
<dialog ref={dialogRef}>
<SongForm onAdd={handleAddSong} onCancel={closeDialog}/>
</dialog>
</div>
);
};
In this exercise, I learned about middleware in express and then refactored everything into an MVC pattern. Started small and added more stuff as it went.
The first part is just basic middleware. Middleware functions are functions that run between the request and the response. They take three arguments, req, res, and next. The next() call is what hands control off to the next thing in line. If you forget to call next() your server just sits there forever and never responds. Learned that one the hard way.
logger.js is the simplest example. It just console.logs "Logged" and calls next(). Thats it. It gets attached to the home route so every time someone visits the home page, the console prints "Logged" before the response sends.
dater.js is where middleware actually does something useful. It creates a date object, formats it as dd/mm/yyyy with zero padding for single digit days and months, and attaches the formatted date to the request object as req.formattedToday before calling next(). The thing you have to know is that you cant just declare a regular variable inside middleware and expect the next function to see it. You have to stick it onto the req object because thats whats getting passed down the chain. Then the home route reads req.formattedToday in its response and sends back something like "Welcome! Today's date is 05/11/2026" or whatever the date is when you load it.
Order matters with middleware too. app.get('/', dater, logger, callback) means dater runs first, then logger, then the actual response. If you put logger before dater, the log fires before the date is set up.
Then theres the /info and /info/:id routes. /info dumps the whole items array as json. /info/:id uses a route parameter to grab whichever id you put in the url, finds the matching item with items.find(), and returns just that one. Same rock paper scissors and cleaver data from earlier exercises.
The second half of the exercise was refactoring everything into MVC. Instead of having one giant index.js doing everything, the code gets split up across files. dater.js and logger.js live at the root level. A routes folder gets routeInfo.js which uses express.Router() to bundle the /info routes together. A controller folder gets infoCon.js which holds the actual logic for getInfo and selectSingle. So when you hit /info, the request goes index.js to routes/routeInfo.js to controller/infoCon.js. Way cleaner. index.js doesn't have to know HOW the info routes work, just that they exist.
The dater middleware also got upgraded to app.use(dater) so it runs on every request, not just the home page. Logger stays on the home route so it only fires there. Thats the difference between global middleware and route-specific middleware.
// index.js
const express = require('express');
const app = express();
const dater = require('./dater.js');
const logger = require('./logger.js');
const info = require('./routes/routeInfo.js');
app.use('/info', info);
app.get('/', dater, logger, (req, res) => {
return res.status(200).send(`Welcome! Today's date is ${req.formattedToday}.`);
});
app.listen(8080, () => {
console.log('Server running at http://localhost:8080/');
});
// dater.js (middleware that attaches a formatted date to the request)
const dater = (req, res, next) => {
const today = new Date();
const yyyy = today.getFullYear();
let mm = today.getMonth() + 1;
let dd = today.getDate();
if (dd < 10) dd = '0' + dd;
if (mm < 10) mm = '0' + mm;
req.formattedToday = dd + '/' + mm + '/' + yyyy;
next();
};
module.exports = dater;
// logger.js (middleware that logs every request to the console)
const logger = (req, res, next) => {
console.log('Logged');
next();
};
module.exports = logger;
// routes/routeInfo.js (groups all /info routes using express.Router)
const express = require('express');
const router = express.Router();
const { getInfo, selectSingle } = require('../controller/infoCon');
router.get('/', getInfo);
router.get('/:id', selectSingle);
module.exports = router;
// controller/infoCon.js (the actual logic for the info routes)
const { items } = require('../data');
const getInfo = (req, res) => {
res.status(200).json({ items });
};
const selectSingle = (req, res) => {
const { id } = req.params;
const singleItem = items.find((item) => item.id === Number(id));
res.json(singleItem);
};
module.exports = { getInfo, selectSingle };
The site that you are on now! This workspace showcases all of my work throughout the semester. The 8-bit retro game theme was inspired by my sister. I couldn't come up with an idea for what to make the theme about, as I had already exhausted ones such as a NASA terminal or a cold war era terminal through other projects. When I asked her, she was able to propose the idea of an arcade theme and I thought it was an incredible idea. And as you can see, voila, it has now come to fruition! I hope you enjoy :)
Go to M01 Project: WorkspaceIn this project, I went ahead and showed off the absurdity of console pricing. I used javascript to change out my images and text in response to user interaction. I also showed off loops, console logging, and fall through functionality. It is all copiable too!
Go to M02 Project: Page About ScriptsIn this project, I learned how to pass parameters in Node.js. This one definitely took me the longest as I had to think through what I was going to do and then spend a bunch of time fine tuning it to just what I wanted. I think that it turned out well! You can click on any of the characters on the screen and get a description of who they are like they are in a RPG. You can also see the code below and copy it for your own use. If you scroll a little lower, you can also see an example of what you can do with the code. You can go into your command prompt and pass the name of a specific character and a specific weapon and get a custom story that involves that character with the weapon of your choice. Feel free to test it out and try differnet combinations. I hope you enjoy! Despite this one being quite difficult, I enjoyed it thrououghly.
Go to M03 Project: Entering Parameters to Node.js
In this project, I built a retro game store site that pulls game data from the RAWG API and shows it in a scrollable list. The whole thing runs on a custom node.js http server. I had way too much fun with this one.
It has two npm scripts. "npm run fetch" runs games.js which hits the RAWG API and grabs 40 retro games from 1970 to 1999 on NES, Atari, and Game Boy. Then lodash filters those down to only games rated 4.5 or higher, sorts them by rating, and trims the data down to just the name, rating, release date, and genres. That all gets saved into games-data.json. Figlet prints a cool ASCII art header in the console while it runs, which is honestly half the reason I added it.
"npm start" runs server.js which is a raw http server. It manually loads every single file with fs.readFileSync and serves them with the right content-type headers. Every page, every css file, every image, every js file has its own if/else route. Thats why server.js is so dang long. The 404 page also has a Blockbuster joke on it because I'm 19 and I think I'm funny.
The frontend uses script.js to fetch games-data.json and loop through each game, building a slide for each one with the name, genre, and rating. Theres also a coupon banner in the corner that cycles through different deals every 5 seconds using setInterval.
The design is modeled after a late 90s game shop. Neon exit and about signs that flicker, sticky notes from fake employees, a wooden border on the game list, a rotating coupon. The about page even has a duck svg in the title because why not.
Dependencies on this one are lodash for filtering and sorting, and figlet for the ASCII art. Nodemon as a dev dependency for auto restart.
// imports
const https = require('https');
const fs = require('fs');
const _ = require('lodash');
const figlet = require('figlet');
const API_KEY = '122241539d6f4e3caf835bf5ad59c871';
// figlet header for ASCII art
figlet('Rocket\'s Games!', function(err, data) {
if (err) {
console.log('Figlet is broken...');
return;
}
console.log(data);
console.log('Fetching games');
// grabs retro games from 1970-1999 on NES, Atari, and Game Boy
const url = `https://api.rawg.io/api/games?key=${API_KEY}&page_size=40&ordering=-rating&dates=1970-01-01,1999-12-31&platforms=49,43,26`;
https.get(url, (res) => {
let rawdata = '';
res.on('data', (chunk) => {
rawdata += chunk;
});
res.on('end', () => {
const data = JSON.parse(rawdata);
const games = data.results;
// lodash: filter to 4.5+ rating
const topgames = _.filter(games, (g) => g.rating >= 4.5);
// lodash: sort by rating descending
const sorted = _.orderBy(topgames, ['rating'], ['desc']);
// lodash: clean to just the fields we need
const cleaned = _.map(sorted, (g) => {
return {
name: g.name,
rating: g.rating,
released: g.released,
genres: g.genres.length > 0 ? _.map(g.genres, 'name').join(', ') : 'N/A'
};
});
console.log(cleaned);
// save to file for the frontend
fs.writeFileSync('games-data.json', JSON.stringify(cleaned, null, 2));
console.log('Data written to games-data.json');
});
});
});
In this project, I was able to finally not have to create a custom script that launches the nuclear codes on the start of every hummingbirds flap interval. I simply made a list where you can add your favorite games! It saves what you put in there with localstorage and cookies. Feel free to try it out and put any of your favorite games in and edit them and delete them if you want! My favorite games, thank you for asking by the way, despite me being alone in my room, are Doom Eternal for stress relief, Wolfenstein: The New Order also for stress relief, and L4D2, ALSO for stress removal. As you can see, with all of my games, I'm not a very stressed out person. Oh and I also play GTA V. That one isn't for stress removal, I just wanted a tank. Don't judge me, you would also want a tank if you could have one. Don't tell me you never have looked at a tank and weren't like "Man.. only if I could afford that".
Go to M05 Project: Storage AppIn this project, I built out a project which utilizes an API from the Open Trivia DB, category 15 for video games. The site generates a quiz filled with questions that you must go through and answer correctly. The more you answer correct, the higher score you will get. The test has three different difficulties, easy, medium, and hard. Feel free to try each level and see what the highest score you can get is. Good luck!
Go to Project 6: API ProjectIn this project, I built out a character creator that uses jQuery UI to let you pick between three classes: Fighter, Runner, and Mage. Each class has its own tab with sliders to adjust stats like strength, speed, stamina, and spell power. There is also an accordion section that explains how each class works (with some commentary about Emily summoning a tsunami in Kansas, don't worry about it). Once you've got your character dialed in, you hit the create button and a dialog pops up with your final stats. Feel free to make your own hero and send them off to do whatever it is heroes do, fighting dragons or whatever.
Go to Project 7: JQuery UI ProjectIn this project, I built out my resume but made it look like a video game manual that you open. There is a cover with an "OPEN" button, and when you click it, the cover flips over with the Web Animations API and reveals the inside where my info, work history, skills, and education all live. The bottom has a Bootstrap carousel that scrolls through screenshots of some of my other projects. The flip animation is disabled on mobile because trying to do a 3D book flip on a phone screen looked terrible. It also pushes a history state when you open it so it has a #open hash in the URL, which was one of the requirements. Feel free to crack it open and take a look!
Go to M08 Project: My Motion Page
In this project, I built an arcade themed script that shows off how eventemitters work in node.js. You run it in the terminal and pass in a player name and a score, like node arcade.js Alex 1500, and it fires off a few events to greet you and tell you what your rank is.
I set up three events on the arcade emitter. The welcome event takes the player name as an argument and prints a greeting. The score event takes three arguments (your raw score, the bonus points, and your total) and prints them out together. The gameover event fires last and assigns you a rank based on what your final score is.
There are four different ways data gets manipulated through the script. The name gets uppercased so it shouts at you when it greets you. The bonus points get calculated as 10% of your score using Math.floor. The score and bonus get added together and stuffed into a string template. And the rank gets decided through an if/else chain.
The rank tiers are a little personal lol. 2000+ is "how the hell did you get here", 1000+ is "discord mod levels", 500+ is "nice, but go back to tf2", and anything below 500 is "were you even trying?". Try and break it if you want.
// node modules
const EventEmitter = require('events');
// arcade eventemitter
const arcade = new EventEmitter();
// get user input
const playername = process.argv[2] || 'Player1';
const playerscore = parseInt(process.argv[3]) || 0;
// data manipulation example 1: convert player name to uppercase
const displayname = playername.toUpperCase();
// data manipulation example 2: math operation to calculate bonus points
const bonuspoints = Math.floor(playerscore * 0.10);
const totalscore = playerscore + bonuspoints;
// event 1: welcome player
arcade.on('welcome', (name) => {
console.log(`Welcome to the arcade, ${name}!`);
});
// event 2: display score (accepts arguments for score, bonus, and total)
arcade.on('score', (score, bonus, total) => {
// data manipulation example 3: string concatenation with math
console.log(`Your score is ${score} + ${bonus} bonus = ${total} TOTAL POINTS!`);
});
// event 3: game over
arcade.on('gameover', () => {
// data manipulation example 4
let rank;
if (totalscore >= 2000) {
rank = 'how the hell did you get here';
} else if (totalscore >= 1000) {
rank = 'discord mod levels';
} else if (totalscore >= 500) {
rank = 'nice, but go back to tf2';
} else {
rank = 'were you even trying?';
}
console.log(`Game Over! Your final rank is: ${rank}`);
});
// trigger events
arcade.emit('welcome', displayname);
arcade.emit('score', playerscore, bonuspoints, totalscore);
arcade.emit('gameover');
In this project, I built my first React app using Vite. The page is a small list of retro games where you can filter them by console using a dropdown. It is built out of three components: a Header that shows the title and today's date, a GameList that takes the games array as a prop and maps through it to display each one, and a Footer that auto-updates the copyright year. The filtering uses useState to track the selected console and then .filter() to only show games that match. It also shows a counter at the top that tells you how many of the total games are currently showing. Pretty bare bones, but it was my first time touching React so I'm proud of it. Feel free to filter through and see some of my favorite retro games!
Go to M10 Project: Simple ReactIn this project, I remade my parents' website, Insite Motion. The original site has been around forever and was due for some accessibility and layout updates, so I rebuilt the home page from scratch while keeping the brand identity intact (same colors, same logos, same timeline of their company history). The page has a sticky header that shrinks when you scroll, a hamburger menu for mobile, a services section with circle icons, a portfolio grid where each item opens a modal with details about the project, an about section with a vertical timeline of all the businesses my parents have built (Insite Motion, Rep Nexus, Ready Power Market, and Moxie Automotive Consulting), and a contact form at the bottom. The modal closes if you hit Escape, click the X, or click outside of it. Was a fun one to make since I got to build something my parents might actually use.
Go to M11 Project: Portfolio Item 01For this one, I created a site that demostrates how a four stroke cylinder engine works using React. You can click through the four different stroke (stages) of the engine and see each part with a detailed explaination. Engines are something I have a passion in and I always love having the opportunity to spread knowledge about them as they are quite simple once you get the grasp of them. Enjoy!
Go to Project 02: Skill Showcase