${title}
+${companyLink(`${company}`)} | ${location} | ${formatDate( + start + )}-${formatDate(end)}
+ ${description ? `${description}
` : ""} +From fff2261fa18fbe838ea4b7819d3c8bb680496853 Mon Sep 17 00:00:00 2001
From: travissouthard From the project page: The project we are
- currently working on is building out an expungement
- petition generator for PLSE. The application takes in
- PDFs of clients' criminal dockets and returns fully
- formatted petitions to expunge the clients' criminal
- records. Having a clear criminal record helps those
- clients to better find work, housing, and services. A Philadelphia-specific weather based packing
- list for bike camping trips. A debt snowball calculator with a chart showing how the
- different debts pays down as you follow the plan A clone of the classic computer game, Minesweeper. A small 2D simulation of particles enacting gravity on
- each other and combining as they get close together. PhilaVibes is a map application that helps people in
- Philadelphia at point A with more time than they need to get to point B
- find a comfortable place to be between those spaces. The map will
- feature points surrounded by word clouds describing the spaces (and
- will show the name, address, etc, once the user clicks on that space).
- The user can see choose to see spaces around them, along their route,
- or around their destination. Philavibes started as a
- Code for Philly Launchpad 2023
- project. A demo project by Azavea (now Element 84) developers to show Bipartisan Infrastructure Bill Spending by state. A repository of the work that fellow Azavea co-workers and I went through to learn computer science fundamentals. This is the first dedicated blog post as an attempt to
- make a blog from scratch that can be read by RSS readers. I have been
- wanting to make this for some time and finally made the time to figure
- out how to do this on a static site and without having to write out XML
- code by hand. I have been redoing my portfolio site recently and wanted to sit down
- and figure this out. I wrote a basic JavaScript script to parse a data
- object I was already using for the portfolio and made a way to generate
- XML code from it. The code is pretty simple but I will include it at the
- bottom of this post. I make no promises about the frequency of how often I will be making
- posts here, but will update with new code projects and pixel art pieces
- as they come up. But really, that's the beauty of using an RSS reader:
- The reader checks for you! For the past year or so I have really re-fallen in love with using an
- RSS reader to keep up with blogs, webcomics, and a few other things that
- post irregularly (or that I used to follow on Twitter). It feels good to
- read through and empty out my feed each day and really helps me to keep
- up with creators and companies with ease. I have been using NetNewsWire's
- iOS app for a few weeks and like how it works, especially the "reader"
- view. Go ahead and add https://travissouthard.com/rss.xml to your reader of
- choice. Edit: I cleaned up how the rss is generated and adding HTML tags
- to actually show the images and organize my writing better. In fall of 2022, I wrote a blog post for
- work on what makes for good documentation. Read the full post here. I am currently volunteering on a project as part of Code for
- Philly's Launchpad 2023. I am working with a small team of developers and UXers to develop a project
- that will seek to connect Philadelphians to comfortable public spaces, with the focus being on finding places
- spontaneously. Jump to code solution. We got started at the end of March this year and the launchpad demos will be on May 10 as part of
- Philly Tech Week 2023. Come see our presentation!
- For the past few weeks we have been simultaneously going
- through the discovery of what we want to build and setting up the boilerplate to build our code on.
- One speed bump I ran into was setting up a For context, we are going to be building a React frontend with Leaflet for our map component, and a Django
- backend with PostgreSQL and PostGIS. We also decided to set up React with Vite to avoid create-react-app. Setting up the Dockerfiles and docker-compose.yml file were easy enough using the Docker setup documentation. I
- made sure each docker container built and ran on its own, but when putting them together with compose, I was
- getting this error from Docker: After a lot of searching, looking at forums and docs, and trying many different things, this error mostly pointed
- to the idea that vite is missing. Where a lot of the solutions pointed to an old issue with Node 14, Typescript,
- and Vite and that solving it with updating Node to 16, that wouldn't work for us since we are using Node 18. I checked to make sure it was in the package.json, and making sure that npm install was running successfully. And
- there I found it: Despite, the Dockerfile calling for The solution I found pointed to using the following line in the Dockerfile in our React app: And of course adding that entrypoint.hs file with: The Edit: 6/16/23 - Fixed a tab error in the code portions. I am still very much on the job
- search. I am actively seeking a full-time (though ideally 32-hours-a-week) software engineering position
- in civic technology with geospatial work. I have had some great conversations and opportunities so far and I
- hope I can find something I love very soon. While I have been home, I was hoping to establish a new routine that honors my physical, emotional, and
- interpersonal needs. Ruby recently prompted me to imagine my ideal life and I struggled to even begin. I
- have the ability and strength to imagine a better world, improvements to my city, and even fantasy elements, but
- apparently not my ideal day. I also recently thought about this issue because of a great
- (13-year-old) blog post about feeling helpless about how we spend our non-work time. But recently, Ruby and I did a 4-day camping trip at Raystown Lake in Central PA. We drove out on a Monday,
- brought our mountain bikes, an enormous borrowed tent, and a new cooler full of lovely foods to eat. At camp, I
- found I was able to live more like I want: I have felt this feeling before; that I feel better at camp than I do at home. This was a large part of why I did
- so much bike camping from 2016 to 2019 and tried to make bike touring my job a few years back. I have recognized
- that a campsite is a better environment for me than the one I make for myself at home. Granted, that does not mean I understand the solution suddenly. When Ruby and I were talking about this the other
- day, she asked me what made the difference. I wasn't really sure, but I did say, "WiFi." And I think there's
- some truth to that. I tend to stay up too late clicking on another YouTube video, or scrolling down a social
- media feed, or finding another reason to avoid bedtime. Part of this is a very ADHD struggle with transitioning
- activities, but I think I also feel resistant to be done with the day (even though all of those activities just
- reinforce that I am, in fact, done). Those late nights make it harder to get up when I want to and everything
- gets thrown off. I have come to recognize that I am far more capable of harder and more complex tasks in the morning, whether
- that's work, chores, or even a hobby. I was good at building that into my last job by blocking off the start of
- my day through 1 or 2pm. I think trying to build my day more like that above schedule is going to really help me
- just feel like I'm living more fully. Especially the idea that I would have a morning block for harder things to do and an afternoon block for easier
- or less complex things to do. Another huge thing that I have really been needing more and more is to
- specifically leave the house and bring my computer to a cafe or do chores outside the house (including outside
- my own backyard). I just don't go outside nearly enough if I don't already have a "thing" to go to. So it's up
- to me and my adult self to make that happen. Yes, the schedule above is simple, but having that as an established habit will be better than the current habit:
- Just throwing myself at tasks and feeling paralyzed so I just avoid them while falling in scroll holes until
- they are too urgent to avoid. To
- no surprise; that feels awful. I genuinely believe that establishing such a routine would help me shake some of
- the executive dysfunction I've
- been feeling lately. Not to mention it will help me really feel like I'm living up to my potential and living a
- fulfilling and
- actually restful life. Building habits is very hard, but I have done it before and can do it again. I have been trying to keep this portfolio and its parts pretty simple
- and lightweight. Part of this is wanting to
- use free services as much as possible, but part of it is to keep my vanilla Javascript skills up to snuff. It
- also may be my recent interest in permacomputing as
- well as a great disdain for overcomplicated (and data-sucking) big tech products. Sometimes though it means I miss out on some features that come a little more "out of the box." I met up with a
- former colleague last week and we were talking about just this issue. He pointed out that while he has been
- enjoying reading these, there was no way to link to a particular post. Well no more! I did some work this morning to add support for permalinks with these posts despite not using a backend or
- database. The solution I adopted here was to use query parameters from the url and a custom function to create
- slugs from the post titles as unique-ish identifiers. This was pretty easy to set up, luckily, and you can look
- at what I did to
- put this
- together here. This also gave the the ability to refactor some functions to better organize what is
- happening where and add the navigation buttons to the top of the
- blog page as well. Granted, this current system using a javascript array in a There is something powerful about mastering the "simpler" or lower-level skills in coding. I felt this way when
- digging more deeply into bash scripting, C, and SQL queries the last few years. But I think that since I am most
- interested in building out things for the web, I will continue to work in vanilla (or close to) Javascript and
- try to make things as lightweight as can also be readable (and maintainable). Moreso than feeling more like a wizard with code, I am feeling good about having my own online space to put the
- weird
- things I am working on or just what is interesting me at any time. It is also nice to feel like I have more
- control over the presentation of that space and its content. I am not necessarily interested in
- becoming a Content Creator™, I am interested in sharing what I make, how I make it, and
- especially in helping others to learn how to create spaces like these for themselves. These posts still feel good to write and especially to see them update in my RSS reader. These will likely
- continue to be a mix of project updates and personal blogs. I got a lot of good feedback on and
- have somewhat improved the way I am spending my days, but that is a whole other work in progress. Thanks for reading!
- I have been arguing with myself for a while now about getting back into content creation for the
- past few years. I have started and
- Theoretically this is the "right reason" to be creating content: Just feeling a drive to make
- things. However, I really despise the phrase/title content creator for both its
- hand-wavey vagueness and especially the implication that one is not a filmmaker or an illustrator or a writer
- but a machine akin to a meat grinder whose main output is "stuff". This is especially shakey ground to even
- acknowledge considering AI generated content.
- But I suppose I am considering making intentional, scheduled work again. And while I am not quitting my day job
- (certainly not in week 3), I do have an interest in trying to grow an audience and add some commerce to the art.
- And thats where I start to get nervous: When I have started to treat making things for fun as a job they have
- gotten less fun. Specifically resenting time pressure, feeling like I have to make a thing or
- else, and that the things I am making are just not good. Often I felt like the "just to get it done"
- factor was hurting how I felt about making things.
-
- Granted, I say all that and yet: I was always far happier having made the thing and seeing it posted than I ever
- was not posting the things that didn't make it. I genuinely enjoy the act of editing a video, hand-placing a
- pixel, and writing a blog post. Making things is fun, and as long as it stays fun I'll be happy to make things.
- If I can continue to make things that make me happy and make some income from it without feeling burned
- out by it, then that would be genuinely dreamy.
-
- This is not an announcement of anything in particular. I don't have a specific plan right now, but I suppose
- keep an ear out because I'll be making something soon. Which of course reminds me to remind you that
- you can get this blog delivered to you automatically by subscribing to the RSS
- feed I maintain for this website!
-
- So definitely no promises, but lately I have been interested in:
- Documentation is vital for anything open source; from open
- hardware, to free recipes, to DIY repair tutorials, but it is especially
- vital when it comes to software. Whether you're a user or a developer,
- documentation provides the “how to” for interacting with any piece of
- software.
-
- Since I first started getting into software development two years ago,
- I've learned that documentation frequently assumes you already know how
- to use the item in question, and are using the docs as a reference.
- While acting as a reference material is one of documentation's important
- functions, it cannot be the only function...docker-compose.yml
file. In my work life and in my other
- Code for
- Philly project, we use these to build, run, and manage our Docker containers for the frontend, backend,
- and
- database. However, I realized this week I have never set one up from scratch since those above projects were
- either already built or built from a template we couldn't use for this project.
- error: cannot resolve import from 'vite' from vite.config.ts
npm install
to run when building, the logs
- showed it wasn't actually running.
-
- # From:
- ADD . .
- RUN npm install
- CMD ["npm", "run", "dev", "--host"]
- # To:
- # See the PR for the full Dockerfile.
- ADD . .
- ENTRYPOINT [ "/entrypoint.sh" ]
- CMD ["npm", "run", "dev", "--host"]
-
-
- #!/bin/sh
- npm install
- npm rebuild esbuild
- exec "$@"
-
npm rebuild
is there as a precaution for a problem I was running into with the wrong esbuild
- coming from the Docker host, but using that entry point file did successfully get the node_modules installed
- with the correct esbuild and now we are happily boilerplated and ready to roll building out as much of our demo
- as we can in the next week!
-
- .js
file will only last so many more
- posts before I want to convert this to using a database, but I have genuinely been enjoying this set up and the custom RSS generator to put
- these together. My tolerance for a too-long file may grow as that file does.abandoned moved on from so many projects over the years. None of them necessarily "gained traction", but I also
- never gave things enough time to really find their audience. I've been consistently thinking for a year now that
- I did enjoy making those things at the time and probably would again.
-
-
-
- I have been working on personal projects in my spare time for a few weeks: stepping into a leadership role at Code for - Philly, writing tutorials for upcoming streaming I'll be doing, and working with Ruby to improve our - house. -
-- The PLSE Expungement team at Code for Philly has been putting in a ton of work lately and I - am thrilled to work with such stellar volunteers. I recently wrote a new - update for our project page - talking about the progress we've made over the past year. We're actively accepting new volunteers if - you live in Philly and are interested in meaningful work and expanding your skills. -
-- I decided to start writing out tutorials for weekend-sized projects in browser-only, - native tech (i.e. just HTML, CSS and JavaScript). I have one tutorial completely done, a second almost ready, - and - a third one in the wings. Once I have all three done I will start hosting weekly streams - teaching those - tutorials as well as working through personal projects and pixel art. I plan to do those bigger tutorials - monthly and edit them into more digestible videos and written blogs. I have found a lack of beginner-friendly - written content in this area and am seeking to fill that gap. -
-- There's a few motivations for doing the streaming and content creation despite being concerned about getting back into creating content. One is that I do like making - videos. I spent years growing my video production skills and genuinely love making them. Another is that with - the streaming, writing, and developing, I will become a better developer and - continue growing my skills. The last is that I am interested in doing more independent work and that likely - means trying to grow an audience (Hi!) and build out some options beyond just working for a boss. -
-- That being said, I still plan to work full-time and you shouldhire - me full time as a Software Engineer, I am certainly still interested in working on - civic technology and geospatial projects that improve people's lives. Doing work that is meaningful to me will - always be one of the most important factors in how I choose to spend my days. A salary and healthcare are - important to - me but I work best when I am working to make the world a better place. And that's a huge part of why I'm - interested in trying to take more of my workday decisions into my own hands. My goal with these projects is to - plant seeds so that I can begin to cultivate a career aligned with my values. -
-- The best way to keep up with what's happening is to check here or subscribe to this blog's RSS feed. To see my upcoming - streams subscribe to my - YouTube channel where I'll be streaming them. -
- `, - altText: - "Travis holding a bouquet of flowers in a field of flowers!", - public: true, - lastUpdated: Date.parse("Sep 24 2023"), - }, - { - title: "September Updates and October Upcoming", - siteLink: "", - codeLink: "", - imagePath: "./assets/images/blog/sunset-graveyard.jpg", - description: ` -- I have some updates from the past week and a content schedule for the next month! -
-- Firstly I am honored that last Monday I was named one of Technical.ly's RealLIST - Engineers of Philadephia for 2023! It always feels nice to be recognized for work and this is the first - professional recognition I've received since transitioning into tech. I am especially honored that my work with - Code for Philly was specifically mentioned because I am - incredibly proud of the work that I've done there over the past three years. -
-- Speaking of Code for Philly, my second update this week is that I am stepping up to join Code for Philly's - leadership team as the Operations Lead. Over the next few months I will be learning more about the different - projects we facilitate, who the players are (that I don't already know), and what needs and strengths each - project team has. I will also be supporting the teams' operational needs like making sure teams have the tools - they need to thrive. I have loved volunteering on the PLSE Expungement team for - the past three years and will continue to support them as team lead until a new lead is fully in place. -
-- My final update from the week is that I have put in some work on updates to my personal website and portfolio! I - have made the home page a little less scattered so that now it only shows my most recent - projects, blogs, and pixel art pieces. There is also a separate about page so that it is easier to link to. The - biggest change is that the projects, blogs, and pixel art all have their own pages. Each of the pages shows a - list of the content and when clicked on, shows a detailed view with the ability to navigate to previous and next - posts. -
-- This change felt relevant to my interest in writing blogs, doing streams, and making more pixel art. The thought - was "If I will be making more content, it should be easier to look at content on my portfolio." Granted this has - been an - upgrade to my website I have been wanting to do for a long time. But of course, priorities for a long time were - elsewhere and finally I was able to make those changes. There's always more to do but I'm pleased with this - update and excited to post more and find need for things like pagination and a more robust database than a JSON - file. -
-- Speaking of content: I made a content schedule for myself for the next month! I am still figuring out a good way - to do streams from my refurbished early-2015 MacBook Pro, so those will not be announced yet, but I will be sure - to post them when they are officially scheduled. For now what I can say is that they will be Mondays from 7-9pm - Eastern Time. The blogs though are certainly ready to be sheduled! In the month of October, I will be - publishing: -
-- The titles and topics may change, like everything in life, really. And once streaming is figured out I will add - that to the schedule as well. But, like always, the best way to keep up with this blog is to subscribe to this blog's RSS feed and to my subscribe to my - YouTube channel where I'll be streaming (once I figure out the tech for it). You're also welcome to join - the test streams as they pop up. -
- `, - altText: "Sunset over a local church and graveyard", - public: true, - lastUpdated: Date.parse("Oct 1 2023"), - }, - { - title: "How to make Minesweeper part 1", - siteLink: "", - codeLink: "", - imagePath: "./assets/images/blog/minesweeper06.jpg", - description: ` -Let’s make minesweeper with just native web tools! Using only native web components we can build this classic video game using some loops, array methods, and recursion. Specifically we'll be using HTML5, CSS3 with Flexbox, and ES6 JavaScript.
-To make this project you will need:
-Start a project folder called “minesweeper” and in it create 3 files:
-Note: If you don’t already navigate your machine and manage your code files in the terminal or shell I recommend it! To do the above:
-# Make your folder and change directories to it
-mkdir minesweeper
-cd minesweeper
-
-# Create the files
-touch index.html style.css app.js
-
-Open all three files into your editor of choice. In index.html start with the boilerplate code (in VS Code you can type !
and click the first option, this fills it out as a snippet):
<!DOCTYPE html>
-<html lang="en">
-
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Title</title>
-</head>
-
-<body>
-</body>
-
-</html>
-
-In the <body>
section, add: <h1>Minesweeper</h1>
and change the text in the <title>
tag to also read Minesweeper.
-Open index.html in your browser. You can drag and drop the file into the browser or use open index.html
in your terminal or even use the file://
protocol if you’re feeling fancy.
This isn’t much of a website yet, and certainly not a game or stylish. But to get either of those qualities we need to connect our app.js
and style.css
files. To do so we need two lines in the head section of the html:
<link rel="stylesheet" href="style.css">
- <script src="app.js"></script>
-
-Then in the style.css
file we will need the following:
h1 {
- color: pink;
-}
-
-It doesn't have to be pink, but it does help to have something stand out pretty dramatically. Now in our app.js
file let's add:
console.log("This is connected");
-
-Note: I want to establish a pattern here that whenever we add new code to our files, we should test it. In this case, we’ll refresh the browser to see what we changed. So each time we make a change I’ll refer to this step as “refresh and check”. Also, each section in this is designed to be its own commit (and the titles of the sections work as good commit messages).
-When we refresh and check, we should see our title on a pink background. To see the console.log message, we’ll have to right-click, and choose “inspect” from the context menu. If it’s your first time opening the inspector, welcome! Most browsers open by default to to the Elements inspector tab to let you see the html and css as it’s rendered on the page. And we’ll come back to that later. For now, let’s choose the Console tab and we should see our message!
-A key part of the minesweeper game is a board of squares on a grid. So let’s add a <div>
element with the id: “board” (this will look like <div id="board"></div>
). A div by itself will not really do much. So we'll add our squares with JavaScript.
There’s no prescriptive size that our “minefield” should be, and in fact many implementations use many different sizes, but a 10x10 grid has an appeal for both easy numbers and a nice size for playing.
-So to make a 10x10 board we will need to make 100 squares. But rather than using 10 and 100, let’s use variables like width
and boardSize
so we can make this easier to work on later. So in our app.js
file we'll replace our console.log
line with:
const width = 10;
-const boardSize = width * width;
-
-const board = document.getElementById("board");
-
-for (let i = 0; i < 100; i++) {
- const square = document.createElement("p");
- square.innerHTML = \`\${i + 1}\`;
- board.appendChild(square);
-}
-
-Let's check and refresh. Here we run into an interesting bit of trouble, there should be 100 numbers on the screen and there are none and an error is in the console telling us no element exists with an id of "board." Code in HTML, CSS, and JS is read by the browser from top to bottom. The HTML file asks for the <link>
to the style sheet in the <head>
and then asks for the <script>
containing the Javascript. Then once the <head>
is read and rendered, the browser executes the <body>
code.
This means that in this case the <div>
with the id of "board" hasn't been rendered yet when the JavaScript is executed. There is a window.load
event we can ask for, but another way to handle this is to move the <script>
tag to the bottom of the <body>
section. This way the Javascript is only run once the whole page is rendered.
Refresh and check again and we have 100 numbers in our "board".
-Now we have 100 numbers in a weirdly long list, but that’s not much of a board. For a moment we will need to write some CSS.
-Note: For a lot of engineers, CSS can be very daunting, especially where page layout is concerned. And yes there are many component libraries that will handle this for us, but those libraries compile or otherwise eventually render CSS. Therefore, knowing how CSS works natively, it will help us to diagnose and solve design and UX issues because they come up with libraries too.
-We will worry about the aesthetic style of the game later, for now we want to get the board to be functional. And for now, we only have a few elements on the page to worry about first. We have the board itself and 100 <p>
elements within it.
The board is a <div>
with an id
of "board" so we can easily grab that with #board
in our CSS. We'll set an arbitrary width (ideally one that will make math easy later), center it, and add a border so we can see the board.
#board {
- width: 500px;
- margin: 0 auto;
- border: 1px solid black;
-}
-
-A refresh and check shows us a square with a border at the center of our page, but the numbers are still falling down the page. There's a few ways to get our elements to sit in a grid in the board, and my go-to is Flexbox, which is natively supported in CSS3. In this case it's appropriate since we have one array that we are arranging as a 2-dimensional grid and flexbox will treat them that way as well.
-Note: Flexbox Froggy is a great way to learn how to use the basics of Flexbox and CSS Tricks has an excellent reference guide.
-Let's add the following lines to our #board
CSS block to use Flexbox and display its children (our squares) as rows that wrap to the next row when they run out of horizontal space.
display: flex;
-flex-flow: row wrap;
-
-We still need to style the numbers to get them to fit nicely since their defaults will change depending on what text is in them. We will want to give them a fixed width and height, a border so we can see them for now, and center the text in each square.
-Above, we (completely arbitrarily) chose 500px
as our board width and a game board size of 10 squares by 10 squares. So each square should be 50px
, but if we just set height and width to be 50px
, the squares wouldn't fit. HTML elements render width as the content (in this case: text) width. Padding, border, and margin will add more size to the box and will not fit on our grid.
We don't want any margin, since each square will sit directly against its neighbors. We also don't want padding since we're just centering with text-align
. However, we do want a border so we can see where the boxes are.
So, we will start our squares with the following styles: A width and height of 48px
to leave room for the 1px
of border on each side (e.g. for width, the left and right borders both add to the overall width).
#board p {
- width: 48px;
- height: 48px;
- margin: 0;
- padding: 0;
- line-height: 48px;
- text-align: center;
- border: 1px solid gray;
-}
-
-A refresh and check should now show us a board with 100 squares arranged in a nice grid.
-Now that we have a grid, let's make it interactive! This is typically an expectation of any video game. The document
API has an addEventListener
method that is incredibly useful for this. The best place to add these event listeners will be as we make each square. So let's make our loop that creates the squares look like this.
for (let i = 0; i < 100; i++) {
- const clickedClass = "clicked";
- const square = document.createElement("p");
- square.innerHTML = \`\${i + 1}\`;
- square.addEventListener("click", (e) => {
- if (!square.classList.contains(clickedClass)) {
- square.classList.add(clickedClass);
- }
- });
- board.appendChild(square);
-}
-
-Note: clickedClass
may be extraneous for now, but we wanted to use the string "clicked"
more than once and it often helps to set that with a constant to ensure they match (and to make changing it easier later). I have had plenty of projects get stalled for a little too long trying to find a typo in a string.
And now that we are adding a "clicked" class to each square, let's change the color to see if our event handling works by adding this to our CSS:
-.clicked {
- background-color: green;
-}
-
-Refresh and check and click around the board. Any unclicked squares will change color now! However, as it's written above, we are only adding a class if it's not already there. If you'd like it to change the color back on the second click instead, try square.classList.toggle(clickedClass)
Now that we have a grid of 100 numbers ready to be built and clicked, let's add some mines to our minefield! In our javascript, let's create an array of 100 elements, with 20 bombs and 80 empties.
-But what data should we put in this array? We have a lot of options, but since we are developing this as a webpage, adding classes to our square elements gives us an easy way to check if that square is a bomb (and then style it) by checking square.classList.contains()
.
Note: For a savvy game player, this does expose the locations via the inspector if a player wanted to cheat. If we were building something that needed to be competitive, a better choice would be to refer back to our array and check the value there. While less likely, that savvy player could still have access to that array via the console. In that case, a backend server would probably be necessary to keep things competitive. This is certainly outside our scope here, but it's always worth thinking about security especially when practicing.
-So if we are going to use classes on each square, an array of strings would be a good choice. The strings in this case will be "bomb" and "valid", as in: There is either a bomb in this square or this square is a valid place to click. Booleans like true
and false
could also work, however, with enough distance it may become muddy wondering if it's true
this is a bomb or true
that we can safely click it.
Note: In this case I've chosen the word "bomb" instead of "mine" since "mine" has a few common meanings (place to dig for ores, belonging to me, etc), but "bomb" commonly is just that (though I suppose it is also sometimes a cake). But the goal of naming like this is to choose something as clear as possible.
-In our app.js
file let's add the following after the boardSize
is declared but before the board
is defined:
const bombCount = 20;
-const validCount = boardSize - bombCount;
-const squareValues = [];
-
-for (let i = 0; i < boardSize; i++) {
- if (i < bombCount) {
- squareValues.push("bomb");
- } else {
- squareValues.push("valid");
- }
-}
-
-Now we have an array of 20 bombs and 80 valids. We'll randomize that later so that the board is not just bombs at the top. But for now, let's use this array to create the board. So let's change our loop to live within a function (with a clear name) and use that array:
-const createBoard = () => {
- for (let value of squareValues) {
- const clickedClass = "clicked";
- const square = document.createElement("p");
- square.innerHTML = value === "bomb" ? "💣" : "😀";
- square.classList.add(value);
- square.addEventListener("click", (e) => {
- if (!square.classList.contains(clickedClass)) {
- square.classList.add(clickedClass);
- }
- });
- board.appendChild(square);
- }
-};
-
-createBoard();
-
-Note: If you're not familiar with for...of loops, they are very useful for iterating over every value in an array. Another option could be using squareValues.forEach()
, but in this case I prefer how the for...of loop looks.
Now when we refresh and check, we should see the 20 bomb icons in the top two rows of the board and the remaining of the board should be smiley faces.
-Our minesweeper game is going to get very stale very fast if the board is always populated with all the bombs up at the very top. So let's randomize the array we are building the board from each time we build the board.
-Let's make a shuffleValues
function and call it on the first line inside the createBoard
function.
const shuffleValues = () => {
- for (let i = 0; i < squareValues.length; i++) {
- const randomIndex = Math.floor(Math.random() * squareValues.length);
- let temp = squareValues[i];
- squareValues[i] = squareValues[randomIndex];
- squareValues[randomIndex] = temp;
- }
-};
-
-This is a very simple method of scrambling our array, but it is pretty sufficient. Every element will switch with a random element, which could mean that an element will switch with itself, but that's okay since it's unlikely all 100 will switch with just themselves.
-There's two steps to this function, for every element:
-Note: Instead of the temp switch method, another option with ES6 is to use destructuring: [squareValues[i], squareValues[randomIndex]] = [squareValues[randomIndex], squareValues[i]]
to switch them in a single line. Both are valid, though neither are immediately better than the other so choose accordingly.
Refresh and check and now our values should be randomized for every time we refresh the page.
-Check out part 2 to build out most of the gameplay. Add the RSS feed to your favorite RSS reader to get it as soon as it comes out
- `, - altText: - "Our partially done Minesweeper game with randomly placed bombs and safe squares", - public: true, - lastUpdated: Date.parse("Oct 8 2023"), - }, - { - title: "How to make Minesweeper part 2", - siteLink: "", - codeLink: "", - imagePath: "./assets/images/blog/minesweeper12.jpg", - description: ` -This is part 2 of 3 and I recommend starting with part 1 before moving on - to this part. If you have already worked through part 1, welcome back! In the previous part, we built a board of - 100 squares with 20 bombs, and randomized them around the board. In this part we will build out most of the - gameplay.
-To help keep our createBoard
function from getting too cluttered, we can start refactoring out some
- functionality into its own function.
Note: Refactoring is a specific action in which the functionality of our code stays the same, but we change the - code to (ideally) be more readable or more efficient. At the end of any refactor, nothing should be different - from the user perspective.
-A great candidate here is to take our callback code in the addEventListener
to its own function. So
- we will add the following above our createBoard
function.
const handleClick = (squareObj) => {
- const clickedClass = "clicked";
- if (!squareObj.classList.contains(clickedClass)) {
- squareObj.classList.add(clickedClass);
- }
- };
-
- And the addEventListener
line will be:
- square.addEventListener("click", () => handleClick(square));
-
Note: The clickedClass
declaration is also likely a good candidate for a global constant or a
- classes
object if we find ourselves adding other classes later. Especially if we create bigger
- boards, we want to be mindful of memory efficiency, and not be redeclaring the same string every loop.
-
And now we should add one last bit of infrastructure before adding in gameplay. We want to be able to reset the
- board without refreshing the whole page. Let's start that by adding a new button to the page by adding
- <button id="reset">Reset</button>
between the <h1>
and
- board.
-
Now let's set up the reset logic. At the top of app.js
: we'll add
- let isGameOver = false;
to the bottom of our constants block. We'll use this more in the next
- section.
-
Next we'll go down to where we select and declare the board and add
- const reset = document.getElementById("reset");
-
Then, just above our handleClick
function, we'll add in our reset function:
const resetGame = () => {
- isGameOver = false;
- board.innerHTML = "";
- };
-
- At the top of our createBoard
function, we will call resetGame();
to ensure we clear
- the board before building it again.
And finally we will add an event listener to the reset
DOM object just before we create the board at
- the bottom.
reset.addEventListener("click", createBoard);
-
- Note: On looking at this again, it may make more sense to call createBoard
at the bottom of the
- resetGame
function and have the onClick
callback be resetGame
instead.
- This will need some rearranging but is very likely worth the work for a little clarity.
-
Refresh and check to make sure the reset button does what we expect. Right now the two ways to see if it's - working is to see the squares get rearranged. If there are any green squares from clicking on the board, those - should also be cleared from the board during the reset.
-Let's add some actual gameplay features! The one we'll start with is getting a game over, which is not
- the most fun feature, but is one that we'll need a lot going forward. We already have an
- isGameOver
boolean, and our reset function already sets it back to false
but we
- haven't set it to true
anywhere yet.
-
In Minesweeper, there's only two reasons why the game would be over:
-We don't have the ability to flag bombs yet, but the player is able to click them. What we will want to do is
- check if the square that was clicked has a "bomb" class. DOM objects have a classList
- object and we can check if a string is among those classes with the classList.contains()
method. So
- at the top of handleClick
let's add:
if (squareObj.classList.contains("bomb")) {
- isGameOver = true;
- squareObj.innerHTML = "💥";
- }
-
- Once the game is over, we want to make the board stay put and no longer be clickable. So we can add a check to
- handleClick
to check if the game is over, and in that case return from the function early,
- effectively stopping the click action. At the top of handleClick
add
- if (isGameOver) return;
-
Refresh and check and let's test a few things now. Click some smiley faces, and these should still turn - green. Click a bomb and it should "explode". Then click more smiley faces or bombs to confirm that no - click actions are happening now that the game is over. Finally click the reset button to reset the game and be - able to click in the game again.
-A key feature in minesweeper is to check how many bombs are next to the square you just clicked. This helps the - player know which square to click next or where a bomb might be. Each square that's not on an edge has eight - neighbors, and we can visualize how to access those neighbors by displaying their array index numbers instead of - the emojis.
-We do still want to keeo track of where the bombs are, so let's add the following to the CSS file:
-.bomb {
- background-color: pink;
- }
-
- Now instead of adding emojis let's change the line in createBoard
where we declare the
- square's innerHTML
to: square.innerHTML = \`\${i}\`;
Also rather than adding the i
as a class; we know these will be unique to each square and would be a
- good candidate to set the squares' id
attributes to that i
value. Under our
- square.innerHTML
line let's add: square.id = \`\${i}\`;
-
When we refresh and check we should see the squares numbered 0-99 with 20 randomly-placed pink squares.
-Now that we can see the numbers, we can examine how we can use those numbers to check each of the square's - neighbors. Let's look at square 12, it's neighbors are (top to bottom, left to right): - 1, 2, 3, 11, 13, 21, 22, 23 - In our case, the difference between each and 12 is: - -11, -10, -9, -1, +1, +9, +10, +11
-Every board with 10 squared squares will follow this pattern, but for us wanting to choose any size in the
- future, we can make the top and bottom neighbors relative to the width
of the board. For example to
- get 1 from an index
of 12 we would say it is index - width - 1
and extrapolate our
- other neighbor values from that.
Let's add a checkNeighbors
function above the handleClick
function that looks like
- this and then break that down:
const checkNeighbors = (squareObj) => {
- const index = parseInt(squareObj.id);
- const neighborIndexes = [
- index - width - 1,
- index - width,
- index - width + 1,
- index - 1,
- index + 1,
- index + width - 1,
- index + width,
- index + width + 1,
- ];
- let count = 0;
-
- for (let neighborId of neighborIndexes) {
- const neighbor = document.getElementById(\`\${neighborId}\`);
- if (neighbor && neighbor.classList.contains("bomb")) {
- count++;
- }
- }
-
- squareObj.innerHTML = \`\${count}\`;
- };
-
- There are three steps to this function:
-count
when that neighbor has a bombinnerHTML
of the clicked square to be the count
And then of course we have to call checkNeighbors
as part of our handleClick
function.
- So we'll add checkNeighbors(squareObj);
within the if
block where we check if the
- square is not clicked.
When we refresh and check now, we should be able to click any non-pink square and be shown the number of pink - squares next to each clicked square.
-Our function to check neighbors is working pretty well, but if you can find a board configuration where a square - on the left edge has a pink square in the right-most square of the row above or a square on the right has a pink - square on the left-most square of the row below, you may notice that the count has too many bombs in it.
-From the user perspective a square on an edge only has five neighbors, and a corner square only has three. But
- our array-based method has a drawback: It doesn't know where the edges are. An array has no concept of
- edges. It is only a line of numbers we tricked into forming rows. So we will have to add a way to check the
- edges in our checkNeighbors
function.
Because we are asking the DOM for elements with specific IDs (the neighbor
in the loop above) if it
- doesn't exist then it will return undefined
. Naturally, an undefined
object
- won't have a "bomb" and will not increment our count
. For example, if we're on
- square "94" it's nonexistent lower neighbors would be 103, 104, & 105 and would return
- undefined
when we try to getElementById
for those values.
-
However, for square "90" its left neighbors are 79, 89, & 99, which all exist, but are all over on - the right side of the board, and for our gameplay need to not be checked. For the purposes of this tutorial and - showing the concept we will check for all the edges. This will also make our loop skip unnecessary checks and - make it more efficient, even if by a little.
-So let's define some edges. Top and bottom will be pretty easy since those are rows made of consecutive
- numbers and we have a constant defined for how big our rows are. The top row is
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
, so we know that they are all less than 10. But like above, we want
- our boards to be variably sized and don't want to update this check every time, so we can use the
- width
constant. We know our square is on the top edge if index < width
.
-
Likewise we know that the bottom row indeces (90, 91, 92, 93, 94, 95, 96, 97, 98, 99
) are all within
- one width
of 100. We are storing 100 in the constant boardSize
that we haven't
- used in a while. So we know a square is in the bottom row if index >= boardSize - width
.
So we can add to our checkNeighbors
function after we define our index
but before we
- define the neighborIndexes
the following:
const topEdge = index < width;
- const bottomEdge = index >= boardSize - width;
-
- The right and left edges will also be kind of paired so let's look at the numbers for our 10x10 grid:
-Left: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90
- Right: 9, 19, 29, 39, 49, 59, 69, 79, 89, 99
-
- The "ones" digit of each number has the same value. This looks like multiples of 10 (and 0) and numbers - that are each 1 off of multiples of 10. For multiples of any number we can check the modulo of - that number.
-const leftEdge = index % width === 0;
- const rightEdge = (index + 1) % width === 0;
-
- Note: Those parens in rightEdge
are key if you are using Prettier for formatting because it loves
- autoformatting to index + (1 % width)
which will never work how you want and only lead to
- frustration. But also because modulo comes before addition in the order of operations and the parens help give
- you control.
So once these four get added we will want to change our neighborIndexes
to include our edge checks.
- While I feel like this next part was pretty clever, clever solutions make me nervous:
Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?
- - Kernighan's lever
We'll be changing neighborIndexes
to be a 2D array (an array full of arrays), each inner array
- will be a pair of values being: the edge check logic and the index of that neighbor. In this case we'll also
- rename the constant to neighborEdgesAndIndexes
though this likely still needs a better name.
const neighborEdgesAndIndexes = [
- [ !leftEdge && !topEdge , index - width - 1 ],
- [ !topEdge , index - width ],
- [ !rightEdge && !topEdge , index - width + 1 ],
- [ !leftEdge , index - 1 ],
- [ !rightEdge , index + 1 ],
- [ !leftEdge && !bottomEdge , index + width - 1 ],
- [ !bottomEdge , index + width ],
- [ !rightEdge && !bottomEdge, index + width + 1 ],
- ];
-
- Most of my trepidation with this constant is that it looks pretty intimidating at first glance, but I did give it
- some whitespace here to make it a little clearer what's what. What I like about it is how it gets used in
- our loop for each neighbor, because we'll be using destructuring! Instead of just neighborId
,
- we'll have shouldCheck and neighborId
and make it very easy to skip if we should not check.
for (let [shouldCheck, neighborId] of neighborEdgesAndIndexes) {
- if (!shouldCheck) continue;
- const neighbor = document.getElementById(\`\${neighborId}\`);
- if (neighbor && neighbor.classList.contains("bomb")) {
- count++;
- }
- }
-
- Note: On second look, the shouldCheck
value is a bit of a double negative how we're using it.
- Rather than all those !
s in the arrays and the !
in the if
, we could
- remove all the !
s and rename shouldCheck
to shouldNotCheck
or
- shouldSkip
.
-
Now with a refresh and check we should see accurate counts of pink squares when we click on the non-pink squares. - We should check corners and edges to make sure they work as expected.
-One of the more satisfying parts about playing minesweeper is clicking on an empty square with no bombs - surrounding it because it then reveals all its neighbors and if those neighbors are empty then each of - those neaighbors' neighbors are revealed and then each of THOSE - neaighbors' neighbors are revealed and so on. When we see a pattern like that it's probably - a good case for recursion!
-Note: Recursion can be a scary concept and in our case the recursion will be a little removed but is still there. - One thing to remember with recursion is that we want to avoid infinite loops and thus need an exit condition. - Luckily we already have one built in, and I'll explain below.
-Let's add the following chunk after the for
loop where we increment the count
:
if (count === 0) {
- for (let [shouldCheck, neighborId] of neighborEdgesAndIndexes) {
- if (!shouldCheck) continue;
- const neighbor = document.getElementById(\`\${neighborId}\`);
- handleClick(neighbor);
- }
- }
-
- What does this loop do? If the count for this square is 0, loop over each of its neighbors, if we should check
- them, and run handleClick
on that neighbor. handleClick
will check if the neighbor has
- already been clicked and if not, will run checkNeighbors
on it. This is our recursive call:
- checkNeighbors
will call handleClick
on those neighbors it's checking which will
- call checkNeighbors
which will call handleClick
and so on until all the 0s and their
- neighbors have been clicked.
-
Above I mentioned we have a buit in exit condition for our recursion, which is that in handleClick
,
- this only calls checkNeighbors
if the square hasn't been clicked. If it has been, then we end
- the function there. This prevents a square checking all eight of its neighbors and then those neighbors checking
- the original square that checked them causing an infinite loop.
Refresh and check by clicking on a square with no pink squares around it so we can see all those 0s appear! - Groups of 0s should now show up like little seas with continents of bombs with shores of 1s and 2s.
-Eagle-eyed learners will notice that we have two nearly identical loops. stepping through neighbors and doing - something to them. The principle of DRY code - (Don't repeat yourself) tells us this loop should be its own function that gets called multiple - times to make our code easier to read, debug, and develop in the future.
-Let's start by writing out what code is shared in both loops:
-for (let [shouldCheck, neighborId] of neighborEdgesAndIndexes) {
- if (!shouldCheck) continue;
- const neighbor = document.getElementById(\`\${neighborId}\`);
- // Some stuff we want to do to the neighbor
- }
-
- We do still want access to neighborEdgesAndIndexes
so we should keep our new function inside
- checkNeighbors
and I think we should declare our new function in between where we declare
- neighborEdgesAndIndexes
and where we declare count
:
-
const doForEachNeighbor = (callBack) => {
- for (let [shouldCheck, neighborId] of neighborEdgesAndIndexes) {
- if (!shouldCheck) continue;
- const neighbor = document.getElementById(\`\${neighborId}\`);
- callBack(neighbor);
- }
- };
-
- Where we want to "do stuff" to the neighbor, we will use a callBack
function or one that
- we give as an argument to our new function. And when we call doForEachNeighbor
, we will have to set
- a parameter that will accept neighbor
as an argument in our callbacks. This is starting to sound
- like recursion again, but it really is just an oddity of callback functions.
So instead of our two loops, let's replace them with:
-doForEachNeighbor((neighborObj) => {
- if (neighborObj.classList.contains("bomb")) {
- count++;
- }
- });
- if (count === 0) {
- doForEachNeighbor((neighborObj) => handleClick(neighborObj));
- }
-
- And let's break this down. First we call doForEachNeighbor
and our anonymous callback accepts a
- neighborObj
parameter and checks if that neighborObj
contains a bomb class. If it
- does, it increments the count. Once that's complete, if count
is 0, we call
- doForEachNeighbor
again, but this time, we pass in an anonymous function that also accepts a
- neighborObj
parameter and calls handleClick
on that neighborObj
.
-
Like I wrote up above, refactoring means changing our code but that the functionality remains the same as before - we changed anything. So let's refresh and check and make sure that everything is still working as before. -
-Now that we have the ability to show empty areas of the board with our recursive checks, let's make the board
- a little less noisy. Rather than showing 0
in squares that have no bombs as neighbors, let's
- only show the count
if the count
is greater than 0. So then these empty areas will
- also be visually empty and the board will be easier to look at.
To do this let's cut squareObj.innerHTML = \`\${count}\`;
from the bottom of
- checkNeighbors
. We'll then change our if (count === 0)
block to be:
-
if (count > 0) {
- squareObj.innerHTML = \`\${count}\`;
- } else {
- doForEachNeighbor((neighborObj) => handleClick(neighborObj));
- }
-
- So now, we will only print the count
if it actually indicates a nearby bomb and otherwise just let
- the squares be empty. This will cut down on visual overload to the player.
Another bit of visual overload we can remove is our index
values since we only needed that to write
- and debug checkNeighbors
. So remove square.innerHTML = \`\${i}\`;
from the
- createBoard
function.
-
When we refresh and check we should see blank squares but still be able to identify our bombs by the pink squares
- they're sitting in. Now the game should be much easier to parse. Also if the black-text-on-green-squares
- bother you, you can also add color: white;
to the .clicked
block in the CSS.
Next it makes sense to me to add some difficulty settings to our game. Yes, this may seem early since our player - cannot actually win yet, so why add multiple difficulties? To make testing easier too. Later when we add flags, - 20 flags are a lot to place over and over.
-Different implementations of the game had different settings but for a board of 100, it seems to me to make sense - that "easy" have 10 bombs, "medium" have 20, and "hard" have 40. But on different - sized boards those may be way too many or too little, so let's set this up with a ratio instead.
-An object make sense to store these values so instead of setting bombCount
as 20
,
- let's declare:
const bombCount = {
- easy: boardSize * .1,
- medium: boardSize * .2,
- hard: boardSize * .4
- };
- let difficulty = "easy";
-
- Now in our loop where we populate squareValues
we'll check if
- i < bombCount[difficulty]
. Also this loop is a little odd sitting between our constants and our
- DOM elements, especially since we will want to create the array more than just on loading now. We will want to
- build this array every time we reset the game, so let's add it to the end of resetGame
!
-
If we refresh and check everything should work the same, except that when we hit reset, will will have too many
- extra squares! This is because that loop is just pushing new values into the array and the array isn't
- actually be reset to empty. We got away with this before since we were also scrambling the array with
- shuffleValues
everytime the board was created.
-
To fix this we just have to add squareValues.length = 0;
right before the loop which effectively
- deletes all the elements in the squareValues
array.
Okay now that we have the ability to build different difficulties, let's add a way for the player to choose
- their difficulty. In the HTML (which we haven't touched in a while), lets add the following between the
- reset button and the board div
:
<button id="easy">Easy</button>
- <button id="medium">Medium</button>
- <button id="hard">Hard</button>
-
- Then back in our js
file, we'll add the following after we declare board
and
- reset
:
-
const easy = document.getElementById("easy");
- const medium = document.getElementById("medium");
- const hard = document.getElementById("hard");
-
- So now we have the buttons and a way to access them, so we'll write a simple function that takes in a string,
- sets the difficulty
to that string, and immediately creates the board anew:
const setDifficultyAndReset = (difficultyStr) => {
- difficulty = difficultyStr;
- createBoard();
- };
-
- Finally we have to call setDifficultyAndReset
whenever those buttons are clicked: so between the
- reset.addEventListener
and the createBoard
call at the bottom of the js
- file add:
-
easy.addEventListener("click", () => setDifficultyAndReset("easy"));
- medium.addEventListener("click", () => setDifficultyAndReset("medium"));
- hard.addEventListener("click", () => setDifficultyAndReset("hard"));
-
- Let's do a refresh and check to make sure that our reset button still works, we can click squares and rest as - many times as we like. Then each of the difficulty buttons also reset the game and we can see they change the - number of bombs (by roughly counting the pink squares).
-Finish the gameplay and style the board in part 3! Add the RSS feed to your favorite RSS reader for more tutorials and blogs every Sunday!
- `, - altText: - "Our partially done Minesweeper game with blank squares, some pink ones, and an array of buttons reading 'reset', 'easy', 'medium', and 'hard'", - public: true, - lastUpdated: Date.parse("Oct 15 2023"), - }, - { - title: "How to make Minesweeper part 3", - siteLink: "", - codeLink: "", - imagePath: "./assets/images/blog/minesweeper19.jpg", - description: ` -This is the final part of this 3-part series! I recommend starting with part 1 and also working through part 2 before moving on - to this one. If you have already worked through parts 1 and 2, welcome back! In the previous parts, we built a board of - 100 squares with variable amount of bombs depending on the difficulty, randomized them around the board, and gave the player ways to find those bombs. In this part we will finish the gameplay, including adding the win condition and styling the board to look nice.
-Another feature of Minesweeper is that when the player clicks a bomb, we want to show them all the bombs so that they can see where they made their mistake. So in handleClick
let's add the following to our if (squareObj.classList.contains("bomb"))
block between setting isGameOver
to true
and setting the clicked bomb to an explosion:
const bombs = document.querySelectorAll(".bomb");
-bombs.forEach((bomb) => {
- bomb.innerHTML = "💣";
- bomb.classList.add("clicked");
-});
-
-The document.querySelectorAll
method is pretty powerful. It takes a CSS selector string as an argument (e.g. .clicked
for the clicked class, p
for paragraph elements) and returns a NodeList
which acts kind of like an array and has many of the same methods and properties as an array like .length
and .forEach()
.
We want to use the .forEach()
method to both set set each bomb square to show a bomb emoji and to add the "clicked" class for styling later. We want to add the above code before setting the square-that-was-clicked to an explosion so that we don't overwrite that explosion when we set all the bombs to bomb emojis.
Let's refresh and check and immediately click a bomb to see all the bombs show up and to see the one we clicked explode!
-Finally we can start making it so the player can actually win the game! In minesweeper, the player can click to see how many bombs are nearby. But once they think they know where a bomb is, they can right-click to place a flag. The flag tells the player where they already think bombs are and to help them no click on them. And for us, we can keep track of them later to let us know when the player has found them all!
-But first, let's allow the player to right-click the squares at all. We'll start by adding a function after the handleClick
function:
const handlePlaceFlag = (squareObj) => {
- if (isGameOver) return;
- if (!squareObj.classList.contains("clicked")) {
- squareObj.innerHTML = squareObj.classList.contains("flag") ? "🚩" : "";
- squareObj.classList.toggle("flag");
- }
-};
-
-The above function makes sure the game is not over, makes sure the square that was right-clicked has not already been clicked, and then fills in the appropriate text and class. This function as written will add a flag if there isn't one and will remove a flag if there is one.
-We are using a ternary operator to set the square.innerHTML
. If you haven't seen these before the following phrases work the same:
// With ternary operator
-squareObj.innerHTML = squareObj.classList.contains("flag") ? "🚩" : "";
-
-// With if/else
-if (squareObj.classList.contains("flag")) {
- squareObj.innerHTML = "🚩"
-} else {
- squareObj.innerHTML = ""
-}
-
-Now that we have our function, we can call it with a new event listener. Add the following after the click
event listener in createBoard
:
square.addEventListener("contextmenu", () =>
- handlePlaceFlag(square)
-);
-
-"contextmenu" is the event that is called with the player right-clicks. When we refresh and check, we can now add and remove flags whenever. Except that the actual context menu (the right-click menu) is getting in the way! We can prevent that by adding the event
as an argument to handlePlaceFlag
. So let's change the anonymous callback we just added to be:
square.addEventListener("contextmenu", (e) => handlePlaceFlag(e, square));
-
-We don't have to call the event
e
, but that is a pretty established pattern that other developers will also understand. Also we do have to use the e
argument before the square
argument, this is an odd JavaScript thing, but it is required. Now let's add the e
to the handlePlaceFlag
parameters which should now look like:
const handlePlaceFlag = (e, squareObj) => {
-
-And then before anything else in the handlePlaceFlag
function we'll add e.preventDefault();
This is a great little method that helps us stop that context menu from appearing.
Now another refresh and check will let us place and remove flags as much as we want. No menus in the way anymore.
-Now we are able to place flags, but we are able to place infinite flags. This does not make for very fun gameplay so we should limit the amount of flags to the amount of bombs on the field. This way if the player has placed 10 flags and there are 10 bombs, but they haven't won, then it lets them know that one or more of their flags are misplaced.
-Similar to adding our difficulty levels above, let's first create a DOM element to place our number. Let's place it between the buttons and the board:
-<p id="flagCounter">Flags left:</p>
-
-Next we'll grab with with document.getElementByID
calling this in a similar place to our other document
calls:
const flagCounter = document.getElementById("flagCounter");
-
-We want this element to update each time a flag is removed or placed and when the game updates. So we will need to have a variable that keeps track of how many flags are left, functionality to change that value when necessary, and a function to update the DOM element whenever the variable is changed.
-Let's start by adding let flagsLeft = bombCount[diffculty];
up in our constants and variables at the top of the JS file (It can go anywhere in that block as long as it's after both bombCount
& difficulty
).
Next we'll add an updateFlagCounter
function above our resetGame
function:
const updateFlagCounter = () => {
- flagCounter.innerHTML = \`Flags left: \${flagsLeft}\`;
-};
-
-And then within resetGame
, before we build our array of square values, we'll also reset the flagsLeft
and update the flagCounter
:
flagsLeft = bombCount[difficulty];
-updateFlagCounter();
-
-Finally, we will change our handlePlaceFlag
logic. When the player right-clicks, a flag should only be placed if:
In both scnarios we want to show the right icon in the square, add or remove the "flag" class, increment or decrement the flagsLeft
, and update the flagCounter
. But if has no flag and there are no more flags we want to do nothing. So in this case we'll actually remove the ternary operator and classList.toggle
method and add the following in their place:
if (squareObj.classList.contains("flag")) {
- squareObj.innerHTML = "";
- squareObj.classList.remove("flag");
- flagsLeft++;
- updateFlagCounter();
-} else if (flagsLeft > 0) {
- squareObj.innerHTML = "🚩";
- squareObj.classList.add("flag");
- flagsLeft--;
- updateFlagCounter();
-}
-
-Now when we refresh and check, we should be able to place 10 flags anywhere but not an 11th flag. We should also be able to remove flags once they have been placed. For each addition or removal, the counter above the board should display the correct amount of flags remaining.
-One last bit of functionality we need to add is the ability for the player to win the game (and for us to tell them). There are two win conditions:
-So there are two values we want to check:
-So for this a great way to check the classes of the squares is to once again use document.querySelectorAll()
to check the entire document for those values. As above, document.querySelectorAll()
receives one argument of a CSS Selector string and returns a NodeList
that behaves very similarly to an array. For the values we want to grab, we can use the .length
property to figure out how many of each type we have.
But to make it very obvious the player has won, we'll take advantage of the .forEach()
method to change the board to be fully revealed and show all the squares.
So we need a function that will check that at least one of our two win conditions is met and then change the board to show the player they won. Let's add a checkForWin
function after the resetGame
function:
const checkForWin = () => {
- const flaggedBombsCount = document.querySelectorAll(".flag.bomb").length;
- const clickedCount = document.querySelectorAll(".clicked").length;
- if (
- flaggedBombsCount === bombCount[difficulty] ||
- clickedCount === boardSize - bombCount[difficulty]
- ) {
- isGameOver = true;
- document.querySelector("h1").innerHTML = "You win!";
- document.querySelectorAll(".bomb").forEach((b) => (b.innerHTML = "💣"));
- document
- .querySelectorAll("#board p")
- .forEach((p) => p.classList.add("clicked"));
- }
-};
-
-And you can see above, since we want to check for both the "flag" and "bomb" classes, we can use .flag.bomb
, which for CSS reads as "flag class AND bomb class". But then lower down since we only want to grab p
elements that are inside the board (specifically to exclude the flag counter element), we we #board p
which reads to CSS as "paragraphs IN the board ID". I strongly recommend learning how CSS selectors work because it's pretty powerful. But also this is a great CSS selector reference resource for you to look through to see all the neat tricks.
The last thing we have to do then is to call checkForWin
in both places where the player interacts with the board to see if that interaction won or lost the game. So well add those calls to the end of both the handlePlaceFlag
and handleClick
functions.
Now when we refresh and check we should be able to flag each bomb and trigger a win, and then reset the game and click each of the non-bomb squares and trigger a win. And now we have a fully playable Minesweeper game!
-Before we're done though we should make the game look good. We have all of our functionality together but we are still revealing where the bombs are and do want to make our game look attractive.
-Aesthetically, I wanted to imitate some early looks of the game, but also I wanted to have a chiseled stone look similar to an old chess game my grandmother had on her computer in the late nineties.
-Note: Like I said above, CSS invokes the fear of the ancients in many developers, but it really is a powerful thing to learn. Even if you mostly use component libraries like Bootstrap or Tailwind, understanding CSS means you will understand how those components work "under the hood" and how best to interact with those libraries' APIs.
-The first things we will change is to remove out current .clicked
and .bomb
blocks. They only existed for us to test the game with but don't really contribute to the look of the game. And now let's make our #board
block look like:
#board {
- width: 500px;
- margin: 0 auto;
- display: flex;
- flex-flow: row wrap;
- border-top: 4px solid lightgray;
- border-right: 4px solid gray;
- border-bottom: 4px solid gray;
- border-left: 4px solid lightgray;
-}
-
-The border
attribute (as well as other box model striibutes like padding
) can be broken out into its individual sides. This lets us color and size the sides individually and in this case let's us create a faux-shadow effect that looks like of like a flat-topped pyramid.
Next we want to style each of our squares. The squares are all p
elements in the board so we can use the same selector we want our existing #board p
block to look like this:
#board p {
- width: 42px;
- height: 42px;
- margin: 0;
- padding: 0;
- line-height: 42px;
- text-align: center;
- background-color: darkgray;
- border-top: 4px solid lightgray;
- border-right: 4px solid gray;
- border-bottom: 4px solid gray;
- border-left: 4px solid lightgray;
-}
-
-It was noted towards the beginning of the project that borders, margin, and padding add to the width and to keep the squares only 50 pixels wide so we have to subtract both sides' border and from the width
and line-height
. In this case the borders will also give a faux-shadow look for each of the squares.
Finally we want to re-add a .clicked
block to show a clear difference when the squares are clicked. So let's add a #board .clicked
block:
#board .clicked {
- width: 50px;
- height: 50px;
- border: none;
-}
-
-So now when we refresh and check, the board should appear like raised stone except when we click on a square and then clicked squares should appear flat. Also this is the first time we're seeing the game as a fully-working game where you aren't shown where all the bombs are at the start. So now is a good time to test the game and check over how it's playing in full.
-Another feature of most Minesweeper implementations is color-coding the different numbers to see the "urgency" of squares at a glance, but also I have a bias towards color-coding because it helps my particular way of thinking. This will require adding a little more JavaScript to set those colors with more classes.
-So back in our app.js
file, let's add a little extra code to our checkNeighbors
function starting with a new constant after the neighborEdgesAndIndexes
:
const numberClasses = [
- "one",
- "two",
- "three",
- "four",
- "five",
- "six",
- "seven",
- "eight",
-];
-
-Then at the bottom inside our if (count > 0)
block, we will add squareObj.classList.add(numberClasses[count - 1]);
. CSS classes don't like to start with numbers so we want to manually set the classes with the number words instead. Once we've set these classes we can update our CSS:
.one {
- color: blue;
-}
-
-.two {
- color: green;
-}
-
-.three {
- color: yellow;
-}
-
-.four {
- color: darkorange;
-}
-
-.five {
- color: red;
-}
-
-.six {
- color: purple;
-}
-
-.seven {
- color: pink;
-}
-
-.eight {
- color: blue;
-}
-
-When we refresh and check the game, if we find as many number as we can we can see the different number colors. The top half of the numbers are pretty rare, but you can also edit the classes in the Elements inspector so that you can check the different classes are working (and whether or not you like the colors.)
-Now we have a pretty well put together game and just need to add some polish. To better accomplish this, we'll rearrange our HTML a little bit:
-<body>
- <header>
- <h1>Minesweeper</h1>
- <nav>
- <button id="reset">Reset</button>
- <button id="easy">Easy</button>
- <button id="medium">Medium</button>
- <button id="hard">Hard</button>
- <p id="flagCounter">Flags left:</p>
- </nav>
- </header>
- <main>
- <div id="board"></div>
- </main>
- <script src="app.js"></script>
-</body>
-
-Now we have some specific elements to grab and arrange better. Next we'll change our CSS file to look like:
-:root {
- --highlight: rgba(255, 255, 255, 0.5);
- --lowlight: rgba(0, 0, 0, 0.3);
-}
-
-html {
- font-family: monospace;
- background-color: lightgray;
-}
-
-header {
- width: 492px;
- margin: 0 auto;
- border-top: 8px solid var(--highlight);
- border-right: 8px solid var(--lowlight);
- border-bottom: 8px solid var(--lowlight);
- border-left: 8px solid var(--highlight);
-}
-
-h1 {
- text-align: center;
-}
-
-nav {
- display: flex;
- flex-flow: row nowrap;
- justify-content: space-around;
- width: 476px;
- margin: 0 auto;
- padding: 8px;
-}
-
-nav button {
- border-radius: 0;
- background-color: transparent;
- border-top: 4px solid var(--highlight);
- border-right: 4px solid var(--lowlight);
- border-bottom: 4px solid var(--lowlight);
- border-left: 4px solid var(--highlight);
-}
-
-nav button:hover {
- background-color: var(--lowlight);
-}
-
-nav button:active {
- border-top: 4px solid var(--lowlight);
- border-right: 4px solid var(--highlight);
- border-bottom: 4px solid var(--highlight);
- border-left: 4px solid var(--lowlight);
-}
-
-.one {
- color: blue;
-}
-.two {
- color: green;
-}
-.three {
- color: yellow;
-}
-
-.four {
- color: darkorange;
-}
-
-.five {
- color: red;
-}
-.six {
- color: purple;
-}
-.seven {
- color: pink;
-}
-.eight {
- color: blue;
-}
-#board {
- width: 500px;
- margin: 0 auto;
- display: flex;
- flex-flow: row wrap;
- cursor: pointer;
- border-top: 4px solid var(--highlight);
- border-right: 4px solid var(--lowlight);
- border-bottom: 4px solid var(--lowlight);
- border-left: 4px solid var(--highlight);
-}
-
-#board p {
- width: 42px;
- height: 42px;
- margin: 0;
- padding: 0;
- line-height: 42px;
- text-align: center;
- border-top: 4px solid var(--highlight);
- border-right: 4px solid var(--lowlight);
- border-bottom: 4px solid var(--lowlight);
- border-left: 4px solid var(--highlight);
-}
-
-#board p:active {
- border-top: 4px solid var(--lowlight);
- border-right: 4px solid var(--highlight);
- border-bottom: 4px solid var(--highlight);
- border-left: 4px solid var(--lowlight);
-}
-
-#board .clicked {
- width: 50px;
- height: 50px;
- border: none;
-}
-
-#board .clicked:active {
- border: none;
-}
-
-And we'll go through what that changes from top to bottom. If you refresh and check, you can see the changes as you read about them below:
-At the very top with the :root
block we set some custom colors that add a somewhat transparent highlight or shadow to the borders instead of using lightgray
and gray
. That transparency will also help later if we want to set different background colors to the game.
Speaking of in the html
block we set a new background-color
attribute. Doing so at this level sets that color for the whole page (with some exceptions, elements have a transparent
background by default). Also you can change the background-color
to any color you like and the board will now still create realistic(-ish) looking shadows since the custom colors are just lightly-transparent white and black. Try some colors out!
Next we add a header
block to get the page's header to match the decor of the game board. The notable change here is that we are using the var()
function to set our custom colors from the :root
block.
Now we move on to the nav
where we want to use Flexbox to arrange the buttons as a row with space around them. The buttons are one of those rare elements that come with some default styling. So to get them to match the styling pattern we've been using so far, we have to undo the border-radius
& background-color
. The other attribute that has a default is the border
, but we want to style that anyway so that's taken care of.
The buttons have some :hover
and :active
state defaults as well. But also if we switch the colors of the border segments while the player is clicking (aka: the element is :active
), the shadow appears to invert creating an illusion that the button is now concave. We'll also visit these with the squares in a second.
Most of the rest of the changes are switching the border colors to use the custom colors and use the transparency that comes with them. Notably we also are adding an :active
state to the squares that also invert the border colors and help make the squares look "pressed in" when the player clicks on them.
And finally the last change here is to set an :active
state for the .clicked
squares that takes away the border
so that there is no difference than the regular .clicked
squares. If the player clicks a square that is otherwise unclickable, but responds as if it should be doing something that will confuse the player.
We have a fully working game built entirely in native web tooling. This is just a static page that takes very little data to store or run (about 78 kilobytes as built here)! Web pages are pretty powerful and (while sometimes opaque or obtuse) the JavaScript and CSS can do a lot with very little.
-Lately I have found myself interested in permacomputing principles and one of the principles is to "Keep it small". Making pages smaller also helps users use less resources to play your games or visit your sites, and to put less burdern on the user to "keep up" with system requirements for more and more complex games and sites. As I continue building out more tutorials, this is going to be a driving part of how I design projects. Small games like this will be similarly complex, but even more complex works will be designed to use as few dependencies and power as possible.
-Thanks for following along with this tutorial! Find more of my projects and writing at my portfolio and blog.
- - `, - altText: - "Our fully done Minesweeper game with styling to make it look like cut stone.", - public: true, - lastUpdated: Date.parse("Oct 22 2023"), - }, - { - title: "Smaller code and bigger plans", - siteLink: "", - codeLink: "", - imagePath: "./assets/images/blog/ruby-beach.jpg", - description: ` -- Over the past few weeks I have released a tutorial to make the classic video game, Minesweeper, from scratch - using native web tools (being HTML, CSS, and Javascript). (Check out parts 1, 2, & 3!) I was motivated to write tutorials in general to help - new developers bridge the gap between what they currently know and what they need to know to get a job - developing code. The other half of that motivation is to help me become a better developer by teaching. -
-- I chose to write tutorials about native web tools because developers do need to know what the browser is - actually reading and rendering and why it does what it does. Part of this is that frameworks and libraries - compile down to HTML, CSS, and Javascript, so developers should be aware of what is happening 'under the hood.' I - was taught to try to be aware of how the tech is working at least one or two layers down to ensure my code will - behave how I want it to. The other part of that motivation has been feeling more and more that we should be - delivering smaller websites to end users. -
-- I have been reading a lot of work around permacomputing, more - sustainable technology, and making the web better for - people, including developers. And one of the easiest ways I can do that as a developer is to write my - own code that is easier on both storage and compute power and also to encourage others to do the same. One of - permacomputing's principles is to "Keep - it small" since: -
-- Small systems are more likely to have small hardware and energy requirements, as well as high understandability. - They are easier to understand, manage, refactor and repurpose. -Permacomputing Principles page --
- There's certainly more to permacomputing and sustainability than just making things smaller, and there is also a - "code golf" tradeoff where the code - becomes so small that it makes it harder to understand and thus will be harder to maintain. This blog and my - portfolio site in general are built as just a static webpage and the only library I'm using is JQuery, though it - could be rewritten in vanilla Javascript. Granted, there are downsides to this setup: one is that I cannot tell how many - people are visiting the site unless they tell me directly. For now this is fine since I am writing these for fun - and practice more than relying on them for any sort of income just yet. -
-- Along the lines of not knowing who is reading, I have no plans to track readers ever. User tracking is - creepy and invasive. I personally use tools to protect my privacy as much as I can, and I recommend you do too. - At some point, I will want to know how many times posts are being read (and if possible how many unique readers, - if there is a non-tracking way to do that). I currently have the site and blog hosted on GitHub Pages, which is - easy to use and free. But to start seeing some readership numbers, I would have to set up my own server. I would - like to do this anyway to improve how I'm coding the different posts, to fulfill my own DIY desires, and to take - more control over my work and the resources it uses. -
-
- In the meantime, I am reasonably happy with this site. It serves my need of having a consistent site for software
- projects, pixel art, and blog posts. One of my TODOs
is to create a resume.html
page
- with a list of all my past jobs and download link for the PDF. If you enjoy what
- I'm building here, please consider supporting my work to help me
- make more tutorials, blogs, pixel art, and everything.
-
- Autumn is in full show here in Philadelphia, at least up in the Northwest of Philly where I now live. The colors - are a great time to reflect on the passage of time. It being the weekend daylight savings ends is also a big - hint that time is always changing, even if only arbitrarily. -
-- I am grateful to see the leaves change like this and for consistent access to more trees in general than I had - experienced in South Philly. Despite being much closer to the Wissahickon park, I still don't go there often - enough (at least not as often as I'd like to). I also appreciate the irony that as a teenager, I completely took - for granted the massive forests around me in Northeast PA, but mostly because I wanted to be closer to my - friends. -
-- I've gone through a lot of changes to my daily life this year: My employer got acquired, the new employer laid - me off, I am - joining the Code for Philly leadership team, and Ruby is kicking up her business again! I'm still - expecting more changes to my daily life before the end of the year. -
-- Changes can be very hard, and many people are resistant to them. I had to learn to weather changes as a child - who attended at least one new school every year until the sixth grade. Then in my late teens and early twenties - I welcomed changes to help get me into better places. When I can afford to, I also bring on daily changes as - recreation in the form of bike tours. Each day is a different ride to a different place and I rarely wake up in - the same place twice on those trips. I even planned a trip in 2019 along the East Coast Greenway to follow the beginning of the changing leaves that - year. -
-- A huge change for me in the last year was buying a house with Ruby. What's especially wild about that is that we - effectively signed up to be involved with this one house and piece of land for thirty years! Granted, we don't - have to stay here for thirty years if we don't want, but a mortgage is still on that scale of - financial planning. And for most future plans, selling this house would lead to buying another. -
-- This is often a "stage" of American life where a person really wants to stop changes. They want to keep their - neighborhood exactly like it was when they bought their home. But I try to remember change is inevitable. - Planets and stars continue moving through space. Water pulls mountains down towards oceans. Winds push leaves - and seeds off plants. I hope I can continue to take on changes with grace and never try to lock my life in - place. -
-- I still don't feel ready for winter, but there is certainly still time to prepare. And arguably, that is the - purpose of a transitional time like the fall to help us get ready for the cold and dark. That we can slowly - approach the hard things and steel ourselves to them as they creep closer. I tend to be an optimist and one way - that hurts me is that I spend most of the winter just looking ahead to springtime rather than embracing what - winter has to offer. -
-- This year I want to commit to changing my approach to winter: To use the inside time to improve my engineering - and my art. To make more time for friends to get together. To take advantage of the slowed plant growth to - prepare an overgrown lot to become a garden. To spend time decorating my house. This winter I want to resist - hibernating. -
- `, - altText: - "The Wissahickon Creek flowing from around the corner through orange leaved tees that have dropped about 20% of their leaves", - public: true, - lastUpdated: Date.parse("Nov 5 2023"), - }, - { - title: "A personal financial project", - siteLink: "", - codeLink: - "https://github.com/travissouthard/travissouthard.github.io/tree/master/snowball", - imagePath: "./assets/images/projects/debt-snowball.png", - description: ` -- Earlier this year I built a project - that came from a personal need. I have three student loans from my time at University of the Arts, my time at - Temple University, and my coding bootcamp. My spouse Ruby has her own loans from undergraduate and graduate - school. We also share a mortgage on our home. I have been exploring different options to ideally pay down these - debts early and save both money on interest and time for ourselves in the future with less debt payment burden. -
-- As part of my research, the most personally compelling plan I found is called the debt snowball plan. The - essence of the plan is: -
-- What is nice about this plan is that, unless you are adding extra each month to accelerate the plan, the amount - you pay towards your debts each month stays the same. Arguably this is also a drawback, since the monthly - payment burden stays the same, even as debts are resolved. I am not a financial expert but appreciate the - simplicity and effectiveness of this plan. There are similar approaches like the debt avalanche, which works - similarly except that the first step is to order the debts from highest interest rate to lowest. My - understanding is that while also effective, the avalanche is slower overall, but especially slower to start - feeling the benefits of paying off the first debt in the sequence. -
-- For our situation, without going too deeply into our personal finances, following this plan would allow us to - pay off our student loans a few years early. However, the much bigger effect is that our mortgage would be paid - off about 14 years earlier than the 30 years our mortgage is planned for. If all goes to plan, this would save - us a lot in interest and dramatically cut down our living expenses. Granted, all our loan terms allow us to pay - down the principal ahead of schedule, and not all loans allow this (although they absolutely should). You should - check your terms on your loans before making any plans. -
-- Now that I understood this plan, I wanted to show Ruby how this may benefit us, but needed a way to show her how - it works. There are many debt snowball calculators, and most have great written descriptions, but almost none of - the ones I came across had any sort of chart showing how the plan is paid off over time. I am always one to make - a plan with a spreadsheet (including vacation plans) and my favorite part of any spreadsheet is creating charts. -
-- Having charts would also help me explain the concept and benefits to Ruby, but also would help me visualize for - myself the differences between different plans and also continuing to pay the minimums. Faced with a serious - dearth of charts amongst the existing resources, I did what all engineers do when faced with a missing tool: I - began to build it myself. -
-- To build this out I had to do some research on how loans, interest, and amortization work at a more detailed level than I had really - thought about since learning about complex and simple interest in high school math classes. I started with a - spreadsheet for myself, but quickly realized that what I wanted was a webpage. I found it much easier to create - loops and recursive functions with JavaScript than with spreadsheet macros. Besides, if I was looking for debt - snowball charts, others likely were too, so once I had a complete site, I could share it publicly as well. You - should try it out and see if a plan - like this might work for you. -
-- I built this early on in my learnings about permacomputing and building out smaller websites with just HTML, CSS, and JavaScript. I wanted to lean on - and design around the HTML tags as they exist. I think I did an okay job with this but some of the parts of how - this is built go against some of my better design sensibilities. -
-- In particular, I think the site uses too much text without intentional visual hierarchy and too little color to - help orient a person to what to use or read first. I do still think this project works but I learned some things - about designing a project from scratch and would like to revisit the aesthetics of this project sometime. - Overall, I am still happy with how it came out for a weekend project. Plus, I do still use this to occasionally - to recheck my own financial plans as our life and the world around us changes. -
-- Excitingly, we have our snowball rolling down the hill and are just this week starting to see it picking up some - momentum. My loans from my time at UArts were fully paid off at the beginning of this month and I was able to use - what I was paying towards that and put it towards my boot camp loan payments. It feels wonderful so far and I look - forward to completing this plan. -
- `, - altText: - "A screenshot of the debt snowball chart showing the paydown of the debts compared to the traditional payment plan", - public: true, - lastUpdated: Date.parse("Nov 12 2023"), - }, - { - title: "New year. New job. New habits.", - siteLink: "", - codeLink: - "https://github.com/travissouthard/travissouthard.github.io/tree/master/snowball", - imagePath: "./assets/images/blog/snowyard.jpg", - description: ` -- New year. I am newly 35 years old and just came through a tough but still good year. Many - years have been tough for me, but in the grand scheme of tough years, 2023 was pretty easy on me. This year I - was laid off from a dream job, joined the leadership at Code for Philly, didn't ride my bike enough, worked in a large corporate environment - that I dislike, but ended the year by securing a job that fits my values. Ruby and I also have some travel plans - that I will talk about more once we're back home! -
-- New job. I will be joining Community Legal Services this - week as a Developer and Data Analyst in their Digital Innovation Lab. I am excited to be working on local issues - and helping Philadelphians, but I am also just excited to be working at a values-driven organization once again! - CLS is a great organization and I have been a fan of their work for some time. -
-- New habits. I have a new Bonk planner and want to leverage - it to build up some new habits and routines I have been wanting to build. I am using it as part tracker and part - journal. Each day I note: How many cups of coffee I've had (less than I thought). Which meals I've prepared. - When I woke up and went to bed. What exercise I did that day. What 3rd place I visited. What tasks I need to do - and which I did. And finally a highlight list of what happened that day. I have been doing it for a week now and - it's already given me some insight into my habits and helped me build some new ones. -
-- I was expecting that I would have more reflections I'd like to share from my usual birthday/new year - introspection, but I think most of that is best left private. I am looking forward to this year, even if it is - starting with winter (as Gregorian years typically do), but time with friends should at least make that easier. -
- `, - altText: - "A dithered photo of our backyard lightly coated in snow, a rarity now in Philly. A mostly brown landscape features some orange pots, a blue umbrella, and white snow.", - public: true, - lastUpdated: Date.parse("Jan 7, 2024"), - }, - ], - resume: [ - { - title: "Developer & Data Analyst", - company: "Community Legal Services", - coLink: "https://clsphila.org/", - start: Date.parse("1 Jan 2024"), - end: null, - location: "Philadelphia, PA", - description: "", - }, - { - title: "Software Engineer", - company: "Brooksource", - coLink: "https://www.brooksource.com/", - start: Date.parse("1 Jul 2023"), - end: Date.parse("1 Dec 2023"), - location: "Philadelphia, PA", - description: - "Worked as a consultant full time at Comcast helping to build and launch the Iris tool built in Node.js, Express.js, MongoDB, Unity, and Angular.", - }, - { - title: "Junior Software Engineer", - company: "Element 84", - coLink: "https://element84.com/", - start: Date.parse("1 Feb 2023"), - end: Date.parse("1 May 2023"), - location: "Philadelphia, PA", - description: - "Element 84 acquired Azavea. I continued to work on the Civic Applications team working on client services projects to build and maintain geospatial web applications built in React, Django, MapBoxGL, and PostGIS for governments, non-profits, and corporations.", - }, - { - title: "Software Engineer", - company: "Azavea", - coLink: "https://www.azavea.com/", - start: Date.parse("1 Dec 2021"), - end: Date.parse("1 Feb 2023"), - location: "Philadelphia, PA", - description: - "I worked on the Civic Applications team working on client services projects to build and maintain geospatial web applications built in React, Django, MapBoxGL, and PostGIS for governments, non-profits, and corporations.", - }, - { - title: "Apprentice Software Engineer", - company: "Azavea", - coLink: "https://www.azavea.com/", - start: Date.parse("1 May 2021"), - end: Date.parse("1 Dec 2021"), - location: "Philadelphia, PA", - description: - "Spent 6 months with a mentor extending my knowledge of React, Django, and geospatial tools like PostGIS and MapboxGL.js with reading materials, hands-on 'breakable toy' projects, as well as independent and supervised client work. Besides my mentor, I also worked with a small cohort of two other apprentices sharing what we worked on each bi-weekly sprint and helping to bridge each other's skillsets.", - }, - { - title: "Junior Front End Engineer", - company: "Urality", - coLink: "https://www.urality.com/", - start: Date.parse("1 Feb 2021"), - end: Date.parse("1 May 2021"), - location: "Philadelphia, PA", - description: - "Built new features for and maintained a community mapping and digital placemaking web application and content management system using React Hooks, Bootstrap, MapboxGL, and various NPM modules.", - }, - { - title: "Shop Manager", - company: "Cycles PHL", - coLink: "", - start: Date.parse("1 Apr 2017"), - end: Date.parse("1 Jan 2021"), - location: "Philadelphia, PA", - description: - "Repaired and sold bicycles in a community-oriented shop in North Philadelphia. We also provided adult camping programming and fix-your-own-bike workshops. Our shop catered to local neighbors and university students. I worked on bicycles, kept the shop in order, and ran programming like our Cycles Campout overnight camping trips and Bike Salon, our do it yourself repair program. We helped adults and youth alike keep rolling and to enjoy their bikes.", - }, - { - title: "Software Engineering Immersive Fellow", - company: "General Assembly", - coLink: "https://generalassemb.ly/", - start: Date.parse("1 May 2020"), - end: Date.parse("1 Aug 2020"), - location: "Philadelphia, PA", - description: - "Learning frontend and backend programming alongside frameworks and languages like Javascript, jQuery, React, Python, and MongoDB. Building and deploying a portfolio of functional projects that all address various needs.", - }, - { - title: "Video Producer", - company: "Freelance", - coLink: "https://www.youtube.com/playlist?list=PL8hP1JY9f2dh7vnCpoVDSIknMWIk4s55H", - start: Date.parse("1 Oct 2013"), - end: Date.parse("1 Apr 2020"), - location: "Philadelphia, PA", - description: - "I have been making digital content like video, photography, and comics from a few different outlets. My video works have been a variety of works ranging from music videos to talk shows to fiction stories to vlogs to documentary style shorts", - }, - { - title: "Service Counter", - company: "Bicycle Revolutions", - coLink: "", - start: Date.parse("1 Feb 2016"), - end: Date.parse("1 Jan 2017"), - location: "Philadelphia, PA", - description: "", - }, - { - title: "Trip Leader", - company: "Bike & Build", - coLink: "https://bikeandbuild.org/", - start: Date.parse("1 Dec 2015"), - end: Date.parse("1 Aug 2016"), - location: "Providence to Seattle", - description: - "I co-led a group of 29 young people on a bike tour from Providence, RI to Seattle, WA to raise money and awareness for affordable housing. We rode roughly 4000 miles, built in 13 different cities, and raised roughly $60,000 to give away in affordable housing grants.", - }, - { - title: "After School Program Facilitator", - company: "CORA Services", - coLink: "https://www.coraservices.org/", - start: Date.parse("1 July 2012"), - end: Date.parse("1 Feb 2016"), - location: "Philadelphia, PA", - description: - "With CORA Services I worked at 3 different schools in different capacities working with a variety of students, age-groups, and lesson materials. I have worked with all grades between Kindergarten and 8th grade and have taught acting, better behavior, science lessons, photography and film lessons, a variety of crafts, and even managing a public group profile for projects with my current group. I have also helped all of my students in countless hours of homework help and different tutoring sessions.", - }, - { - title: "Intern", - company: "WHYY", - coLink: "https://whyy.org/", - start: Date.parse("1 Sep 2015"), - end: Date.parse("1 Jan 2016"), - location: "Philadelphia, PA", - description: - "Served as a production assistant on the WHYY Friday Arts show providing field production assistance, transcribing interviews, and editing shorts and extras for the web.", - }, - { - title: "Rider", - company: "Bike & Build", - coLink: "https://bikeandbuild.org/", - start: Date.parse("1 May 2013"), - end: Date.parse("1 Aug 2013"), - location: "South Carolina to Santa Cruz", - description: - "n the summer of 2013, I and 27 other young people crossed a continent by the power of our own legs. We saw the United States at a pace, scale, and perspective that few have or will ever have. We crossed plains and deserts, climbed hills and mountains, and built homes along the way. As a team we rode 4,250 miles through 13 states, raised $154,895 for affordable housing, ate countless peanut butter and jelly sandwiches, and built for a collective 3,584 hours with Habitat for Humanity. ", - }, - { - title: "Diplomas Now Team Leader", - company: "City Year Philadelphia", - coLink: "https://www.cityyear.org/philadelphia", - start: Date.parse("1 Jun 2011"), - end: Date.parse("1 Jun 2012"), - location: "Philadelphia, PA", - description: - "I served as a Diplomas Now Team Leader for City Year and will lead a team in addressing the Drop-Out Crisis at a Philadelphia school. I will be in charge of tracking the data for my team, building and maintaining relationships with service partners, collaborating with a School Transformation Facilitator from the Philadelphia Education Fund and a Site Director from Communities in Schools in helping to create a beautiful environment for our students to work in and to tutor and mentor students to help make sure they are on-track and inspired to graduate high school.", - }, - { - title: "Corps Member", - company: "City Year Philadelphia", - coLink: "https://www.cityyear.org/philadelphia", - start: Date.parse("1 Aug 2010"), - end: Date.parse("1 Jun 2011"), - location: "Philadelphia, PA", - description: - "My corps year, I served on the TEVA Pharmaceuticals Team at Thurgood Marshall Elementary. I followed a class of 8th graders from class to class providing classroom support, tutoring, behavior and attendance coaching, and helping to make sure my students had everything they needed to work hard in school. I also created and ran a project to have all the students at Marshall write letters to the President of the United States, helped to run our after school programming, played at recess with my students, and helped to organize many assemblies including a play at the end of the year.", - }, - { - title: "Technical Director", - company: "Shawnee Playhouse", - coLink: "", - start: Date.parse("1 Dec 2009"), - end: Date.parse("1 Apr 2009"), - location: "Shawnee-on-Delaware, PA", - description: "", - }, - { - title: "Intern", - company: "Shawnee Playhouse", - coLink: "", - start: Date.parse("1 Nov 2009"), - end: Date.parse("1 Dec 2009"), - location: "Shawnee-on-Delaware, PA", - description: "", - }, - { - title: "Server", - company: "Siamsa Irish Pub", - coLink: "", - start: Date.parse("1 Oct 2008"), - end: Date.parse("1 Aug 2009"), - location: "Stroudsburg, PA", - description: "", - }, - { - title: "Server", - company: "Five Star Staffing", - coLink: "", - start: Date.parse("1 Oct 2008"), - end: Date.parse("1 Aug 2009"), - location: "Philadelphia, PA", - description: "", - }, - { - title: "River Room Server", - company: "Shawnee Inn and Golf Resort", - coLink: "", - start: Date.parse("1 Sep 2007"), - end: Date.parse("1 Jun 2009"), - location: "Shawnee-on-Delaware, PA", - description: "", - }, - { - title: "Cabin Leader", - company: "Brainerd Presbyterian Center", - coLink: "", - start: Date.parse("1 Jun 2007"), - end: Date.parse("1 Aug 2007"), - location: "Philadelphia, PA", - description: "", - }, - { - title: "Server", - company: "Aramark", - coLink: "", - start: Date.parse("1 Sep 2004"), - end: Date.parse("1 Aug 2005"), - location: "East Stroudsburg, PA", - description: "", - }, - ], -}; - -// Uncomment to output JSON from this -// Use with `node data.js > data.json` -// console.log(JSON.stringify(data)); diff --git a/scripts/buildPages.js b/scripts/buildPages.js new file mode 100644 index 0000000..358e80f --- /dev/null +++ b/scripts/buildPages.js @@ -0,0 +1,287 @@ +const fs = require("fs"); +const dataFile = fs.readFileSync("./data/data.json", "utf-8"); +const data = JSON.parse(dataFile); + +const LOCALNAV = [ + { name: "Home", linkUrl: "index.html" }, + { name: "Projects", linkUrl: "projects.html" }, + { name: "Art", linkUrl: "art.html" }, + { name: "Blog", linkUrl: "blog.html" }, + { name: "About", linkUrl: "about.html" }, + { name: "Resume", linkUrl: "resume.html" }, +]; + +const EXTERIORLINKS = [ + { name: "RSS Feed", linkUrl: "rss.xml" }, + { name: "GitHub", linkUrl: "https://github.com/travissouthard" }, + { + name: "LinkedIn", + linkUrl: "https://www.linkedin.com/in/southardtravis/", + }, +]; + +const stripHTML = (html) => { + return html.replace(/(<([^>]+)>)/gi, ""); +}; + +const createPostSlug = (title) => { + const justWords = title.replace(/[.,\/#!$%\^&\*;:{}=_`~()\?]/g, ""); + const wordsArr = justWords.toLowerCase().split(" "); + return wordsArr.join("-"); +}; + +const getDetailPageName = (title) => { + const pageNames = { + projects: "projects", + art: "art", + blog: "blog", + }; + for (let [key, arr] of Object.entries(data)) { + for (let i = 0; i < arr.length; i++) { + const post = arr[i]; + if (title === post.title || title === createPostSlug(post.title)) { + return [pageNames[key], i]; + } + } + } + return; +}; + +const sortArrayByDate = (arr) => { + const isResume = !arr[0].lastUpdated && arr[0].end; + const sortCallback = isResume + ? (a, b) => { + const endA = a.end === null ? Infinity : a.end; + const endB = b.end === null ? Infinity : b.end; + return endB - endA; + } + : (a, b) => b.lastUpdated - a.lastUpdated; + return arr.sort(sortCallback); +}; + +const buildNav = (linkArr, isPost) => { + let navList = []; + for (let { name, linkUrl } of linkArr) { + const isLocal = linkUrl.slice(0, 4) !== "http"; + navList.push(` + +${companyLink(`${company}`)} | ${location} | ${formatDate( + start + )}-${formatDate(end)}
+ ${description ? `${description}
` : ""} +${descText}
++ + Thank you for reading! + + Please consider supporting this blog. + + +
+Print resume
+ + ${sortArrayByDate(data.resume) + .map((job) => buildResumeCard(job)) + .join("")} + `; + } + if (page.name === "about") { + return `+ My name is Travis, and I am a software engineer living in Philadelphia with my spouse, Ruby, and our two cats, Topaz and Basil. I + am interested in gardening, bike touring, and solarpunk living. +
++ I am currently working as a Developer & Data Analyst in the Digital Innovation Lab at Community Legal Services of Philadelphia. I have + previously worked at Element 84, Azavea, and Urality.
++ I am actively volunteering as one of the co-directors of Code for Philly. I previously served as the tech lead for the Expungement + Petition Generator benefitting Philadelphia + Lawyers for Social Equity, and helped put together a third + places finder as part of Code for Philly's Launchpad 2023. +
++ Before transitioning into working in tech, I spent about 5 years working in the bicycle industry as a + mechanic and bike tour organizer. I am also an Americorps alum who served in City Year Philadelphia. I + have ridden two cross-country bike tours with Bike & + Build, and another on the full East Coast + Greenway with my + best friend. +
+Philadelphian Software Engineer
+