Netlify Image Game 🩻🕹️

Matt Lewandowski - May 11 - - Dev Community

This is a submission for the Netlify Dynamic Site Challenge: Visual Feast.

You can find it here: Image Guess

What I Built

I built a text and image based game where an image starts off very pixelated and really low quality h=1px w=1px q=0. You must guess what the subject of the image is. If you get the answer wrong, the image gets clearer and clearer.

There are 10 images to guess, and new images every day. Think of it like Wordle for images. Once you have guessed for the day, your name will be added to the Leaderboard!

Images from previous challenges get added to the Gallery. Because I needed to include a Gallery 😅😁. You can play around with the quality of the images, just like you would in the game. Hovering over the image reveals the word.

The game is simple.

  1. Guess what the subject is in the image.
  2. If you guess wrong, the image gets clearer.
  3. Lose points for guessing wrong. Earn points for speed.

Demo

The game in action

Getting the answer wrong makes the image clearer.

Gif of getting the answer wrong a lot

Leaderboard

The leaderboard for the current challenge

Keeping score with leaderboard

Gallery from previous Challenges

After each day is over, the images will be available for everyone.

Gallery of previous images

Platform Primitives

I've leveraged Netlify's CDN by using the NextJS <Image/> component. I had implemented a custom loader, before realising that you won't need to do this with Netlify. However, if anyone is planning on using Netlify's CDN outside of a Netlify, you can easily do so with this:



const customLoader = ({
                          src,
                          width,
                          quality,
                      }: ImageLoaderProps) => {
    const encodedSrc = encodeURIComponent(src);
    return `https://image-guess.netlify.app/.netlify/images?url=${encodedSrc}&w=${width}&q=${quality}`;
}


Enter fullscreen mode Exit fullscreen mode

Saying that I used the NextJS image component on it's own is boring though, so lets look at how I used it.



 <Image
                    loading={"eager"}
                    alt={"image to be guessed"}
                    width={10 * (attempt) || 1}
                    height={10 * attempt || 1}
                    quality={attempt * 5}
                    style={{
                        width: "100%",
                        height: "100%",
                        aspectRatio: "1/1",
                        objectFit: "contain",
                    }}
                />


Enter fullscreen mode Exit fullscreen mode

Every round starts off with attempt = 0. This means that I can utilize the CDN to return me really horrible quality photos, that are impossible to make up. After a failed attempt, we increase the quality and size of the image. Making it more distinguishable overall.

More Platform Primitives

The images in this game are also hosted in Netlify as blobs. There is an admin page where I have the ability to create new challenges, for different days. Creating a challenge requires 10 images, a title, and description.

These images are all uploaded to netlify as blobs.

Blob storage for admin page

Netlify Blobs and CDN

This part was kind of tricky to figure out. I needed a way to use my blobs stored in Netlify, from the CDN. I ran into a lot of weird issues when implementing this, but it worked out in the end. Here is the endpoint I created in NextJS, that will pull the image from the blob storage, for the CDN to use.

EDIT: Just updating how I handled fetching the images from the blob storage. TLDR is that if you use query parameters for the imageId, the caching will break. The caching seems to work per endpoint, ignoring the query parameters. So we utilize dynamic routes instead.

The route: /challenge/api/image/[id]/route.ts

The handler:



export const GET = async (_: Request, {params}: { params: { id: string } }) => {
    const id = params.id;

    try {
        if (!id) {
            return NextResponse.json({Message: "Image ID not found", status: 400});
        }

        const blobStore = getStore({name: 'images', consistency: 'strong'});
        const image = await blobStore.get(id, {consistency: "strong", type: "blob"});

        if (!image) {
            return NextResponse.json({Message: "Image not found", status: 404});
        }

        return new NextResponse(image, {
            status: 200,
            statusText: "OK",
            headers: {
                'Content-Type': 'image/webp',
                "Cache-Control": "public, max-age=604800, must-revalidate",
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
                'Access-Control-Allow-Headers': 'Content-Type, Authorization',
            },
        });
    } catch (error) {
        return NextResponse.json({Message: "Failed for some reason", status: 500, error: error});
    }
};


Enter fullscreen mode Exit fullscreen mode

That is it! Hope you enjoy 🙌

Oh and if anyone is interested in the repo, I'll share it after the challenge. I didn't have a lot of time to work on this and the code quality is pretty embarrassing. Just want to clean it up first 😁

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .