Next up, we'll build the front-end.js script. First setup an event listener for the "Fetch NFTs" button to send the address to our API route (more on this when we build back-end.js). Then the functions for displaying the NFTs, user selecting the images, previewing those images, and finally creating the GIF once the user clicks on the "Make GIF" button. This is an async function that checks for a valid address, otherwise alert the user. If valid, then await the json response and send that data to the next function -> displayNfts, otherwise catch the error. document.addEventListener('DOMContentLoaded', () => { document.getElementById('fetchNfts').addEventListener('click', fetchAndDisplayNFTs); }); let selectedImagesData = []; async function fetchAndDisplayNFTs() { const ensAddress = document.getElementById('ensAddress').value.trim(); if (!ensAddress) { alert('Please enter a valid ENS address.'); return; } try { const response = await fetch(`/api/fetch-nfts?ensAddress=${encodeURIComponent(ensAddress)}`); const data = await response.json(); displayNfts(data); } catch (error) { console.error('Error fetching NFTs:', error); } } This function setups the DOM modifications to update the HTML with the ID nftGallery, creates an object for each of the NFTs received by the script, a fallback image and sets an event listener for a 'click' to add a class 'selected' to each of the images. function displayNfts(data) { const gallery = document.getElementById('nftGallery'); gallery.innerHTML = ''; gallery.classList.add('carousel'); Object.keys(data).forEach(blockchainKey => { const tokenBalances = data[blockchainKey].TokenBalance; tokenBalances.forEach(tokenBalance => { const imageUrl = tokenBalance.tokenNfts?.contentValue?.image?.medium || 'fallback-image-url.png'; const imgElement = document.createElement('img'); imgElement.src = imageUrl; imgElement.alt = tokenBalance.token?.name || 'NFT Image'; imgElement.classList.add('nft-item'); imgElement.addEventListener('click', function() { this.classList.toggle('selected'); handleImageSelection(this, imageUrl); }); gallery.appendChild(imgElement); }); }); } Now that we have the HTML and javascript for handling the NFT images and selecting them, we'll need a way to create a GIF out of those images. This function previews of the images that will be used for the GIF and displays them next to the "Make GIF" button. function updatePreviewAndButtonVisibility() { const previewArea = document.getElementById('selectedImagesPreview'); previewArea.innerHTML = ''; selectedImagesData.forEach(item => { const imgElement = document.createElement('img'); imgElement.src = item.dataURL; imgElement.classList.add('preview-image'); previewArea.appendChild(imgElement); }); const createGifButton = document.getElementById('createGifButton'); if (selectedImagesData.length > 0) { createGifButton.classList.remove('hidden'); } else { createGifButton.classList.add('hidden'); } } Now that we have a list of images selected and previewed, we'll need a way to create the GIF from that array. GIF.js is an awesome library that let's you create GIFs right in the browser. We'll include the CDN file for this library. Just add this tag below our style.css: Let's set up the functions that "Make GIF" calls. This function will create the GIFs from a dataUrl that allows the browser to store "data blobs". This is due to browser security that prevents tainting an HTML Canvas with images urls from different domains. In this case, we're fetching the images from Airstack, but serving the Canvas on our hosted domain. This is a CORS workaround that will allow us to store the images from Airstack and use them to create the GIF. First up is the function to convert the selected images into a data blob. function convertImageToDataURL(imageSrc, callback) { const img = new Image(); img.crossOrigin = 'Anonymous'; img.onload = function() { const size = Math.min(img.width, img.height); const canvas = document.createElement('canvas'); canvas.width = canvas.height = 300; const ctx = canvas.getContext('2d'); const x = (canvas.width / 2) - (img.width / 2) * (size / img.width); const y = (canvas.height / 2) - (img.height / 2) * (size / img.height); ctx.drawImage(img, x, y, img.width * (size / img.width), img.height * (size / img.height)); const dataURL = canvas.toDataURL('image/png'); callback(dataURL); }; img.src = imageSrc; } And this function takes those data blobs and uses GIF.js and webworkers to sequentially add the images to the GIF with a 200ms delay between the images, completes the rendering of the GIF, updates the preview image, adds a download button, and scrolls to the bottom of the page. function createGifFromDataUrls(dataUrls) { if (dataUrls.length === 0) { console.log('No images selected for GIF creation.'); return; } console.log('Create GIF button clicked'); const gif = new GIF({ workers: 2, quality: 10, workerScript: 'gif.worker.js', width: 300, height: 300, }); let loadCount = 0; dataUrls.forEach(dataUrl => { const img = new Image(); img.onload = () => { console.log('Adding image to GIF'); gif.addFrame(img, { delay: 200 }); loadCount++; if (loadCount === dataUrls.length) { console.log('All images loaded, starting GIF render...'); gif.render(); } }; img.src = dataUrl; }); gif.on('finished', function(blob) { const url = URL.createObjectURL(blob); let gifContainer = document.getElementById('gifContainer'); if (!gifContainer) { gifContainer = document.createElement('div'); gifContainer.id = 'gifContainer'; gifContainer.classList.add('flex', 'flex-col', 'items-center', 'mt-4'); document.body.appendChild(gifContainer); } else { gifContainer.innerHTML = ''; } const previewImg = document.createElement('img'); previewImg.src = url; gifContainer.appendChild(previewImg); const downloadLink = document.createElement('a'); downloadLink.href = url; downloadLink.download = 'nft-collection.gif'; downloadLink.textContent = 'Download GIF'; downloadLink.id = 'download'; downloadLink.classList.add('mt-8', 'bg-green-500', 'hover:bg-green-700', 'text-white', 'font-bold','py-2','px-4','rounded'); gifContainer.appendChild(downloadLink); scrollToPageBottom(); }); }; We've completed the front-end.js so we can handle the images coming from our API call, a way to select and preview those images and finally a method to create the GIF and download it. Note on Deployment: Currently our app is structured with our HTML, CSS, and front-end.js in our public folder, with our back-end.js in our root. We'll need to move and rename back-end.js to api/index.ts. This is a deployment detail for Vercel in order to uses edge functions. Learn more here: Express.js Guide. Let's install the needed packages and spin up a local dev version so we can take a look at what we've built. Run this from the root of the project. npm install && node api/index.ts Now we can navigate to in our browser to https://localhost:3000 and check out our work. App running on localhost:3000 Looking good! Now that we've verified that it's working, we can focus on deployment. We'll need Vercel and Github accounts, both free version will work for our purposes. Fork our project into your own account. https://github.comGitHub - robertcedwards/Airstack-NFT-GIF-Builder: A tool built with Airstack to create an animated GIF from NFTs A tool built with Airstack to create an animated GIF from NFTs - robertcedwards/Airstack-NFT-GIF-Builder Once you have your fork you'll proceed to Vercel to import and deploy from there. Login to your Vercel account and click Add new - Project. You'll see the screen below, import our Airstack-NFT-GIF-Builder repo. Importing our Project from Github in to Vercel We'll need to add an Environment Variable for our Airstack API Key. Add the key "AIRSTACK_API_KEY" and your API key in the value field. Vercel will auto-detect the rest of our configuration, so all we need to do it hit the Deploy button. Setting the Environment Variable for Airstack API + Deploy Congrats 🚀🎉 - You've deployed a Airstacked powered NFT GIF maker! From concept to ideation, back-end to front-end, we've come a long way. But hopefully this project helped you to better understand how to create any blockchain based project using the power of Airstack and modern build & deployment tools. If you need any help, feel free to reach out on Warpcast. My DCs are open! Now get out there and build something with Airstack and share it in the /airstack channel!
## Publication Information
- [0xHashbrown](https://paragraph.com/@0xhashbrown/): Publication homepage
- [All Posts](https://paragraph.com/@0xhashbrown/): More posts from this publication
- [RSS Feed](https://api.paragraph.com/blogs/rss/@0xhashbrown): Subscribe to updates
- [Twitter](https://twitter.com/robertcedwards): Follow on Twitter
## Optional
- [Collect as NFT](https://paragraph.com/@0xhashbrown/airstack-nft-gif): Support the author by collecting this post
- [View Collectors](https://paragraph.com/@0xhashbrown/airstack-nft-gif/collectors): See who has collected this post