Jo Fuller Case Study

Working Dog Development

Jo Fuller Educational Consulting

Visit Website

Overview:
- Front end design and programming
- Logo and graphic design
- Back end microservice for contact form

Project

The initial conversations with Jo went all over the place - from the kinds of tools he might need to run he small educational consulting business, to the kinds of things she's interested in outside of work. These are the kinds of conversations I like to have with clients - they help me paint a full picture and get an idea of who the client is, what they like, and what their website and and brand should look like.

At the end of our conversations, it was decided that the best solution for her needs was a static website. Like before, I started out with design - creating some graphical assets and trying to envision what her website would look like.
I also helped the client decide on a domain name - 3 actually.
- JoFuller.com
- JoFuller.org
- JoTo.college

We went with 3 domains, the .org and .com one to avoid any confusion with clients going to the wrong one, both feeling appropriate for Jo's target audience. The last one, .college, was a fun branding choice to give her an edge over competitors.

A first draft.
An initial design

Programming

I wanted to make her website stick out from the competition, while also remaining serious and professional. Parents need to trust her if they're going to turn to her for decisions involving their childrens education. In order to give her website some flair I used JavaScript to generate blobs in the background that moved and bounced around gently, like a lava lamp. In the below final iterations of the website design, you can see the lava lamp "blobs" in the background.

The final iteration, mobile version.
Final iteration on mobile

The final iteration, desktop version. Final iteration on desktop

Lava lamp by ChatGPT

This was another instance where I leveraged AI - as a solo developer trying to maintain a schedule, figuring out the JavaScript code to create to lavalamp would have taken me to much time. The code generated by ChatGPT uses the leverages the canvas html element, and then creates a Blob class. This Blob class is used to instantiate the blobs in our background. We have a few functions that actually generate the look of the blobs - a generatePoints functions that creates the initial values, and a smoothPoints function that smooths out the blob shapes. You can check out the code ChatGPT wrote below. If you have a better way of doing it and want to teach me, or noticed that ChatGPT plagarized your code, please contact me!

    const canvas = document.getElementById('blobs');
    const ctx = canvas.getContext('2d');

    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    const blobs = [];

    class Blob {
      constructor(x, y, radius, color) {
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.color = color;
        this.points = this.generatePoints();
        this.velocity = {
          x: (Math.random() - 0.01),
          y: (Math.random() - 0.01)
        };
      }

      generatePoints() {
        const points = [];
        const numPoints = 8; // Number of points for the blob shape
        for (let i = 0; i < numPoints; i++) {
          const angle = (i / numPoints) * Math.PI * 2;
          const offset = (Math.random() - 0.5) * this.radius * 0.4;
          const x = this.x + (Math.cos(angle) * (this.radius + offset));
          const y = this.y + (Math.sin(angle) * (this.radius + offset));
          points.push({ x, y });
        }
        return points;
      }

      smoothPoints(points) {
        const smoothedPoints = [];
        const numPoints = points.length;

        for (let i = 0; i < numPoints; i++) {
          const p0 = points[(i - 1 + numPoints) % numPoints];
          const p1 = points[i];
          const p2 = points[(i + 1) % numPoints];
          const p3 = points[(i + 2) % numPoints];

          const t0 = 0.0;
          const t1 = t0 + Math.pow(Math.sqrt((p1.x - p0.x) ** 2 + (p1.y - p0.y) ** 2), 0.5);
          const t2 = t1 + Math.pow(Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2), 0.5);
          const t3 = t2 + Math.pow(Math.sqrt((p3.x - p2.x) ** 2 + (p3.y - p2.y) ** 2), 0.5);

          for (let t = t1; t < t2; t += 0.1) {
            const a1 = (t1 - t) / (t1 - t0);
            const a2 = (t - t0) / (t1 - t0);
            const p01 = { x: p0.x * a1 + p1.x * a2, y: p0.y * a1 + p1.y * a2 };

            const b1 = (t2 - t) / (t2 - t1);
            const b2 = (t - t1) / (t2 - t1);
            const p12 = { x: p1.x * b1 + p2.x * b2, y: p1.y * b1 + p2.y * b2 };

            const c1 = (t3 - t) / (t3 - t2);
            const c2 = (t - t2) / (t3 - t2);
            const p23 = { x: p2.x * c1 + p3.x * c2, y: p2.y * c1 + p3.y * c2 };

            const d1 = (t2 - t) / (t2 - t0);
            const d2 = (t - t0) / (t2 - t0);
            const p012 = { x: p01.x * d1 + p12.x * d2, y: p01.y * d1 + p12.y * d2 };

            const e1 = (t3 - t) / (t3 - t1);
            const e2 = (t - t1) / (t3 - t1);
            const p123 = { x: p12.x * e1 + p23.x * e2, y: p12.y * e1 + p23.y * e2 };

            smoothedPoints.push({ x: p012.x * (1 - e2) + p123.x * e2, y: p012.y * (1 - e2) + p123.y * e2 });
          }
        }
        return smoothedPoints;
      }

      draw() {
        ctx.beginPath();
        const smoothedPoints = this.smoothPoints(this.points);
        ctx.moveTo(smoothedPoints[0].x, smoothedPoints[0].y);
        for (let i = 1; i < smoothedPoints.length; i++) {
          ctx.lineTo(smoothedPoints[i].x, smoothedPoints[i].y);
        }
        ctx.closePath();
        ctx.fillStyle = this.color;
        ctx.fill();
      }

      update() {
        this.draw();

        // Update position based on velocity
        this.x += this.velocity.x;
        this.y += this.velocity.y;

        // Update points based on new position
        this.points = this.points.map(point => ({
          x: point.x + this.velocity.x,
          y: point.y + this.velocity.y
        }));

        // Bounce off walls
        if (this.x - this.radius <= 0 || this.x + this.radius >= canvas.width) {
          this.velocity.x = -this.velocity.x;
        }
        if (this.y - this.radius <= 0 || this.y + this.radius >= canvas.height) {
          this.velocity.y = -this.velocity.y;
        }
      }
    }

    function animate() {
      requestAnimationFrame(animate);
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      blobs.forEach(blob => {
        blob.update();
      });
    }

    function init() {
      for (let i = 0; i < 10; i++) {
        const radius = Math.random() * 300 + 20;
        const x = Math.random() * (canvas.width - radius * 2) + radius;
        const y = Math.random() * (canvas.height - radius * 2) + radius;
        const color = "#d7dfd5";

        blobs.push(new Blob(x, y, radius, color));
      }

      animate();
    }

    init();

