Netlify Image Game 🩻🕹️
Tags: netlifychallenge, devchallenge, webdev, javascript
Published: 2024-05-11 04:45:18

*This is a submission for the [Netlify Dynamic Site Challenge](https://image-guess.netlify.app/): Visual Feast.*

You can find it here: [Image Guess](https://image-guess.netlify.app/)

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d31um9q3zx1adhgt7v8o.gif)

### Leaderboard

The leaderboard for the current challenge

![Keeping score with leaderboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h2h62fu10rt3gipr9nqx.png)

### Gallery from previous Challenges

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

![Gallery of previous images](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o7mxgiiibtgtblk6lup0.png)


## Platform Primitives
I've leveraged Netlify's [CDN](https://docs.netlify.com/image-cdn/overview/) 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:

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

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

```ts
 <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",
                    }}
                />
```

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](https://docs.netlify.com/blobs/overview/). 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ma5gdepz7ff7fmc3a21.png)

### 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**:
```ts
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});
    }
};
```

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 😁