✨ Meet EP - A Bespoke Platform For Uploading Event Photos, Powered By Netlify
Tags: netlifychallenge, devchallenge, webdev, javascript
Published: 2024-05-13 05:04:16

*This is a submission for the [Netlify Dynamic Site Challenge](https://dev.to/challenges/netlify): Visual Feast.*

-----

> In a hurry? Check out the [repo on GitHub](https://github.com/omzi/event-photos) or visit the app [here](https://ep.omzi.dev). Thanks!

-----

## 🛠 **What I Built**
👋🏽 Hi, everyone! Over the past 3 days, I worked on EP - a platform that lets you upload photos from events. I got the inspiration to build this from Vercel's [Next.js Conf 2022 site](https://nextjsconf-pics.vercel.app/) & Canva's dashboard card.

-----

## 😍 **Demo**
![Demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wviobzk0d274ttu4xepf.gif)

-----

## 🏚 **Platform Primitives**
I used Netlify Image CDN to ensure seamless resizing, format conversion, and quality optimization, all tailored to fit specific dimensions. I created helper functions to help me generate src sets for images.

```ts
const generateNetlifySrcSet = (url: string, width: number, height: number, isAbsolute: boolean = true) => {
  const breakpoints = [2400, 1600, 1080, 720, 480, 320];
  const baseURL = process.env.NEXT_PUBLIC_NETLIFY_URL || process.env.URL;

  return breakpoints.map(breakpoint => {
    const scaledWidth = Math.min(breakpoint, width);
    const scaledHeight = Math.round((height / width) * scaledWidth);
    const path = isAbsolute ? url : `/${url}`;
    return `${baseURL}/.netlify/images?url=${path}&fit=cover&w=${scaledWidth}&h=${scaledHeight} ${scaledWidth}w`;
  }).join(', ');
};
```

I also created another function to help me generate thumbnails in the blurhash format.

```ts
export const generateBlurhashThumbnailUrl = (photo: Photo, isAbsolute: boolean = true) => {
  const MAX_WIDTH = 150;
  const baseURL = process.env.NEXT_PUBLIC_NETLIFY_URL || process.env.URL;
  const path = isAbsolute ? photo.url : `/${photo.url}`;
  const scaledWidth = Math.min(photo.width, MAX_WIDTH);
  const scaledHeight = Math.round((scaledWidth * MAX_WIDTH) / MAX_WIDTH);

  const blurhashThumbnailUrl = `${baseURL}/.netlify/images?url=${path}&fit=cover&w=${scaledWidth}&h=${scaledHeight}&fm=blurhash`;
  return blurhashThumbnailUrl;
};
```

Unfortunately, I didn't end up using it because it didn't work as I expected. I expected a blurred out version of an image with the blurhash format but I got a visible version. This made me opt to use a Netlify function to generate the blurhashes myself. Learn more about [blurhash](https://blurha.sh/).

```js
const sharp = require('sharp');
const { encode } = require('blurhash');
const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient();

exports.handler = async (event, context) => {
	try {
		const requestBody = JSON.parse(event.body);
		const { url } = requestBody;

		// Convert image to buffer...
		const loadedImage = await fetch(url).then(response => response.blob()).then(blob => blob.arrayBuffer());

		const { data, info } = await sharp(Buffer.from(loadedImage))
			.ensureAlpha()
			.raw()
			.toBuffer({ resolveWithObject: true });

		const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4);

		await prisma.photo.update({
			where: { url: url },
			data: { blurhash: blurhash }
		});

		return { statusCode: 200, body: JSON.stringify({ message: 'Blurhash updated successfully' } )};
	} catch (error) {
		console.log('Netlify Function Error :>>', error);
		return { statusCode: 500, body: JSON.stringify({ message: 'Internal Server Error', error }) };
	} finally {
		await prisma.$disconnect();
	}
};
```

This worked pretty well until I another issue came up - timeout 😪. The function took about 30 seconds every time it ran, depending on the size of the image. As you may well know, functions on Netlify's free tier times out after 10 seconds. I then came up with a client-side alternative using Web Workers.

```ts
import { generateImageMetadata } from '#/lib/utils';
import { ImageMetadata, UploadResult, WorkerAction, WorkerResult, workersList } from '#/lib/types';

addEventListener('message', async (e: MessageEvent<string>) => {
  const { action, data } = e.data as unknown as WorkerAction<UploadResult>;
  if (action !== workersList.generateImageMetadata) {
    return;
  }

  const imageMetadata = await generateImageMetadata(`${data.preview}`);
  const result = {
    ...imageMetadata,
    url: data.url,
    thumbnailUrl: data.thumbnailUrl
  };
  
  const response = { data: result } as WorkerResult<ImageMetadata>;

  postMessage(response);
});

export {};
```

With this, it took about 15-20 seconds. I had to settle for this as it was the best option I had. Still haven't figured out why it took so long on a lambda function ;(.

The site was hosted on Netlify & I added a custom subdomain ([https://ep.omzi.dev](https://ep.omzi.dev)).

-----

### 💻 **Code Repository**

Link: [https://github.com/omzi/event-photos](https://github.com/omzi/event-photos)

{% embed https://github.com/omzi/event-photos %}

-----

### 🔗 **Project Link**

Link: [https://ep.omzi.dev](https://ep.omzi.dev)

-----

### ⚗️ **Test Credentials**

Here's a test admin credential for testing things out:

**Email Address**: admin@event.com
**Password**: NsIG]s{]QokX                     

Visit [here](https://ep.omzi.dev/auth) to login. Please be considerate of others & be careful what you upload there 🙏🏽. _Daalu!_

-----

### **✨ Conclusion**

Whew! This was shorter than I expected 👀. Thanks for making it this far! Feel free to give EP a try & let me know in the comment section below 😎.

<p align="center">
	<img width="100" src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/slrxrjid8fmuhwy9vime.png" alt="Itadori Yuuji">
</p>

I'm grateful to Netlify for organizing this hackathon. I learnt a couple of new stuff while working on EP 😁.

Have any constructive feedback for me? I’d love to know in the comments section below or via a [**Twitter DM**](https://twitter.com/messages/compose?recipient_id=915368574281244672&text=%F0%9F%91%8B%F0%9F%8F%BD%20Hello,%20Omzi.%20Good%20day%20to%20you.%0A%0A...) (I’d prefer this). Connect with me on Twitter ([**@0xOmzi**](https://twitter.com/intent/user?user_id=915368574281244672)).

***Mata ne*** ✌🏽