Rasul Mammadov | Full-Stack Web Developer

Rasul Mammadov

Full-Stack Web Developer

GetTrainer is a landing page for a personalized fitness app. With exercise plans and progress tracking, it's designed to help users achieve their workout goals.

Trainer
Next.js
Typescript
React.js
Tailwind

This project came from one of my long-term clients. The goal was to create a landing page from Figma design, using the following technologies:

  • React.js
  • Typescript
  • Next.js
  • Tailwind CSS

The requirements for this project were very straightforward, however, I did face a few small challenges (more on that later):

  • Make it completely static (SSG)
  • Add dynamic elements: carousel, countdown and a slider

Let's break down the requirements one by one.


Making the website completely static

By itself, it's a very simple task. All it takes is adding one line in next.config.mjs:

/** @type {import('next').NextConfig} */
const nextConfig = {
    output: 'export',
}
 
export default nextConfig

However, things can get more complicated if the project makes use of images. Specifically, the <Image /> component Next.js provides. The reason being, is that if the site uses SSG, the <Image /> components are not optimized out of the box. In order to get all the "optimization juices" from <Image /> component, you have to provide a custom loader:

/** @type {import('next').NextConfig} */
const nextConfig = {
    output: 'export',
    images: {
        loader: 'custom',
        loaderFile: './app/image.ts',
    },
}
 
module.exports = nextConfig

After a few minutes of googling, I came across this next-image-export-optimizer package that solves this problem. It's essentially a script that generates various sizes for images. It's highly configurable and does the job pretty well! This script can be hooked into build process like so:

"scripts": {
  "dev": "next dev",
  "build": "next build && next-image-export-optimizer",
}

If you did everything correctly, this is the result you should get:

Optimized images

Notice that images are still optimized, even though it's an SSG site.



Dynamic elements on the site

Carousels & Sliders

I remember the days when plugging a carousel/slider onto a website was a task that I've spent days working on (I'm looking at you, jQuery)... Thankfully, nowadays it's a matter of installing a few npm packages and writing a few lines of code.


For this project, I went with Embla Carousel React, which I've actually found out about from the shadcn UI library. Later on, I saw Embla Carousel being implemented into my favorite Mantine UI as well.


What can I say... It really is a great package!


Countdown Timer

Countdown timer

The countdown timer for the GetTrainer landing.


The code for the countdown timer is a simple React hook called useCountdown that makes use of Javascript setInterval:

import { useCallback, useEffect, useState } from 'react'
 
const format = (num: number): string => (num < 10 ? `0${num}` : num.toString())
 
const startingPointDate = new Date(Date.UTC(2024, 6, 9, 3, 33)) // 09/06/2024 03:33
const timeUntilLaunch = 60 * 24 * 60 * 60 * 1000 // 60 days
const launchDate = startingPointDate.getTime() + timeUntilLaunch
 
const timeLeft = (launchDate - Date.now()) / 1000
 
export function useCountdown() {
    const [time, setTime] = useState(Math.max(0, Math.floor(timeLeft)))
 
    const decrement = useCallback(
        () => setTime((prevTime) => (prevTime === 0 ? 0 : prevTime - 1)),
        [],
    )
 
    useEffect(() => {
        const id = setInterval(decrement, 1000)
 
        return () => clearInterval(id)
    }, [decrement])
 
    // for hydration mismatch
    const [isMounted, setIsMounted] = useState(false)
    useEffect(() => setIsMounted(true), [setIsMounted])
    if (!isMounted || timeLeft <= 0) {
        return {
            days: '00',
            hours: '00',
            minutes: '00',
            seconds: '00',
        }
    }
 
    return {
        days: format(Math.floor(time / (3600 * 24))),
        hours: format(Math.floor((time / 3600) % 24)),
        minutes: format(Math.floor((time / 60) % 60)),
        seconds: format(time % 60),
    }
}

Looking back at this code now, I would probably use an array instead of an object for the returned payload. Reason being is that destructured array values are never going to cause a rerender in React components. This is a topic for an entire blog post on its own. But I digress...



Conclusion

Overall, it was an interesting project to work on and I'm very happy with the results.


Thanks for reading this case study!

Hug me brothaaa

Want to see how I helped other businesses? Head back to the Homepage to find more case studies just like this one!


✌️