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.
- Guess what the subject is in the image.
- If you guess wrong, the image gets clearer.
- Lose points for guessing wrong. Earn points for speed.
Demo
The game in action
Getting the answer wrong makes the image clearer.
Leaderboard
The leaderboard for the current challenge
Gallery from previous Challenges
After each day is over, the images will be available for everyone.
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}`;
}
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",
}}
/>
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.
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});
}
};
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 😁