This is a submission for the Netlify Dynamic Site Challenge: Visual Feast.
In a hurry? Check out the repo on GitHub or visit the app here. 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 & Canva's dashboard card.
😍 Demo
🏚 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.
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.
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.
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.
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).
💻 Code Repository
Link: https://github.com/omzi/event-photos
omzi / event-photos
👥 EP (Event Photos) is a customizable platform for uploading photos from your events. ⚡ Powered by Next.js, Edgestore & Netlify.
👥 EP (Event Photos) is a customizable platform for uploading photos from your events.
⚡ Powered by Next.js, Edgestore & Netlify.
📜 About
EP (Event Photos) is a customizable platform for uploading photos from your events.
⚙️ Features
- Image Upload to Edgestore
- Bare-Bones Authentication
- Blurhash Generation
- Image Rendering using Netlify CDN
- Adding & Editing Event Details
- CRUD Access to Uploaded Files
- Gallery Lightbox
- User Feedback Mechanisms
- Enhanced User Interface
🛠 Tech Stack
- Hosting: Netlify
- CDN: Netlify CDN
- Full Stack Framework: Next.js
- Database: Neon (w/ Prisma)
- Authentication: JWT Cookies (w/ Jose)
- File Storage: Edgestore
- Styling: Tailwind CSS
- Programming Language: TypeScript
🚩 Prerequisites
Ensure that your system meets the following requirements:
⚡ Installation
Before proceeding, make sure your system satisfies the prerequisites mentioned above.
Firstly, clone the EP repository into your desired folder and navigate into it:
$ git
…🔗 Project Link
Link: 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 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 😎.
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 (I’d prefer this). Connect with me on Twitter (@0xOmzi).
Mata ne ✌🏽
Top comments (2)
Nice work!
Thanks Phil!