Anime.js

I also used a library called Anime.js to create the animation in the drop down menu on mobile. Anime.js made it much easier to create the desired effect. You can see the code below:

import anime from '/theme/anime-master/lib/anime.es.js';

document.addEventListener("DOMContentLoaded", () => {
    let menu = document.getElementById("hamburger-menu");
    let nav = document.getElementById("menu-items");
    let closeButton = document.getElementById("close-menu-button");

    menu.addEventListener("click", () => {
        anime({
            targets: nav,
            height: "100vh",
            duration: 150,
            easing: "easeInQuad" 
        }); 

    });

    closeButton.addEventListener("click", () => {
        anime({
            targets: nav,
            height: 0,
            duration: 150,
            easing: "easeInQuad" 
        });
    });


});

The code above causes the following drop down menu to slide down from the top of the screen.
Drop down menu

Email Microservice

I also had to create an email microservice to handle the contact froms. This was done using Flask. This was a relatively simple project that leveraged the Flask-Mail extension. This required hooking up my microservice to my email provider, as well as incorporating CSRF tokens. Overall, this part of the project took the longest but was also deeply rewarding - now I have a contact form microservice that I can use with other clients.

Contact form

Future and Take aways

Working with plain HTML, CSS, and JavaScript and using an SSG was a joy. This project really demonstrated how fun and simple, yet effective, sticking to the fundamentals can be. In the future I want to take this approach more with my clients, especially after witnessing how busy some of them are and how little they actually want to manage their websites. It also makes it incredibly cheap and simple to host their websites.

Anime.js also made getting the effect that I wanted a breeze. While I've created animations before using CSS, Anime.js made this drop down super easy. Although I was ultimately happy with the code that ChatGPT gave me for the lavalamp effect, the experience utlimately left me wanting for more. In the future, I think I'd like to explore libraries that make creating the effect I was looking for easier, like P5.Js, or taking a deeper dive into JavaScript and front end development to gain a deeper understanding. I want to revolve my craft around creating visually interesting experiences for my clients and their users, so studying up on JavaScript is definitely in my future.

I also want to invest in my microservice. Just as I want to invest more into learning JavaScript, I'm also investing more in learning Rust. Eventually, I'd like to rewrite this little microservice in Rust. While that might seem like overkill, especially as flexibility is seen as one of the pros of being a solo dev, I really like the idea of the security and stability that Rust can provide for a solo dev. I really like the idea of being able to create this microservice using Rust and not having to worry about it for a long time. Most of my small business clients don't want a million features - they want something that works, that they don't have to think about, so that they can go about running their business.