try this again?
b4228f69
20 file(s) · +749 −649
| 11 | 11 | const siteTitle = `${title} ${titleSeparator} ${siteConfig.title}`; |
|
| 12 | 12 | const canonicalURL = new URL(Astro.url.pathname, Astro.site); |
|
| 13 | 13 | const socialImageURL = new URL(ogImage ? ogImage : "/social-card.png", Astro.url).href; |
|
| 14 | - | ||
| 15 | 14 | --- |
|
| 15 | + | ||
| 16 | 16 | <meta charset="utf-8" /> |
|
| 17 | 17 | <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" /> |
|
| 18 | 18 | <meta http-equiv="X-UA-Compatible" content="IE=edge" /> |
| 1 | 1 | --- |
|
| 2 | - | const { link, site, image } = Astro.props |
|
| 2 | + | const { link, site, image } = Astro.props; |
|
| 3 | 3 | import { Image } from "@astrojs/image/components"; |
|
| 4 | 4 | --- |
|
| 5 | 5 | ||
| 6 | - | <a class="flex justify-start items-center gap-2 font-bold" href={link} target="_blank"> |
|
| 7 | - | <Image src={image} width={30} height={30} alt={`Link to ${site}`} /> |
|
| 8 | - | Read this post on {site} |
|
| 6 | + | <a class="flex items-center justify-start gap-2 font-bold" href={link} target="_blank"> |
|
| 7 | + | <Image src={image} width={30} height={30} alt={`Link to ${site}`} /> |
|
| 8 | + | Read this post on {site} |
|
| 9 | 9 | </a> |
| 5 | 5 | tags: ["web3", "nfts", "tutorials", "web development"] |
|
| 6 | 6 | ogImage: "https://global-uploads.webflow.com/629e4fe96456f8219203e7f1/6410b6848afd85df8fe0a193_2023-01-10_How-to-Create_blog-img-tiny.png" |
|
| 7 | 7 | --- |
|
| 8 | + | ||
| 8 | 9 | import { Image } from "@astrojs/image/components"; |
|
| 9 | 10 | import pinnie from "../../assets/pinnie.png"; |
|
| 10 | - | import OutLinkButton from "../../components/OutLinkButton.astro" |
|
| 11 | + | import OutLinkButton from "../../components/OutLinkButton.astro"; |
|
| 11 | 12 | ||
| 12 | - | <OutLinkButton link="https://www.pinata.cloud/blog/resume-app-nft" site="Pinata" image={pinnie} /> |
|
| 13 | + | <OutLinkButton link="https://www.pinata.cloud/blog/resume-app-nft" site="Pinata" image={pinnie} />{" "} |
|
| 13 | 14 | ||
| 14 | 15 | Despite the current market conditions, web3 jobs are still on the rise. But competition is at an all-time high. Sure you can build a portfolio of projects to help display your skills, but that’s what everyone else is doing too. What can you do to stand out among the crowds of developers out there like yourself? How are you thinking outside the box? |
|
| 15 | 16 | ||
| 35 | 36 | ||
| 36 | 37 | As said in Justin’s tutorial, you can use a free Pinata account to do all of this, but to truly harness the full user experience of this app, using a paid account with a Dedicated Gateway will make this app much faster than if you used a public gateway or depended on external sources. Here is an example of the application through Steve’s Dedicated Gateway: |
|
| 37 | 38 | ||
| 38 | - | <iframe src="https://stevedsimkins.mypinata.cloud/ipfs/QmdKYQpczE7giv15Yx2tkk1pkbRe862eaLhTR5e7FjhJ8F/index.html" frameborder="0" height="500px" width="100%" class="hidden sm:block" /> |
|
| 39 | + | <iframe |
|
| 40 | + | src="https://stevedsimkins.mypinata.cloud/ipfs/QmdKYQpczE7giv15Yx2tkk1pkbRe862eaLhTR5e7FjhJ8F/index.html" |
|
| 41 | + | frameborder="0" |
|
| 42 | + | height="500px" |
|
| 43 | + | width="100%" |
|
| 44 | + | class="hidden sm:block" |
|
| 45 | + | /> |
|
| 39 | 46 | <Image |
|
| 40 | - | src="https://global-uploads.webflow.com/629e4fe96456f8219203e7f1/63bd95503c654e14fc1b3b00_Slide%2016_9%20-%203.png" |
|
| 41 | - | alt="Screenshot of web app" |
|
| 42 | - | width={1920} |
|
| 43 | - | aspectRatio={1/1} |
|
| 44 | - | class="sm:hidden" |
|
| 47 | + | src="https://global-uploads.webflow.com/629e4fe96456f8219203e7f1/63bd95503c654e14fc1b3b00_Slide%2016_9%20-%203.png" |
|
| 48 | + | alt="Screenshot of web app" |
|
| 49 | + | width={1920} |
|
| 50 | + | aspectRatio={1 / 1} |
|
| 51 | + | class="sm:hidden" |
|
| 45 | 52 | /> |
|
| 46 | 53 | ||
| 47 | 54 | Awesome! Resume app is done, time to turn it into an NFT. Following the guide, Steve created a simple smart contract to deploy the App NFT in a way that he could keep updating it with new versions, very handy if you ever have an update in experience or education. Also keep in mind you can do your own smart contract customization here to do cool stuff, like mass airdrop your NFT to a list of wallets or ENS addresses! |
|
| 50 | 57 | ||
| 51 | 58 | ```json |
|
| 52 | 59 | { |
|
| 53 | - | "name": "Steve's App NFT Resume", |
|
| 54 | - | "description": "A dynamic NFT resume by Steve", |
|
| 55 | - | "image": "ipfs://QmTa46bKHxcQCBoNt887X2zNJwAHpAZ93hTXDi9KeJeM4W", |
|
| 56 | - | "animation_url": "https://stevedsimkins.mypinata.cloud/ipfs/QmdKYQpczE7giv15Yx2tkk1pkbRe862eaLhTR5e7FjhJ8F/index.html" |
|
| 60 | + | "name": "Steve's App NFT Resume", |
|
| 61 | + | "description": "A dynamic NFT resume by Steve", |
|
| 62 | + | "image": "ipfs://QmTa46bKHxcQCBoNt887X2zNJwAHpAZ93hTXDi9KeJeM4W", |
|
| 63 | + | "animation_url": "https://stevedsimkins.mypinata.cloud/ipfs/QmdKYQpczE7giv15Yx2tkk1pkbRe862eaLhTR5e7FjhJ8F/index.html" |
|
| 57 | 64 | } |
|
| 58 | 65 | ``` |
|
| 59 | 66 | ||
| 60 | 67 | With the metadata.json file complete, Steve uploaded that file to Pinata as well and used the CID as the token URI like so: |
|
| 61 | 68 | ||
| 62 | 69 | ```javascript |
|
| 63 | - | const URI = "ipfs://QmU85vmit8ShrUpnJFg3wEAMA61GcQB2X5KcgabchDV1kt" |
|
| 70 | + | const URI = "ipfs://QmU85vmit8ShrUpnJFg3wEAMA61GcQB2X5KcgabchDV1kt"; |
|
| 64 | 71 | ``` |
|
| 65 | 72 | ||
| 66 | 73 | That’s it! After Steve ran the deployment command with Hardhat, the NFT had been minted in his wallet where we could see it on OpenSea! [Check it out](https://testnets.opensea.io/assets/goerli/0x45602432657d8100119e8633b677043b9022c22b/1) 😎 |
|
| 7 | 7 | --- |
|
| 8 | 8 | ||
| 9 | 9 | import { Image } from "@astrojs/image/components"; |
|
| 10 | - | import medium from "../../assets/medium.png" |
|
| 11 | - | import OutLinkButton from "../../components/OutLinkButton.astro" |
|
| 10 | + | import medium from "../../assets/medium.png"; |
|
| 11 | + | import OutLinkButton from "../../components/OutLinkButton.astro"; |
|
| 12 | 12 | ||
| 13 | - | <OutLinkButton link="https://medium.com/pinata/how-to-scan-and-create-1-1-3d-nfts-on-solana-using-polycam-and-pinata-df513dd87937" site="Medium" image={medium} /> |
|
| 13 | + | <OutLinkButton |
|
| 14 | + | link="https://medium.com/pinata/how-to-scan-and-create-1-1-3d-nfts-on-solana-using-polycam-and-pinata-df513dd87937" |
|
| 15 | + | site="Medium" |
|
| 16 | + | image={medium} |
|
| 17 | + | /> |
|
| 14 | 18 | ||
| 15 | 19 | The growth and evolution of NFTs has come a long way from the early days. Some of the early NFT projects were simple .png files or a link to a YouTube video, but now they are an entire industry that consumes brand, utility, even augmented reality. Metaverses, 3D objects, and other virtual reality experiences are all the rage, and I firmly believe we will see more of this in the near future. |
|
| 16 | 20 | ||
| 34 | 38 | ||
| 35 | 39 | With the video below you can get glimpse of how the LiDAR scanner maps over surfaces. Taking it real slow and covering every angle really helps with scanning environments. |
|
| 36 | 40 | ||
| 37 | - | <div style="padding:216.22% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/703877466?h=9659ba4952" style="position:absolute;top:0;left:0;width:100%;height:100%;" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe></div><script src="https://player.vimeo.com/api/player.js"></script> |
|
| 41 | + | <div style="padding:216.22% 0 0 0;position:relative;"> |
|
| 42 | + | <iframe |
|
| 43 | + | src="https://player.vimeo.com/video/703877466?h=9659ba4952" |
|
| 44 | + | style="position:absolute;top:0;left:0;width:100%;height:100%;" |
|
| 45 | + | frameborder="0" |
|
| 46 | + | allow="autoplay; fullscreen; picture-in-picture" |
|
| 47 | + | allowfullscreen |
|
| 48 | + | ></iframe> |
|
| 49 | + | </div> |
|
| 50 | + | <script src="https://player.vimeo.com/api/player.js"></script> |
|
| 38 | 51 | ||
| 39 | 52 | After you’re done scanning there will be a processing step that will take all the data and map it into a 3D environment! |
|
| 40 | 53 | ||
| 41 | - | <div style="padding:216.22% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/703880632?h=52b5f574b7" style="position:absolute;top:0;left:0;width:100%;height:100%;" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe></div><script src="https://player.vimeo.com/api/player.js"></script> |
|
| 54 | + | <div style="padding:216.22% 0 0 0;position:relative;"> |
|
| 55 | + | <iframe |
|
| 56 | + | src="https://player.vimeo.com/video/703880632?h=52b5f574b7" |
|
| 57 | + | style="position:absolute;top:0;left:0;width:100%;height:100%;" |
|
| 58 | + | frameborder="0" |
|
| 59 | + | allow="autoplay; fullscreen; picture-in-picture" |
|
| 60 | + | allowfullscreen |
|
| 61 | + | ></iframe> |
|
| 62 | + | </div> |
|
| 63 | + | <script src="https://player.vimeo.com/api/player.js"></script> |
|
| 42 | 64 | ||
| 43 | 65 | Once it’s processed you can go ahead and view the 3D model/environment! Polycam really is first class; you can create videos, you can view the model in Augmented Reality, it just has so many awesome features! My model isn’t the cleanest since I have so many small detailed objects on my desk, but for something a bit simpler this feature is amazing. |
|
| 44 | 66 | ||
| 45 | - | <div style="padding:216.22% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/703884780?h=de95c5de7a" style="position:absolute;top:0;left:0;width:100%;height:100%;" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe></div><script src="https://player.vimeo.com/api/player.js"></script> |
|
| 67 | + | <div style="padding:216.22% 0 0 0;position:relative;"> |
|
| 68 | + | <iframe |
|
| 69 | + | src="https://player.vimeo.com/video/703884780?h=de95c5de7a" |
|
| 70 | + | style="position:absolute;top:0;left:0;width:100%;height:100%;" |
|
| 71 | + | frameborder="0" |
|
| 72 | + | allow="autoplay; fullscreen; picture-in-picture" |
|
| 73 | + | allowfullscreen |
|
| 74 | + | ></iframe> |
|
| 75 | + | </div> |
|
| 76 | + | <script src="https://player.vimeo.com/api/player.js"></script> |
|
| 46 | 77 | ||
| 47 | 78 | Now that we got our model, we can upload it to the cloud and look at it through our web browser! Just click on the little cloud icon on the top of the screen when viewing your model and it will be uploaded to your cloud storage with Polycam. |
|
| 48 | 79 | ||
| 49 | 80 | Once viewing it in the web browser, we can download the model by clicking “export.” |
|
| 50 | 81 | ||
| 51 | 82 | <Image |
|
| 52 | - | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*Olex5OAd3sMaugwF_iWGig.png" |
|
| 53 | - | alt="screenshot of room capture" |
|
| 54 | - | width={1920} |
|
| 55 | - | aspectRatio={16/9} |
|
| 83 | + | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*Olex5OAd3sMaugwF_iWGig.png" |
|
| 84 | + | alt="screenshot of room capture" |
|
| 85 | + | width={1920} |
|
| 86 | + | aspectRatio={16 / 9} |
|
| 56 | 87 | /> |
|
| 57 | 88 | ||
| 58 | 89 | From there we want to select the GLTF format and start the download! |
|
| 59 | 90 | ||
| 60 | 91 | <Image |
|
| 61 | - | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*0z9cGRbGbAWoflqk7rU0yw.png" |
|
| 62 | - | alt="screenshot of aerial export screen" |
|
| 63 | - | width={1920} |
|
| 64 | - | aspectRatio={16/9} |
|
| 92 | + | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*0z9cGRbGbAWoflqk7rU0yw.png" |
|
| 93 | + | alt="screenshot of aerial export screen" |
|
| 94 | + | width={1920} |
|
| 95 | + | aspectRatio={16 / 9} |
|
| 65 | 96 | /> |
|
| 66 | 97 | ||
| 67 | 98 | Ok so we got our 3D file for our environment, now lets do some 3D objects as well! The process is pretty similar, except for 3D objects we’ll switch to the “photo” mode instead of the LiDAR mode. The photo mode will take a bunch of different pictures and put them together to make a 3D model! In the video below you can see what the process looks like as I scan one of my prized possessions, a 1930's edition of Moby Dick illustrated by Rockwell Kent. |
|
| 68 | 99 | ||
| 69 | - | <div style="padding:216.22% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/704181596?h=92cada3864" style="position:absolute;top:0;left:0;width:100%;height:100%;" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe></div><script src="https://player.vimeo.com/api/player.js"></script> |
|
| 100 | + | <div style="padding:216.22% 0 0 0;position:relative;"> |
|
| 101 | + | <iframe |
|
| 102 | + | src="https://player.vimeo.com/video/704181596?h=92cada3864" |
|
| 103 | + | style="position:absolute;top:0;left:0;width:100%;height:100%;" |
|
| 104 | + | frameborder="0" |
|
| 105 | + | allow="autoplay; fullscreen; picture-in-picture" |
|
| 106 | + | allowfullscreen |
|
| 107 | + | ></iframe> |
|
| 108 | + | </div> |
|
| 109 | + | <script src="https://player.vimeo.com/api/player.js"></script> |
|
| 70 | 110 | ||
| 71 | 111 | Once we finish scanning the book, I like to select the higher end of the quality allowance, and I like to use the object masking to get all the fine details. These photo mode objects are a bit more work, so when you take one Polycam sends it off to a server for higher powered processing. Due to how intense the work is, you can only take 150 of these a month, but I don’t think that will be an issue for most people. Once they finish processing it, the model looks like this! |
|
| 72 | 112 | ||
| 73 | - | <div style="padding:216.22% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/704185926?h=2a929c3d73" style="position:absolute;top:0;left:0;width:100%;height:100%;" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe></div><script src="https://player.vimeo.com/api/player.js"></script> |
|
| 113 | + | <div style="padding:216.22% 0 0 0;position:relative;"> |
|
| 114 | + | <iframe |
|
| 115 | + | src="https://player.vimeo.com/video/704185926?h=2a929c3d73" |
|
| 116 | + | style="position:absolute;top:0;left:0;width:100%;height:100%;" |
|
| 117 | + | frameborder="0" |
|
| 118 | + | allow="autoplay; fullscreen; picture-in-picture" |
|
| 119 | + | allowfullscreen |
|
| 120 | + | ></iframe> |
|
| 121 | + | </div> |
|
| 122 | + | <script src="https://player.vimeo.com/api/player.js"></script> |
|
| 74 | 123 | ||
| 75 | 124 | The download process looks exactly the same as we did the desk, very easy! Also while you’re still in Polycam, you can edit, adjust, crop, etc. your model all inside their app or website, so that way when the model is exported it’s ready to be turned into an NFT! |
|
| 76 | 125 | ||
| 89 | 138 | Let’s log into Pinata and upload our file. Really simple, just click on the upload button in the top left, give it a name, and upload! |
|
| 90 | 139 | ||
| 91 | 140 | <Image |
|
| 92 | - | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*v-x8ltZQ0ep6vlmqoPn2WA.png" |
|
| 93 | - | alt="uploading screen at pinata" |
|
| 94 | - | width={1920} |
|
| 95 | - | aspectRatio={16/9} |
|
| 141 | + | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*v-x8ltZQ0ep6vlmqoPn2WA.png" |
|
| 142 | + | alt="uploading screen at pinata" |
|
| 143 | + | width={1920} |
|
| 144 | + | aspectRatio={16 / 9} |
|
| 96 | 145 | /> |
|
| 97 | 146 | ||
| 98 | 147 | Then we just need to choose a subdomain and make sure its available. If it is, click next! |
|
| 119 | 168 | ||
| 120 | 169 | ```json |
|
| 121 | 170 | { |
|
| 122 | - | "name": "3D Pinnie", |
|
| 123 | - | "symbol": "PIN", |
|
| 124 | - | "description": "A 3D scan of Pinnie taken with Polycam", |
|
| 125 | - | "seller_fee_basis_points": 0, |
|
| 126 | - | "image": "null", |
|
| 127 | - | "animation_url": "https://pinnieblog.mypinata.cloud/ipfs/QmWmcX9ikvNTJtCmuX9oWiUvTABQZ6tC6UARqa7ozBh2Ry?filename=Pinnie.glb", |
|
| 128 | - | "external_url": "https://pinata.cloud", |
|
| 129 | - | "collection": { |
|
| 130 | - | "name": "Pinnie's 3D NFTs", |
|
| 131 | - | "family": "3D NFTs" |
|
| 132 | - | }, |
|
| 133 | - | "properties": { |
|
| 134 | - | "files": [ |
|
| 135 | - | { |
|
| 136 | - | "uri": "null", |
|
| 137 | - | "type": "image/png" |
|
| 138 | - | }, |
|
| 139 | - | { |
|
| 140 | - | "uri": "https://pinnieblog.mypinata.cloud/ipfs/QmWmcX9ikvNTJtCmuX9oWiUvTABQZ6tC6UARqa7ozBh2Ry?filename=Pinnie.glb", |
|
| 141 | - | "type": "vr/glb" |
|
| 142 | - | } |
|
| 143 | - | ], |
|
| 144 | - | "category": "3D", |
|
| 145 | - | "creators": [ |
|
| 146 | - | { |
|
| 147 | - | "address": "D8KLFUfnRwGsMt6n56FzkyRYmVUQXiRnJWFV7rZYCYdd", |
|
| 148 | - | "share": 100 |
|
| 149 | - | } |
|
| 150 | - | ] |
|
| 151 | - | } |
|
| 171 | + | "name": "3D Pinnie", |
|
| 172 | + | "symbol": "PIN", |
|
| 173 | + | "description": "A 3D scan of Pinnie taken with Polycam", |
|
| 174 | + | "seller_fee_basis_points": 0, |
|
| 175 | + | "image": "null", |
|
| 176 | + | "animation_url": "https://pinnieblog.mypinata.cloud/ipfs/QmWmcX9ikvNTJtCmuX9oWiUvTABQZ6tC6UARqa7ozBh2Ry?filename=Pinnie.glb", |
|
| 177 | + | "external_url": "https://pinata.cloud", |
|
| 178 | + | "collection": { |
|
| 179 | + | "name": "Pinnie's 3D NFTs", |
|
| 180 | + | "family": "3D NFTs" |
|
| 181 | + | }, |
|
| 182 | + | "properties": { |
|
| 183 | + | "files": [ |
|
| 184 | + | { |
|
| 185 | + | "uri": "null", |
|
| 186 | + | "type": "image/png" |
|
| 187 | + | }, |
|
| 188 | + | { |
|
| 189 | + | "uri": "https://pinnieblog.mypinata.cloud/ipfs/QmWmcX9ikvNTJtCmuX9oWiUvTABQZ6tC6UARqa7ozBh2Ry?filename=Pinnie.glb", |
|
| 190 | + | "type": "vr/glb" |
|
| 191 | + | } |
|
| 192 | + | ], |
|
| 193 | + | "category": "3D", |
|
| 194 | + | "creators": [ |
|
| 195 | + | { |
|
| 196 | + | "address": "D8KLFUfnRwGsMt6n56FzkyRYmVUQXiRnJWFV7rZYCYdd", |
|
| 197 | + | "share": 100 |
|
| 198 | + | } |
|
| 199 | + | ] |
|
| 200 | + | } |
|
| 152 | 201 | } |
|
| 153 | 202 | ``` |
|
| 154 | 203 | ||
| 156 | 205 | ||
| 157 | 206 | ```json |
|
| 158 | 207 | { |
|
| 159 | - | "uri": "https://pinnieblog.mypinata.cloud/ipfs/QmWmcX9ikvNTJtCmuX9oWiUvTABQZ6tC6UARqa7ozBh2Ry?filename=Pinnie.glb", |
|
| 160 | - | "type": "vr/glb" |
|
| 208 | + | "uri": "https://pinnieblog.mypinata.cloud/ipfs/QmWmcX9ikvNTJtCmuX9oWiUvTABQZ6tC6UARqa7ozBh2Ry?filename=Pinnie.glb", |
|
| 209 | + | "type": "vr/glb" |
|
| 161 | 210 | } |
|
| 162 | 211 | ``` |
|
| 163 | 212 | ||
| 2 | 2 | title: "Arc: The Internet Computer" |
|
| 3 | 3 | publishDate: "08 Mar 2023" |
|
| 4 | 4 | description: "How the Arc web browser is paving the way for the future of consumer computers" |
|
| 5 | - | tags: ["arc", "web browsers", "tech philosophy", "internet", ] |
|
| 5 | + | tags: ["arc", "web browsers", "tech philosophy", "internet"] |
|
| 6 | 6 | ogImage: "https://res.cloudinary.com/df9dofjus/image/upload/v1678385122/arc-browser-blog-post/opluqtxq1ceoigepyjwf.png" |
|
| 7 | 7 | --- |
|
| 8 | 8 | ||
| 9 | 9 | import { Image } from "@astrojs/image/components"; |
|
| 10 | 10 | ||
| 11 | 11 | <Image |
|
| 12 | - | src="https://res.cloudinary.com/df9dofjus/image/upload/v1678295026/arc-browser-blog-post/s3udhcext52umxrydoum.png" |
|
| 13 | - | alt="Arc Logo" |
|
| 14 | - | width={1920} |
|
| 15 | - | aspectRatio={16/9} |
|
| 12 | + | src="https://res.cloudinary.com/df9dofjus/image/upload/v1678295026/arc-browser-blog-post/s3udhcext52umxrydoum.png" |
|
| 13 | + | alt="Arc Logo" |
|
| 14 | + | width={1920} |
|
| 15 | + | aspectRatio={16 / 9} |
|
| 16 | 16 | /> |
|
| 17 | 17 | ||
| 18 | 18 | ## 20 Years of the Same Thing |
|
| 20 | 20 | The internet has grown significantly in the last twenty years. What was once just static web pages with facts is now a bustling cyber metropolis where we write essays, share photos of food, and buy airline tickets. It's something we use every day and take for granted. What's interesting is that in the last twenty years of the internet's evolution, the way we experience it has stayed mostly the same. Web browsers have certainly changed in appearance and performance, but the tab model and disconnect from the rest of the computer have stayed. The Browser Company making the Arc Browser has plans to change that and has the ambitious goal of creating an "Internet Computer." Before we get ahead of ourselves, let's have a brief overview of Arc and its features. |
|
| 21 | 21 | ||
| 22 | 22 | ## Tabs, Folders, and Spaces |
|
| 23 | + | ||
| 23 | 24 | <Image |
|
| 24 | - | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678294127/arc-browser-blog-post/skdp4gdysro6bmmipgkb.png" |
|
| 25 | - | alt="Tabs folders and spaces in arc" |
|
| 26 | - | width={1920} |
|
| 27 | - | aspectRatio={16/9} |
|
| 25 | + | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678294127/arc-browser-blog-post/skdp4gdysro6bmmipgkb.png" |
|
| 26 | + | alt="Tabs folders and spaces in arc" |
|
| 27 | + | width={1920} |
|
| 28 | + | aspectRatio={16 / 9} |
|
| 28 | 29 | /> |
|
| 29 | 30 | ||
| 30 | 31 | One of the fundamental differences between Arc and other browsers is how it handles tabs. Instead of a row of tabs at the top of a window, Arc keeps them all in a sidebar. I genuinely believe there is a strong UX decision being made here: organization is simply easier to accomplish with a side menu than it is with a top menu. Beyond that, Arc uses a “Pinned Tabs” and “Today’s Tabs” approach to organizing your website. You may often feel hesitant to close a website because you might need it later. If you do, you can move it to the pinned tabs. When you feel like you don’t need it, then you can remove it from pinned tabs. Anything that stays in today’s tabs can be automatically cleared after a certain amount of time, or you can click a button to wipe them all out. This is powerful because it makes the decision of what is important and what isn’t easy for people to make. |
|
| 32 | + | ||
| 31 | 33 | <Image |
|
| 32 | - | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678294257/arc-browser-blog-post/pkslbdu6vwxmhcsfpiah.png" |
|
| 33 | - | alt="View of spaces in Arc" |
|
| 34 | - | width={1920} |
|
| 35 | - | aspectRatio={16/9} |
|
| 34 | + | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678294257/arc-browser-blog-post/pkslbdu6vwxmhcsfpiah.png" |
|
| 35 | + | alt="View of spaces in Arc" |
|
| 36 | + | width={1920} |
|
| 37 | + | aspectRatio={16 / 9} |
|
| 36 | 38 | /> |
|
| 37 | 39 | ||
| 38 | 40 | Of course, you may have lots of websites that you need to keep pinned, and for that, Arc has Folders and Spaces. Folders work like any other bookmark folder, where you can store as many as you want with sub-folders as well. This can be useful if you have a lot of sites that need to be referenced for a project. Folders work to a degree, but even still, you can have too many folders. That’s where spaces come in. Spaces are designed to be different workspaces for whatever you do on the internet. You could have a shopping space, an entertainment space, a developer space, anything really. You can customize the appearance, name, icon, and of course, the pinned tabs and folders for each one. Arc makes it simple to swipe between spaces and re-organize them to fit your needs. |
|
| 39 | 41 | ||
| 40 | 42 | <Image |
|
| 41 | - | src="https://res.cloudinary.com/df9dofjus/image/upload/v1678294333/arc-browser-blog-post/c3rjxfzg9fd2f5wjkdje.png" |
|
| 42 | - | alt="View of favorites in Arc" |
|
| 43 | - | width={912} |
|
| 44 | - | height={528} |
|
| 43 | + | src="https://res.cloudinary.com/df9dofjus/image/upload/v1678294333/arc-browser-blog-post/c3rjxfzg9fd2f5wjkdje.png" |
|
| 44 | + | alt="View of favorites in Arc" |
|
| 45 | + | width={912} |
|
| 46 | + | height={528} |
|
| 45 | 47 | /> |
|
| 46 | 48 | ||
| 47 | 49 | Favorites are special in that they are in nice little squares are the top of the sidebar no matter what space you are in, so for instance if you use Spotify a lot then that would be an ideal favorite app or website. They also have a preview feature when you hover over them for selective sites, e.g. Google Calendar will show you a brief schedule window. |
|
| 51 | 53 | ## Split View |
|
| 52 | 54 | ||
| 53 | 55 | <Image |
|
| 54 | - | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678294406/arc-browser-blog-post/riqyizem3k19rq7gjcot.png" |
|
| 55 | - | alt="Split view in Arc" |
|
| 56 | - | width={1920} |
|
| 57 | - | aspectRatio={16/9} |
|
| 56 | + | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678294406/arc-browser-blog-post/riqyizem3k19rq7gjcot.png" |
|
| 57 | + | alt="Split view in Arc" |
|
| 58 | + | width={1920} |
|
| 59 | + | aspectRatio={16 / 9} |
|
| 58 | 60 | /> |
|
| 59 | 61 | ||
| 60 | 62 | Another killer feature on Arc is split views. With a few clicks, you can easily have side-by-side panels of two different websites. You can even pin these dual tabs if you have to reference them quickly. You can actually do more than just two; depending on how big your screen is, you can go crazy! Personally, I use this all the time when reading an article while taking notes or moving information from one app to another. The Arc team recently released vertical splits as well, perhaps enough persuasion to get a vertical monitor. |
|
| 70 | 72 | ## Easels |
|
| 71 | 73 | ||
| 72 | 74 | <Image |
|
| 73 | - | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678295775/arc-browser-blog-post/jwihj6ivleo9hx90ikag.png" |
|
| 74 | - | alt="Easels in Arc" |
|
| 75 | - | width={1920} |
|
| 76 | - | aspectRatio={16/9} |
|
| 75 | + | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678295775/arc-browser-blog-post/jwihj6ivleo9hx90ikag.png" |
|
| 76 | + | alt="Easels in Arc" |
|
| 77 | + | width={1920} |
|
| 78 | + | aspectRatio={16 / 9} |
|
| 77 | 79 | /> |
|
| 78 | 80 | ||
| 79 | 81 | Easels are something completely unique to Arc and have some pretty impressive abilities. The concept is like internet chalkboards. You can snip pieces of websites, add them to an easel, then add other things like your images, text, shapes, etc. Arc provides a great shortcut (that can be customized), where all you have to do is hold down Command and Shift, then click & drag across the screen to select what you want to snip. Then it will prompt what easel you want to add the snippet to, or if you want to make a new one. Easels can also be made public and shared with other people! |
|
| 85 | 87 | ## Boosts |
|
| 86 | 88 | ||
| 87 | 89 | <Image |
|
| 88 | - | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678296116/arc-browser-blog-post/ow0z8neu9fnv3j21uor0.png" |
|
| 89 | - | alt="A view of the Boosts menu in Arc" |
|
| 90 | - | width={1920} |
|
| 91 | - | aspectRatio={16/9} |
|
| 90 | + | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678296116/arc-browser-blog-post/ow0z8neu9fnv3j21uor0.png" |
|
| 91 | + | alt="A view of the Boosts menu in Arc" |
|
| 92 | + | width={1920} |
|
| 93 | + | aspectRatio={16 / 9} |
|
| 92 | 94 | /> |
|
| 93 | 95 | ||
| 94 | 96 | Arc has an affinity to bring back the excitement of the internet from the 90s, both in design and in customization. Not only does Arc feature themes for your browser to personalize your experience, they give you the ability to do “Boosts” to websites. Boosts are simply CSS or JS injections to websites so you can customize the color of a website or give it additional functionality. |
|
| 95 | 97 | ||
| 96 | 98 | <Image |
|
| 97 | - | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678296633/arc-browser-blog-post/m0kdu35asmm6p43t7ml8.png" |
|
| 98 | - | alt="Boost being made in Arc for Tiwtter" |
|
| 99 | - | width={1920} |
|
| 100 | - | aspectRatio={16/9} |
|
| 99 | + | src="https://res.cloudinary.com/df9dofjus/image/upload/w_1920,h_1080,c_fill/v1678296633/arc-browser-blog-post/m0kdu35asmm6p43t7ml8.png" |
|
| 100 | + | alt="Boost being made in Arc for Tiwtter" |
|
| 101 | + | width={1920} |
|
| 102 | + | aspectRatio={16 / 9} |
|
| 101 | 103 | /> |
|
| 102 | 104 | ||
| 103 | 105 | As someone who is obsessed with customizing the personal computer environment, I absolutely love Boosts. [Nord](https://nordtheme.com/) is one of my favorite color palettes, and I use it for as many ports as I can. With Boosts, I’ve been able to customize all my regular sites to give it some flare. This comes a bit naturally for me since I have some web development experience and know how to snag CSS selectors easily, however, there are possibilities for Boosts to become a marketplace feature where non-technical users could save and use Boosts made by others. |
|
| 110 | 112 | ||
| 111 | 113 | ## Internet Computers |
|
| 112 | 114 | ||
| 113 | - | That last point brings us to the concept of Internet Computers that Josh, the CEO of The Browser Company, talks about in [videofile_ : the internet computer](https://youtu.be/v0160IirdL4). He essentially breaks down how most of our lives run on applications in the cloud rather than the computers we use, whether it's our work, our photos and videos, or our entertainment; it doesn’t live locally. An Internet Computer is a fluid concept without definition, but perhaps could look like |
|
| 115 | + | That last point brings us to the concept of Internet Computers that Josh, the CEO of The Browser Company, talks about in [videofile\_ : the internet computer](https://youtu.be/v0160IirdL4). He essentially breaks down how most of our lives run on applications in the cloud rather than the computers we use, whether it's our work, our photos and videos, or our entertainment; it doesn’t live locally. An Internet Computer is a fluid concept without definition, but perhaps could look like |
|
| 114 | 116 | ||
| 115 | 117 | > I can tap my finger on the device, or I can toss at a glance and whoosh: my computer comes down from the internet, comes down from the cloud, and is right there on that machine. Because all of the stuff I need, all of my tools, all of my files, all of my people, my teams, all of those things are out there on the internet too so it doesn't matter where I access it from. |
|
| 116 | - | ||
| 117 | 118 | ||
| 118 | 119 | Josh talks about this as a possible reality in five to ten years, but to be honest, it feels like we could be so much closer. Arc is already an app that can do just about anything you need it to do, thanks to the power of developers building web applications. Of course, there are limitations with heavier software, and there is still a lot of work to be done, but the concept of a computer that lives in the cloud and could seamlessly travel between your phone, your computer, or your partner’s tablet, really excites me! |
|
| 119 | 120 | ||
| 7 | 7 | --- |
|
| 8 | 8 | ||
| 9 | 9 | import { Image } from "@astrojs/image/components"; |
|
| 10 | - | import bueno from "../../assets/bueno.png" |
|
| 11 | - | import OutLinkButton from "../../components/OutLinkButton.astro" |
|
| 10 | + | import bueno from "../../assets/bueno.png"; |
|
| 11 | + | import OutLinkButton from "../../components/OutLinkButton.astro"; |
|
| 12 | 12 | ||
| 13 | 13 | <OutLinkButton link="https://bueno.art/blog/pinata-ipfs-guide" site="Bueno" image={bueno} /> |
|
| 14 | 14 | ||
| 20 | 20 | ||
| 21 | 21 | To avoid having to store all of that data on-chain, most NFTs are stored as metadata that points to an image off the blockchain where the actual file lives. It’s like when you share a Dropbox link. The link you share isn’t the actual thing, it just points you to where the actual thing is stored. That's why most early NFT projects were actually stored on Dropbox or a similar cloud service like AWS or Google Drive. While a full PNG of an NFT might be 4 mb, its metadata is only about 800 bytes. Definitely a step in the right direction, but this solution caused another problem: people started getting rugged. |
|
| 22 | 22 | ||
| 23 | - | Let’s say an influencer decides to launch a new NFT project called “Ceramic Cars." Sounds cool. She uploads an image of the first drop of cars to a storage provider and each car is named “crazycar1.png.” Luckily you snag one. She put a link to that image in your NFT metadata that looks something like this: |
|
| 23 | + | Let’s say an influencer decides to launch a new NFT project called “Ceramic Cars." Sounds cool. She uploads an image of the first drop of cars to a storage provider and each car is named “crazycar1.png.” Luckily you snag one. She put a link to that image in your NFT metadata that looks something like this: |
|
| 24 | 24 | ||
| 25 | 25 | ``` |
|
| 26 | - | https://storageservice.com/friend/crazycar1.png |
|
| 26 | + | https://storageservice.com/friend/crazycar1.png |
|
| 27 | 27 | ``` |
|
| 28 | 28 | ||
| 29 | 29 | Looks fine at first. But here’s the kicker: at any time, that influencer or the storage provider can simply remove that image. Suddenly the NFT is useless. And if she really wanted to be a jerk, she could replace it with a poop emoji with the same name. And since there is no regulation or laws around this, you would basically be screwed with no way of getting what you paid for. And that’s exactly what was happening in those early days: people were getting rugged left and right. These problems begged for a solution. That solution was [IPFS](https://ipfs.io) - the InterPlanetary File System. |
|
| 35 | 35 | The traditional way of sharing files over the internet is through centralized servers. For example, if you write up and send a Tweet, the data is sent to Twitter’s servers. Then when other users want to see that Tweet, the servers pass that information to them. It’s a direct up and down motion. |
|
| 36 | 36 | ||
| 37 | 37 | <Image |
|
| 38 | - | src="https://assets-global.website-files.com/6171adb6a942ed69f5e6b5ee/62fe4cfc48a5e05d952dd2c2_IPFS.png" |
|
| 39 | - | alt="bueno graphic" |
|
| 40 | - | width={1920} |
|
| 41 | - | aspectRatio={9/16} |
|
| 38 | + | src="https://assets-global.website-files.com/6171adb6a942ed69f5e6b5ee/62fe4cfc48a5e05d952dd2c2_IPFS.png" |
|
| 39 | + | alt="bueno graphic" |
|
| 40 | + | width={1920} |
|
| 41 | + | aspectRatio={9 / 16} |
|
| 42 | 42 | /> |
|
| 43 | 43 | ||
| 44 | 44 | Rather than straight up and down, IPFS is up, down, and side to side. IPFS is a decentralized network of nodes that share content with each other. If we imagine a Twitter run on IPFS, every user would have their own IPFS node that would upload data to the network, and as it is requested by other users, the data is passed from node to node. Rather than one company holding and owning that data, everyone owns that data. IPFS gives users the ability to own their data. |
|
| 45 | 45 | ||
| 46 | 46 | <Image |
|
| 47 | - | src="https://assets-global.website-files.com/6171adb6a942ed69f5e6b5ee/62fe4d182d482c2b9c29f199_IPFS-1.png" |
|
| 48 | - | alt="ipfs node graphic" |
|
| 49 | - | width={1920} |
|
| 50 | - | aspectRatio={16/9} |
|
| 47 | + | src="https://assets-global.website-files.com/6171adb6a942ed69f5e6b5ee/62fe4d182d482c2b9c29f199_IPFS-1.png" |
|
| 48 | + | alt="ipfs node graphic" |
|
| 49 | + | width={1920} |
|
| 50 | + | aspectRatio={16 / 9} |
|
| 51 | 51 | /> |
|
| 52 | 52 | ||
| 53 | - | ## How does IPFS protect your NFTs? |
|
| 53 | + | ## How does IPFS protect your NFTs? |
|
| 54 | 54 | ||
| 55 | 55 | When you share a file through IPFS, the file is run through a cryptographic algorithm that gives you something called the Content Identifier, or “CID” for short. This CID plays a huge part in how IPFS works and operates, and it looks something like this: |
|
| 56 | 56 | ||
| 58 | 58 | QmRAuxeMnsjPsbwW8LkKtk6Nh6MoqTvyKwP3zwuwJnB2yP |
|
| 59 | 59 | ``` |
|
| 60 | 60 | ||
| 61 | - | Every CID is determined by the content of the file, making it completely unique. If you change a picture by even one pixel, it would give you a different CID. This unique identifier makes content verifiable. In our example of an image being swapped out for a poop emoji, it wouldn’t be possible since the two images would have completely different CIDs. Combine this power with blockchain, and you get a reference to an image that is verified and cannot be changed. |
|
| 61 | + | Every CID is determined by the content of the file, making it completely unique. If you change a picture by even one pixel, it would give you a different CID. This unique identifier makes content verifiable. In our example of an image being swapped out for a poop emoji, it wouldn’t be possible since the two images would have completely different CIDs. Combine this power with blockchain, and you get a reference to an image that is verified and cannot be changed. |
|
| 62 | 62 | ||
| 63 | 63 | As an NFT creator, this is a huge benefit that you can use to reassure your audience. Any NFT that they collect from your collection will be 100% verifiable. Beyond that, the CID makes content portable and addressable. |
|
| 64 | 64 | ||
| 6 | 6 | ogImage: "https://miro.medium.com/v2/resize:fit:4800/format:webp/1*4xA96GrA9iLYMp5vorcjyQ.jpeg" |
|
| 7 | 7 | --- |
|
| 8 | 8 | ||
| 9 | - | import medium from "../../assets/medium.png" |
|
| 10 | - | import OutLinkButton from "../../components/OutLinkButton.astro" |
|
| 9 | + | import medium from "../../assets/medium.png"; |
|
| 10 | + | import OutLinkButton from "../../components/OutLinkButton.astro"; |
|
| 11 | 11 | ||
| 12 | - | <OutLinkButton link="https://medium.com/pinata/a-case-for-ipfs-on-layer-1-blockchains-like-solana-aptos-and-sui-165a9732c214" site="Medium" image={medium} /> |
|
| 13 | - | ||
| 12 | + | <OutLinkButton |
|
| 13 | + | link="https://medium.com/pinata/a-case-for-ipfs-on-layer-1-blockchains-like-solana-aptos-and-sui-165a9732c214" |
|
| 14 | + | site="Medium" |
|
| 15 | + | image={medium} |
|
| 16 | + | /> |
|
| 14 | 17 | ||
| 15 | 18 | There has been a Cambrian explosion of new layer 1 blockchains — all with brand new technologies that make them faster and more efficient. However there is one problem that no blockchain has managed to overcome: asset storage. |
|
| 16 | 19 |
| 5 | 5 | tags: ["web3", "ipfs", "nfts", "tutorials", "web development"] |
|
| 6 | 6 | ogImage: "https://miro.medium.com/v2/resize:fit:4800/format:webp/1*JbX3kWI20G3EaKJNgXfQiQ.png" |
|
| 7 | 7 | --- |
|
| 8 | - | import { Image } from "@astrojs/image/components"; |
|
| 9 | - | import medium from "../../assets/medium.png" |
|
| 10 | - | import OutLinkButton from "../../components/OutLinkButton.astro" |
|
| 11 | 8 | ||
| 12 | - | <OutLinkButton link="https://medium.com/pinata/how-to-mint-an-nft-on-sui-using-pinata-and-the-sui-js-sdk-4386655e403" site="Medium" image={medium} /> |
|
| 9 | + | import { Image } from "@astrojs/image/components"; |
|
| 10 | + | import medium from "../../assets/medium.png"; |
|
| 11 | + | import OutLinkButton from "../../components/OutLinkButton.astro"; |
|
| 13 | 12 | ||
| 13 | + | <OutLinkButton |
|
| 14 | + | link="https://medium.com/pinata/how-to-mint-an-nft-on-sui-using-pinata-and-the-sui-js-sdk-4386655e403" |
|
| 15 | + | site="Medium" |
|
| 16 | + | image={medium} |
|
| 17 | + | /> |
|
| 14 | 18 | ||
| 15 | 19 | One of my favorite pastimes is playing around with new blockchains and seeing what they’re like, what they offer, and how easy they are to build on. Recently I stumbled upon Sui and some of its unique features, such as dynamic metadata and goals to help onboard everyday people. After playing with it myself I was impressed with its speed and smoothness, which is saying something as it’s still in development! |
|
| 16 | 20 | ||
| 25 | 29 | Getting started with Pinata is easy! Just visit the signup page here and start out with a free account. Now all you have to do is upload the image you want to use. Do that by visiting the main files page and clicking “Upload” and “Select File.” |
|
| 26 | 30 | ||
| 27 | 31 | <Image |
|
| 28 | - | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*TF200pv3qCMx41dHZoE_kg@2x.png" |
|
| 29 | - | alt="pinata files page" |
|
| 30 | - | width={1920} |
|
| 31 | - | aspectRatio={16/9} |
|
| 32 | + | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*TF200pv3qCMx41dHZoE_kg@2x.png" |
|
| 33 | + | alt="pinata files page" |
|
| 34 | + | width={1920} |
|
| 35 | + | aspectRatio={16 / 9} |
|
| 32 | 36 | /> |
|
| 33 | 37 | ||
| 34 | 38 | After that just follow the steps of selecting your file, give it a name, then upload! Once done it should show up in your files page as seen below, and we will want to copy the CID for later. |
|
| 35 | 39 | ||
| 36 | 40 | <Image |
|
| 37 | - | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*68MMViE3btD8hHuxaTbEzg@2x.png" |
|
| 38 | - | alt="pinata files page" |
|
| 39 | - | width={1920} |
|
| 40 | - | aspectRatio={16/9} |
|
| 41 | + | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*68MMViE3btD8hHuxaTbEzg@2x.png" |
|
| 42 | + | alt="pinata files page" |
|
| 43 | + | width={1920} |
|
| 44 | + | aspectRatio={16 / 9} |
|
| 41 | 45 | /> |
|
| 42 | 46 | ||
| 43 | 47 | ## Code Setup with the Sui JS SDK |
|
| 58 | 62 | ||
| 59 | 63 | ```json |
|
| 60 | 64 | { |
|
| 61 | - | "name": "sui-nft", |
|
| 62 | - | "type": "module", |
|
| 63 | - | "version": "1.0.0", |
|
| 64 | - | "description": "", |
|
| 65 | - | "main": "index.js", |
|
| 66 | - | "scripts": { |
|
| 67 | - | "test": "echo \"Error: no test specified\" && exit 1" |
|
| 68 | - | }, |
|
| 69 | - | "keywords": [], |
|
| 70 | - | "author": "", |
|
| 71 | - | "license": "ISC", |
|
| 72 | - | "dependencies": { |
|
| 73 | - | "@mysten/sui.js": "^0.26.1" |
|
| 74 | - | } |
|
| 65 | + | "name": "sui-nft", |
|
| 66 | + | "type": "module", |
|
| 67 | + | "version": "1.0.0", |
|
| 68 | + | "description": "", |
|
| 69 | + | "main": "index.js", |
|
| 70 | + | "scripts": { |
|
| 71 | + | "test": "echo \"Error: no test specified\" && exit 1" |
|
| 72 | + | }, |
|
| 73 | + | "keywords": [], |
|
| 74 | + | "author": "", |
|
| 75 | + | "license": "ISC", |
|
| 76 | + | "dependencies": { |
|
| 77 | + | "@mysten/sui.js": "^0.26.1" |
|
| 78 | + | } |
|
| 75 | 79 | } |
|
| 76 | 80 | ``` |
|
| 77 | 81 | ||
| 88 | 92 | Go ahead and open up your mint-nft.js file in your text editor of choice, and the first thing we’re gonna do is import the following methods at the top of the page. |
|
| 89 | 93 | ||
| 90 | 94 | ```javascript |
|
| 91 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from '@mysten/sui.js'; |
|
| 95 | + | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 92 | 96 | ``` |
|
| 93 | 97 | ||
| 94 | 98 | All we really need are four things, and you’ll see how they play a part as we start building. Next thing we need to do is create a wallet and get the public address for that wallet, which we can do like so. |
|
| 95 | 99 | ||
| 96 | 100 | ```javascript |
|
| 97 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from '@mysten/sui.js'; |
|
| 101 | + | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 98 | 102 | ||
| 99 | - | const keypair = new Ed25519Keypair() |
|
| 100 | - | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString() |
|
| 101 | - | console.log(address) |
|
| 103 | + | const keypair = new Ed25519Keypair(); |
|
| 104 | + | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString(); |
|
| 105 | + | console.log(address); |
|
| 102 | 106 | ``` |
|
| 103 | 107 | ||
| 104 | 108 | Accessing the keypair object makes it super simple to either get the public key or private key, and then turn it into usable data. The Sui address from the public key doesn’t include the leading “0x” so I’m adding that manually here. If you go into the terminal now and run “node mint-nft.js” then you should see an address like this! |
|
| 110 | 114 | Now that we have a wallet address, it’s time to connect to the Sui network and get some test Sui coin! First we’ll declare a new provider and use the JsonRpcProvider, and pass in the Network “DEVNET.” |
|
| 111 | 115 | ||
| 112 | 116 | ```javascript |
|
| 113 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from '@mysten/sui.js'; |
|
| 117 | + | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 114 | 118 | ||
| 115 | 119 | //Create keypair |
|
| 116 | - | const keypair = new Ed25519Keypair() |
|
| 117 | - | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString() |
|
| 118 | - | console.log(address) |
|
| 120 | + | const keypair = new Ed25519Keypair(); |
|
| 121 | + | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString(); |
|
| 122 | + | console.log(address); |
|
| 119 | 123 | ||
| 120 | - | //Create network connection |
|
| 124 | + | //Create network connection |
|
| 121 | 125 | const provider = new JsonRpcProvider(Network.DEVNET); |
|
| 122 | 126 | ``` |
|
| 123 | 127 | ||
| 124 | 128 | Then we’ll use the provider to request some test Sui from the Devnet faucet and use our new address as the receiver like so. |
|
| 125 | 129 | ||
| 126 | 130 | ```javascript |
|
| 127 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from '@mysten/sui.js'; |
|
| 131 | + | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 128 | 132 | ||
| 129 | 133 | //Create keypair |
|
| 130 | - | const keypair = new Ed25519Keypair() |
|
| 131 | - | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString() |
|
| 132 | - | console.log(address) |
|
| 134 | + | const keypair = new Ed25519Keypair(); |
|
| 135 | + | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString(); |
|
| 136 | + | console.log(address); |
|
| 133 | 137 | ||
| 134 | - | //Create network connection |
|
| 138 | + | //Create network connection |
|
| 135 | 139 | const provider = new JsonRpcProvider(Network.DEVNET); |
|
| 136 | 140 | ||
| 137 | 141 | // Get Sui from faucet |
|
| 138 | - | const fund = await provider.requestSuiFromFaucet(address) |
|
| 139 | - | console.log(fund) |
|
| 142 | + | const fund = await provider.requestSuiFromFaucet(address); |
|
| 143 | + | console.log(fund); |
|
| 140 | 144 | ``` |
|
| 141 | 145 | ||
| 142 | 146 | Let’s run the node mint-nft.js command now and see what we get! |
|
| 182 | 186 | With that said, in order for us to mint, we need to combine some of our recently acquired Sui drop. We’re gonna do that with the following code. |
|
| 183 | 187 | ||
| 184 | 188 | ```javascript |
|
| 185 | - | // Merge two of the Sui coin objects |
|
| 186 | - | const coin1 = fund.transferred_gas_objects[0].id |
|
| 187 | - | const coin2 = fund.transferred_gas_objects[1].id |
|
| 189 | + | // Merge two of the Sui coin objects |
|
| 190 | + | const coin1 = fund.transferred_gas_objects[0].id; |
|
| 191 | + | const coin2 = fund.transferred_gas_objects[1].id; |
|
| 188 | 192 | const signer = new RawSigner(keypair, provider); |
|
| 189 | 193 | const mergeTxn = await signer.mergeCoin({ |
|
| 190 | - | primaryCoin: coin1, |
|
| 191 | - | coinToMerge: coin2, |
|
| 192 | - | gasBudget: 1000, |
|
| 194 | + | primaryCoin: coin1, |
|
| 195 | + | coinToMerge: coin2, |
|
| 196 | + | gasBudget: 1000, |
|
| 193 | 197 | }); |
|
| 194 | - | console.log('MergeCoin txn', mergeTxn); |
|
| 198 | + | console.log("MergeCoin txn", mergeTxn); |
|
| 195 | 199 | ``` |
|
| 200 | + | ||
| 196 | 201 | There’s quite a bit going on here so let’s break it down. |
|
| 197 | 202 | ||
| 198 | 203 | First we declare coin1 and coin2 from the airdrop we just received by accessing those object ids from our fund result. Then we need to declare our signer! This is what lets us use our private key from our keypair to transfer or mint on the chain, and we do that by declaring a new RawSigner and passing in our previously made keypair and provider to connect. Finally we use the mergeCoin method and pass in our two coins, along with a gas budget. |
|
| 202 | 207 | ```javascript |
|
| 203 | 208 | // Pause function |
|
| 204 | 209 | const wait = async (time) => { |
|
| 205 | - | return new Promise((resolve, reject) => { |
|
| 206 | - | setTimeout(() => { |
|
| 207 | - | resolve(); |
|
| 208 | - | }, time) |
|
| 209 | - | }); |
|
| 210 | - | } |
|
| 210 | + | return new Promise((resolve, reject) => { |
|
| 211 | + | setTimeout(() => { |
|
| 212 | + | resolve(); |
|
| 213 | + | }, time); |
|
| 214 | + | }); |
|
| 215 | + | }; |
|
| 211 | 216 | ``` |
|
| 212 | 217 | ||
| 213 | 218 | This is really simple and just lets us pass in how many milliseconds we want to wait before continuing our function! Let’s use it after getting our airdrop and passing in 3 seconds. You code should look something like this now. |
|
| 214 | 219 | ||
| 215 | 220 | ```javascript |
|
| 216 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from '@mysten/sui.js'; |
|
| 221 | + | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 217 | 222 | ||
| 218 | 223 | // Generate a new Keypair |
|
| 219 | 224 | const keypair = new Ed25519Keypair(); |
|
| 220 | - | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString() |
|
| 221 | - | console.log(address) |
|
| 225 | + | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString(); |
|
| 226 | + | console.log(address); |
|
| 222 | 227 | ||
| 223 | - | // Create Network Connection and receive airdrop |
|
| 228 | + | // Create Network Connection and receive airdrop |
|
| 224 | 229 | const provider = new JsonRpcProvider(Network.DEVNET); |
|
| 225 | 230 | ||
| 226 | 231 | // Get Sui from faucet |
|
| 227 | - | const fund = await provider.requestSuiFromFaucet(address) |
|
| 228 | - | console.log(fund) |
|
| 232 | + | const fund = await provider.requestSuiFromFaucet(address); |
|
| 233 | + | console.log(fund); |
|
| 229 | 234 | ||
| 230 | 235 | // Pause function |
|
| 231 | 236 | const wait = async (time) => { |
|
| 232 | - | return new Promise((resolve, reject) => { |
|
| 233 | - | setTimeout(() => { |
|
| 234 | - | resolve(); |
|
| 235 | - | }, time) |
|
| 236 | - | }); |
|
| 237 | - | } |
|
| 237 | + | return new Promise((resolve, reject) => { |
|
| 238 | + | setTimeout(() => { |
|
| 239 | + | resolve(); |
|
| 240 | + | }, time); |
|
| 241 | + | }); |
|
| 242 | + | }; |
|
| 238 | 243 | ||
| 239 | - | await wait(3000) |
|
| 244 | + | await wait(3000); |
|
| 240 | 245 | ||
| 241 | - | // Merge two of the Sui coin objects |
|
| 242 | - | const coin1 = fund.transferred_gas_objects[1].id |
|
| 243 | - | const coin2 = fund.transferred_gas_objects[2].id |
|
| 246 | + | // Merge two of the Sui coin objects |
|
| 247 | + | const coin1 = fund.transferred_gas_objects[1].id; |
|
| 248 | + | const coin2 = fund.transferred_gas_objects[2].id; |
|
| 244 | 249 | const signer = new RawSigner(keypair, provider); |
|
| 245 | 250 | const mergeTxn = await signer.mergeCoin({ |
|
| 246 | - | primaryCoin: coin1, |
|
| 247 | - | coinToMerge: coin2, |
|
| 248 | - | gasBudget: 1000, |
|
| 251 | + | primaryCoin: coin1, |
|
| 252 | + | coinToMerge: coin2, |
|
| 253 | + | gasBudget: 1000, |
|
| 249 | 254 | }); |
|
| 250 | - | console.log('MergeCoin txn', mergeTxn); |
|
| 255 | + | console.log("MergeCoin txn", mergeTxn); |
|
| 251 | 256 | ``` |
|
| 252 | 257 | ||
| 253 | 258 | If you run this code you should see the result of the airdrop, a pause before merging the coins, and the successful coin merge! Now that we have all our cows in one heard, we can mint an NFT successfully. Let’s take a look! |
|
| 255 | 260 | ```javascript |
|
| 256 | 261 | // Call to Mint NFT |
|
| 257 | 262 | const mintTxn = await signer.executeMoveCall({ |
|
| 258 | - | packageObjectId: '0x2', |
|
| 259 | - | module: 'devnet_nft', |
|
| 260 | - | function: 'mint', |
|
| 261 | - | typeArguments: [], |
|
| 262 | - | arguments: [ |
|
| 263 | - | 'gm', |
|
| 264 | - | 'A nice gm brought to you by Pinata and Sui', |
|
| 265 | - | 'ipfs://QmZhnkimthxvL32vin2mrQvnhN8ZbWFMvKMxRqHEq7dPz3', |
|
| 266 | - | ], |
|
| 267 | - | gasBudget: 10000 |
|
| 263 | + | packageObjectId: "0x2", |
|
| 264 | + | module: "devnet_nft", |
|
| 265 | + | function: "mint", |
|
| 266 | + | typeArguments: [], |
|
| 267 | + | arguments: [ |
|
| 268 | + | "gm", |
|
| 269 | + | "A nice gm brought to you by Pinata and Sui", |
|
| 270 | + | "ipfs://QmZhnkimthxvL32vin2mrQvnhN8ZbWFMvKMxRqHEq7dPz3", |
|
| 271 | + | ], |
|
| 272 | + | gasBudget: 10000, |
|
| 268 | 273 | }); |
|
| 269 | - | console.log('mint transaction:', mintTxn); |
|
| 274 | + | console.log("mint transaction:", mintTxn); |
|
| 270 | 275 | ``` |
|
| 271 | 276 | ||
| 272 | 277 | Our minting function is simply accessing a pre-built smart contract called ‘devnet_nft’ and we’re using the ‘mint’ function. All we have to pass into the arguments is the name of the NFT, the description, and then the asset link! |
|
| 275 | 280 | ||
| 276 | 281 | ```javascript |
|
| 277 | 282 | // View NFT |
|
| 278 | - | const nftId = mintTxn.effects.effects.created[0].reference.objectId.toString() |
|
| 279 | - | console.log(`View NFT: https://explorer.sui.io/object/${nftId}?network=devnet`) |
|
| 283 | + | const nftId = mintTxn.effects.effects.created[0].reference.objectId.toString(); |
|
| 284 | + | console.log(`View NFT: https://explorer.sui.io/object/${nftId}?network=devnet`); |
|
| 280 | 285 | ``` |
|
| 281 | 286 | ||
| 282 | 287 | Now let’s look at our full code to make sure everything is good, then run it! |
|
| 283 | 288 | ||
| 284 | 289 | ```javascript |
|
| 285 | - | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from '@mysten/sui.js'; |
|
| 290 | + | import { Ed25519Keypair, JsonRpcProvider, Network, RawSigner } from "@mysten/sui.js"; |
|
| 286 | 291 | ||
| 287 | 292 | // Generate a new Keypair |
|
| 288 | 293 | const keypair = new Ed25519Keypair(); |
|
| 289 | - | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString() |
|
| 290 | - | console.log(address) |
|
| 294 | + | const address = "0x" + keypair.getPublicKey().toSuiAddress().toString(); |
|
| 295 | + | console.log(address); |
|
| 291 | 296 | ||
| 292 | - | // Create Network Connection and receive airdrop |
|
| 297 | + | // Create Network Connection and receive airdrop |
|
| 293 | 298 | const provider = new JsonRpcProvider(Network.DEVNET); |
|
| 294 | 299 | ||
| 295 | 300 | // Get Sui from faucet |
|
| 296 | - | const fund = await provider.requestSuiFromFaucet(address) |
|
| 297 | - | console.log(fund) |
|
| 301 | + | const fund = await provider.requestSuiFromFaucet(address); |
|
| 302 | + | console.log(fund); |
|
| 298 | 303 | ||
| 299 | 304 | // Pause function |
|
| 300 | 305 | const wait = async (time) => { |
|
| 301 | - | return new Promise((resolve, reject) => { |
|
| 302 | - | setTimeout(() => { |
|
| 303 | - | resolve(); |
|
| 304 | - | }, time) |
|
| 305 | - | }); |
|
| 306 | - | } |
|
| 306 | + | return new Promise((resolve, reject) => { |
|
| 307 | + | setTimeout(() => { |
|
| 308 | + | resolve(); |
|
| 309 | + | }, time); |
|
| 310 | + | }); |
|
| 311 | + | }; |
|
| 307 | 312 | ||
| 308 | - | await wait(3000) |
|
| 313 | + | await wait(3000); |
|
| 309 | 314 | ||
| 310 | - | // Merge two of the Sui coin objects |
|
| 311 | - | const coin1 = fund.transferred_gas_objects[1].id |
|
| 312 | - | const coin2 = fund.transferred_gas_objects[2].id |
|
| 315 | + | // Merge two of the Sui coin objects |
|
| 316 | + | const coin1 = fund.transferred_gas_objects[1].id; |
|
| 317 | + | const coin2 = fund.transferred_gas_objects[2].id; |
|
| 313 | 318 | const signer = new RawSigner(keypair, provider); |
|
| 314 | 319 | const mergeTxn = await signer.mergeCoin({ |
|
| 315 | - | primaryCoin: coin1, |
|
| 316 | - | coinToMerge: coin2, |
|
| 317 | - | gasBudget: 1000, |
|
| 320 | + | primaryCoin: coin1, |
|
| 321 | + | coinToMerge: coin2, |
|
| 322 | + | gasBudget: 1000, |
|
| 318 | 323 | }); |
|
| 319 | - | console.log('MergeCoin txn', mergeTxn); |
|
| 324 | + | console.log("MergeCoin txn", mergeTxn); |
|
| 320 | 325 | ||
| 321 | 326 | // Call to Mint NFT |
|
| 322 | 327 | const mintTxn = await signer.executeMoveCall({ |
|
| 323 | - | packageObjectId: '0x2', |
|
| 324 | - | module: 'devnet_nft', |
|
| 325 | - | function: 'mint', |
|
| 326 | - | typeArguments: [], |
|
| 327 | - | arguments: [ |
|
| 328 | - | 'gm', |
|
| 329 | - | 'A nice gm brought to you by Pinata and Sui', |
|
| 330 | - | 'ipfs://QmZhnkimthxvL32vin2mrQvnhN8ZbWFMvKMxRqHEq7dPz3', |
|
| 331 | - | ], |
|
| 332 | - | gasBudget: 10000 |
|
| 328 | + | packageObjectId: "0x2", |
|
| 329 | + | module: "devnet_nft", |
|
| 330 | + | function: "mint", |
|
| 331 | + | typeArguments: [], |
|
| 332 | + | arguments: [ |
|
| 333 | + | "gm", |
|
| 334 | + | "A nice gm brought to you by Pinata and Sui", |
|
| 335 | + | "ipfs://QmZhnkimthxvL32vin2mrQvnhN8ZbWFMvKMxRqHEq7dPz3", |
|
| 336 | + | ], |
|
| 337 | + | gasBudget: 10000, |
|
| 333 | 338 | }); |
|
| 334 | - | console.log('mint transaction:', mintTxn); |
|
| 339 | + | console.log("mint transaction:", mintTxn); |
|
| 335 | 340 | ||
| 336 | 341 | // View NFT |
|
| 337 | - | const nftId = mintTxn.effects.effects.created[0].reference.objectId.toString() |
|
| 338 | - | console.log(`View NFT: https://explorer.sui.io/object/${nftId}?network=devnet`) |
|
| 342 | + | const nftId = mintTxn.effects.effects.created[0].reference.objectId.toString(); |
|
| 343 | + | console.log(`View NFT: https://explorer.sui.io/object/${nftId}?network=devnet`); |
|
| 339 | 344 | ``` |
|
| 340 | 345 | ||
| 341 | 346 | If all works as it should you’ll get a link and then you should see your final NFT! |
|
| 342 | 347 | ||
| 343 | 348 | <Image |
|
| 344 | - | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*oCtNaZK3LOx807tA8IEldg@2x.png" |
|
| 345 | - | alt="pinata files page" |
|
| 346 | - | width={1920} |
|
| 347 | - | aspectRatio={16/9} |
|
| 349 | + | src="https://miro.medium.com/v2/resize:fit:4800/format:webp/1*oCtNaZK3LOx807tA8IEldg@2x.png" |
|
| 350 | + | alt="pinata files page" |
|
| 351 | + | width={1920} |
|
| 352 | + | aspectRatio={16 / 9} |
|
| 348 | 353 | /> |
|
| 349 | 354 | ||
| 350 | 355 | ## You did it!! 🎉 |
|
| 5 | 5 | tags: ["web3", "nfts", "tutorials", "web development", "tech philosophy"] |
|
| 6 | 6 | ogImage: "https://miro.medium.com/v2/resize:fit:4800/format:webp/1*KxoVDEZFH3mJrlfeguMYjg.jpeg" |
|
| 7 | 7 | --- |
|
| 8 | + | ||
| 8 | 9 | import { Image } from "@astrojs/image/components"; |
|
| 9 | - | import medium from "../../assets/medium.png" |
|
| 10 | - | import OutLinkButton from "../../components/OutLinkButton.astro" |
|
| 10 | + | import medium from "../../assets/medium.png"; |
|
| 11 | + | import OutLinkButton from "../../components/OutLinkButton.astro"; |
|
| 11 | 12 | ||
| 12 | - | <OutLinkButton link="https://medium.com/pinata/how-to-offset-your-nft-project-carbon-emissions-with-aerial-b5b4b95faba0" site="Medium" image={medium} /> |
|
| 13 | - | ||
| 13 | + | <OutLinkButton |
|
| 14 | + | link="https://medium.com/pinata/how-to-offset-your-nft-project-carbon-emissions-with-aerial-b5b4b95faba0" |
|
| 15 | + | site="Medium" |
|
| 16 | + | image={medium} |
|
| 17 | + | /> |
|
| 14 | 18 | ||
| 15 | 19 | As a believer in NFTs and Web3, I am always ecstatic to see what can be done with this new technology. I get bullish over new projects, experimental ideas, and cutting edge utility (especially when it’s something we make at Pinata like [Submarine.me](https://submarine.me)). However, I don’t look at this Metaverse with rose colored glasses. There are many imperfections in this space, and one of the worst ones is environmental impact. |
|
| 16 | 20 | ||
| 17 | 21 | While I love dooting around on my computer, I also love the outdoors. I love hiking, seeing mountains, hearing rivers, and smelling wildflowers. I love hearing birds in the morning and geese at night. I love to watch my son experience nature and get excited over a butterfly. So yeah, it kinda kills me to think of how this wonderful industry of crypto can harm this amazing planet. |
|
| 18 | 22 | ||
| 19 | 23 | <Image |
|
| 20 | - | src="https://cdn-images-1.medium.com/max/2400/1*xQEP67xsPsQ4vU7wjKLFBw.png" |
|
| 21 | - | alt="ethereum carbon emissions chart" |
|
| 22 | - | width={1920} |
|
| 23 | - | aspectRatio={16/9} |
|
| 24 | + | src="https://cdn-images-1.medium.com/max/2400/1*xQEP67xsPsQ4vU7wjKLFBw.png" |
|
| 25 | + | alt="ethereum carbon emissions chart" |
|
| 26 | + | width={1920} |
|
| 27 | + | aspectRatio={16 / 9} |
|
| 24 | 28 | /> |
|
| 25 | 29 | ||
| 26 | 30 | The energy consumed by proof of work blockchains is staggering. It’s no secret, and if we want to enjoy this planet we need to accept it. I know I feel defensive when someone attacks something I enjoy, but it helps to take a deep breath and ask the big questions: “could I be wrong?” If crypto is putting the planet in danger, what do we do? |
|
| 36 | 40 | ``` |
|
| 37 | 41 | ||
| 38 | 42 | <Image |
|
| 39 | - | src="https://cdn-images-1.medium.com/max/2000/1*IPAefM4T_NiRygdri_DzhA.png" |
|
| 40 | - | alt="screenshot of widget" |
|
| 41 | - | width={1920} |
|
| 42 | - | aspectRatio={6/2} |
|
| 43 | + | src="https://cdn-images-1.medium.com/max/2000/1*IPAefM4T_NiRygdri_DzhA.png" |
|
| 44 | + | alt="screenshot of widget" |
|
| 45 | + | width={1920} |
|
| 46 | + | aspectRatio={6 / 2} |
|
| 43 | 47 | /> |
|
| 44 | - | ||
| 45 | 48 | ||
| 46 | 49 | In this post we’ll take their API a step further and build our own custom widget. We can use it in our frontend website that displays more details about the project’s emissions with the goal of making visitors more aware! |
|
| 47 | 50 | ||
| 57 | 60 | ||
| 58 | 61 | ```javascript |
|
| 59 | 62 | function App() { |
|
| 60 | - | return ( |
|
| 61 | - | <div className="App"> |
|
| 62 | - | </div> |
|
| 63 | - | ); |
|
| 63 | + | return <div className="App"></div>; |
|
| 64 | 64 | } |
|
| 65 | 65 | ||
| 66 | 66 | export default App; |
|
| 70 | 70 | ||
| 71 | 71 | ```javascript |
|
| 72 | 72 | const Aerial = () => { |
|
| 73 | - | return ( |
|
| 74 | - | <h1>Aerial</h1> |
|
| 75 | - | ) |
|
| 76 | - | } |
|
| 73 | + | return <h1>Aerial</h1>; |
|
| 74 | + | }; |
|
| 77 | 75 | ||
| 78 | 76 | export default Aerial; |
|
| 79 | - | ||
| 80 | 77 | ``` |
|
| 81 | 78 | ||
| 82 | 79 | Now let’s import the new component to our App.js |
|
| 83 | - | ||
| 84 | 80 | ||
| 85 | 81 | ```javascript |
|
| 86 | - | import Aerial from "./Aerial" |
|
| 82 | + | import Aerial from "./Aerial"; |
|
| 87 | 83 | ||
| 88 | 84 | function App() { |
|
| 89 | - | return ( |
|
| 90 | - | <div className="App"> |
|
| 91 | - | <Aerial /> |
|
| 92 | - | </div> |
|
| 93 | - | ); |
|
| 85 | + | return ( |
|
| 86 | + | <div className="App"> |
|
| 87 | + | <Aerial /> |
|
| 88 | + | </div> |
|
| 89 | + | ); |
|
| 94 | 90 | } |
|
| 95 | 91 | ||
| 96 | 92 | export default App; |
|
| 112 | 108 | { |
|
| 113 | 109 | "co2": <emissions in CO2>, |
|
| 114 | 110 | "gas": <gas used>, |
|
| 115 | - | "transactions": <number of transactions>, |
|
| 111 | + | "transactions": <number of transactions>, |
|
| 116 | 112 | "credits": <credits required to offset>, |
|
| 117 | 113 | "cost": <cost to offset in USD>, |
|
| 118 | 114 | "credits_purchased": <number of credits already purchased>, |
|
| 129 | 125 | Now make sure to import it into the top of our Aerial component like so |
|
| 130 | 126 | ||
| 131 | 127 | ```javascript |
|
| 132 | - | import axios from "axios" |
|
| 128 | + | import axios from "axios"; |
|
| 133 | 129 | ``` |
|
| 134 | 130 | ||
| 135 | 131 | Now we’re going to make a quick function that will get the data. For our example we’re going to use the NFT contract address 0x2acab3dea77832c09420663b0e1cb386031ba17b. |
|
| 136 | 132 | ||
| 137 | 133 | ```javascript |
|
| 138 | 134 | const getEmissionsData = async () => { |
|
| 139 | - | try{ |
|
| 140 | - | const response = await axios.get("https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b") |
|
| 141 | - | console.log(response.data) |
|
| 142 | - | } catch (error) { |
|
| 143 | - | console.log(error) |
|
| 144 | - | } |
|
| 145 | - | } |
|
| 135 | + | try { |
|
| 136 | + | const response = await axios.get( |
|
| 137 | + | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 138 | + | ); |
|
| 139 | + | console.log(response.data); |
|
| 140 | + | } catch (error) { |
|
| 141 | + | console.log(error); |
|
| 142 | + | } |
|
| 143 | + | }; |
|
| 146 | 144 | ``` |
|
| 147 | 145 | ||
| 148 | 146 | To run this function we’ll use the useEffect hook to fetch the data as we load the app. To do that we simply need to import it at the top like so |
|
| 154 | 152 | Then we need to run the function inside useEffect, |
|
| 155 | 153 | ||
| 156 | 154 | ```javascript |
|
| 157 | - | useEffect(()=> { |
|
| 158 | - | getEmissionsData() |
|
| 159 | - | }, []) |
|
| 155 | + | useEffect(() => { |
|
| 156 | + | getEmissionsData(); |
|
| 157 | + | }, []); |
|
| 160 | 158 | ``` |
|
| 161 | 159 | ||
| 162 | 160 | This is what our code will look like with everything in place: |
|
| 163 | 161 | ||
| 164 | 162 | ```javascript |
|
| 165 | - | ||
| 166 | 163 | import { useEffect } from "react"; |
|
| 167 | 164 | import axios from "axios"; |
|
| 168 | 165 | ||
| 169 | 166 | const Aerial = () => { |
|
| 167 | + | const getEmissionsData = async () => { |
|
| 168 | + | try { |
|
| 169 | + | const response = await axios.get( |
|
| 170 | + | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 171 | + | ); |
|
| 172 | + | console.log(response.data); |
|
| 173 | + | } catch (error) { |
|
| 174 | + | console.log(error); |
|
| 175 | + | } |
|
| 176 | + | }; |
|
| 170 | 177 | ||
| 171 | - | const getEmissionsData = async () => { |
|
| 172 | - | try{ |
|
| 173 | - | const response = await axios.get("https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b") |
|
| 174 | - | console.log(response.data) |
|
| 175 | - | } catch (error) { |
|
| 176 | - | console.log(error) |
|
| 177 | - | } |
|
| 178 | - | } |
|
| 178 | + | useEffect(() => { |
|
| 179 | + | getEmissionsData(); |
|
| 180 | + | }, []); |
|
| 179 | 181 | ||
| 180 | - | useEffect(()=> { |
|
| 181 | - | getEmissionsData() |
|
| 182 | - | }, []) |
|
| 183 | - | ||
| 184 | - | return ( |
|
| 185 | - | <div className="aerial-container"> |
|
| 186 | - | <h1>Aerial</h1> |
|
| 187 | - | </div> |
|
| 188 | - | ) |
|
| 189 | - | } |
|
| 182 | + | return ( |
|
| 183 | + | <div className="aerial-container"> |
|
| 184 | + | <h1>Aerial</h1> |
|
| 185 | + | </div> |
|
| 186 | + | ); |
|
| 187 | + | }; |
|
| 190 | 188 | ||
| 191 | 189 | export default Aerial; |
|
| 192 | 190 | ``` |
|
| 194 | 192 | If we run the app and check the dev console, we can see our data! |
|
| 195 | 193 | ||
| 196 | 194 | <Image |
|
| 197 | - | src="https://cdn-images-1.medium.com/max/2000/1*UrdopTgDhErJObLI3V8DwQ.png" |
|
| 198 | - | alt="dev tools" |
|
| 199 | - | width={1920} |
|
| 200 | - | aspectRatio={16/4} |
|
| 195 | + | src="https://cdn-images-1.medium.com/max/2000/1*UrdopTgDhErJObLI3V8DwQ.png" |
|
| 196 | + | alt="dev tools" |
|
| 197 | + | width={1920} |
|
| 198 | + | aspectRatio={16 / 4} |
|
| 201 | 199 | /> |
|
| 202 | 200 | ||
| 203 | 201 | Now that we have the data, it’s as simple as displaying it so users on our website can see it! |
|
| 205 | 203 | To store the data we’ll import the useState hook at the top of our app along with useEffect |
|
| 206 | 204 | ||
| 207 | 205 | ```javascript |
|
| 208 | - | import { useEffect, useState } from "react" |
|
| 206 | + | import { useEffect, useState } from "react"; |
|
| 209 | 207 | ``` |
|
| 210 | 208 | ||
| 211 | 209 | Then right above our function to grab the data, we’ll declare our state variable as an empty array where we can push stuff in later. |
|
| 212 | 210 | ||
| 213 | 211 | ```javascript |
|
| 214 | - | const [emissionsData, setEmissionsData] = useState([]) |
|
| 212 | + | const [emissionsData, setEmissionsData] = useState([]); |
|
| 215 | 213 | ``` |
|
| 216 | 214 | ||
| 217 | 215 | Now all we have to do is edit our function just a little bit to push that data into our state using the “setEmissionsData”! Here is our function now |
|
| 218 | 216 | ||
| 219 | 217 | ```javascript |
|
| 220 | 218 | const getEmissionsData = async () => { |
|
| 221 | - | try{ |
|
| 222 | - | const response = await axios.get("https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b") |
|
| 223 | - | console.log(response.data) |
|
| 224 | - | setEmissionsData(response.data) |
|
| 225 | - | } catch (error) { |
|
| 226 | - | console.log(error) |
|
| 227 | - | } |
|
| 228 | - | } |
|
| 219 | + | try { |
|
| 220 | + | const response = await axios.get( |
|
| 221 | + | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 222 | + | ); |
|
| 223 | + | console.log(response.data); |
|
| 224 | + | setEmissionsData(response.data); |
|
| 225 | + | } catch (error) { |
|
| 226 | + | console.log(error); |
|
| 227 | + | } |
|
| 228 | + | }; |
|
| 229 | 229 | ``` |
|
| 230 | 230 | ||
| 231 | 231 | The next thing we’re gonna do is create two components inside our file, one as a loading indicator, and the other as the data we want to display. To do this we’ll just make two more functions like this |
|
| 232 | 232 | ||
| 233 | 233 | ```javascript |
|
| 234 | 234 | const loading = () => ( |
|
| 235 | - | <div className="loading-container"> |
|
| 236 | - | <h1>Loading</h1> |
|
| 237 | - | </div> |
|
| 238 | - | ) |
|
| 235 | + | <div className="loading-container"> |
|
| 236 | + | <h1>Loading</h1> |
|
| 237 | + | </div> |
|
| 238 | + | ); |
|
| 239 | 239 | ||
| 240 | 240 | const emissionsComponent = () => { |
|
| 241 | - | ||
| 242 | - | return ( |
|
| 243 | - | <div className="data-container"> |
|
| 244 | - | <h1>Data goes here</h1> |
|
| 245 | - | </div> |
|
| 246 | - | ) |
|
| 247 | - | } |
|
| 241 | + | return ( |
|
| 242 | + | <div className="data-container"> |
|
| 243 | + | <h1>Data goes here</h1> |
|
| 244 | + | </div> |
|
| 245 | + | ); |
|
| 246 | + | }; |
|
| 248 | 247 | ``` |
|
| 249 | 248 | ||
| 250 | 249 | To switch between the two we’ll create a new state called “isLoading” right underneath our previous state. We’ll set the default value to “false” for now |
|
| 251 | 250 | ||
| 252 | 251 | ```javascript |
|
| 253 | - | const [emissionsData, setEmissionsData] = useState([]) |
|
| 254 | - | const [isLoading, setIsLoading] = useState(false) |
|
| 252 | + | const [emissionsData, setEmissionsData] = useState([]); |
|
| 253 | + | const [isLoading, setIsLoading] = useState(false); |
|
| 255 | 254 | ``` |
|
| 256 | 255 | ||
| 257 | 256 | Back in our getEmissionsData function we need to turn the “loading” on when we start the request, and then off when we’re done. |
|
| 258 | 257 | ||
| 259 | 258 | ```javascript |
|
| 260 | 259 | const getEmissionsData = async () => { |
|
| 261 | - | try{ |
|
| 262 | - | setIsLoading(true) |
|
| 263 | - | const response = await axios.get("https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b") |
|
| 264 | - | console.log(response.data) |
|
| 265 | - | setEmissionsData(response.data) |
|
| 266 | - | setIsLoading(false) |
|
| 267 | - | } catch (error) { |
|
| 268 | - | console.log(error) |
|
| 269 | - | } |
|
| 270 | - | } |
|
| 260 | + | try { |
|
| 261 | + | setIsLoading(true); |
|
| 262 | + | const response = await axios.get( |
|
| 263 | + | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 264 | + | ); |
|
| 265 | + | console.log(response.data); |
|
| 266 | + | setEmissionsData(response.data); |
|
| 267 | + | setIsLoading(false); |
|
| 268 | + | } catch (error) { |
|
| 269 | + | console.log(error); |
|
| 270 | + | } |
|
| 271 | + | }; |
|
| 271 | 272 | ``` |
|
| 272 | 273 | ||
| 273 | 274 | Finally, way down at the bottom where we render the whole app, we’ll add in some conditional rendering to say “display the loading component while loading, then display the data component when not loading.” |
|
| 274 | 275 | ||
| 275 | 276 | ```javascript |
|
| 276 | - | return ( |
|
| 277 | - | <div className="aerial-container"> |
|
| 278 | - | {isLoading ? loading() : emissionsComponent()} |
|
| 279 | - | </div> |
|
| 280 | - | ) |
|
| 277 | + | return <div className="aerial-container">{isLoading ? loading() : emissionsComponent()}</div>; |
|
| 281 | 278 | ``` |
|
| 282 | 279 | ||
| 283 | 280 | As a quick recap this is what our component looks like at the moment |
|
| 284 | 281 | ||
| 285 | 282 | ```javascript |
|
| 286 | - | import { useState, useEffect } from "react" |
|
| 287 | - | import axios from "axios" |
|
| 283 | + | import { useState, useEffect } from "react"; |
|
| 284 | + | import axios from "axios"; |
|
| 288 | 285 | ||
| 289 | 286 | const Aerial = () => { |
|
| 287 | + | const [emissionsData, setEmissionsData] = useState([]); |
|
| 288 | + | const [isLoading, setIsLoading] = useState(false); |
|
| 290 | 289 | ||
| 291 | - | const [emissionsData, setEmissionsData] = useState([]) |
|
| 292 | - | const [isLoading, setIsLoading] = useState(false) |
|
| 290 | + | const getEmissionsData = async () => { |
|
| 291 | + | try { |
|
| 292 | + | setIsLoading(true); |
|
| 293 | + | const response = await axios.get( |
|
| 294 | + | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 295 | + | ); |
|
| 296 | + | console.log(response.data); |
|
| 297 | + | setEmissionsData(response.data); |
|
| 298 | + | setIsLoading(false); |
|
| 299 | + | } catch (error) { |
|
| 300 | + | console.log(error); |
|
| 301 | + | } |
|
| 302 | + | }; |
|
| 293 | 303 | ||
| 294 | - | const getEmissionsData = async () => { |
|
| 295 | - | try{ |
|
| 296 | - | setIsLoading(true) |
|
| 297 | - | const response = await axios.get("https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b") |
|
| 298 | - | console.log(response.data) |
|
| 299 | - | setEmissionsData(response.data) |
|
| 300 | - | setIsLoading(false) |
|
| 301 | - | } catch (error) { |
|
| 302 | - | console.log(error) |
|
| 303 | - | } |
|
| 304 | - | } |
|
| 304 | + | const loading = () => ( |
|
| 305 | + | <div className="loading-container"> |
|
| 306 | + | <h1>Loading</h1> |
|
| 307 | + | </div> |
|
| 308 | + | ); |
|
| 305 | 309 | ||
| 306 | - | const loading = () => ( |
|
| 307 | - | <div className="loading-container"> |
|
| 308 | - | <h1>Loading</h1> |
|
| 309 | - | </div> |
|
| 310 | - | ) |
|
| 311 | - | ||
| 312 | - | const emissionsComponent = () => { |
|
| 313 | - | ||
| 314 | - | return ( |
|
| 315 | - | <div className="data-container"> |
|
| 316 | - | <h1>Data goes here</h1> |
|
| 317 | - | </div> |
|
| 318 | - | ) |
|
| 319 | - | } |
|
| 320 | - | ||
| 310 | + | const emissionsComponent = () => { |
|
| 311 | + | return ( |
|
| 312 | + | <div className="data-container"> |
|
| 313 | + | <h1>Data goes here</h1> |
|
| 314 | + | </div> |
|
| 315 | + | ); |
|
| 316 | + | }; |
|
| 321 | 317 | ||
| 322 | - | useEffect(()=> { |
|
| 323 | - | getEmissionsData() |
|
| 324 | - | }, []) |
|
| 318 | + | useEffect(() => { |
|
| 319 | + | getEmissionsData(); |
|
| 320 | + | }, []); |
|
| 325 | 321 | ||
| 326 | - | return ( |
|
| 327 | - | <div className="aerial-container"> |
|
| 328 | - | {isLoading ? loading() : emissionsComponent()} |
|
| 329 | - | </div> |
|
| 330 | - | ) |
|
| 331 | - | } |
|
| 322 | + | return <div className="aerial-container">{isLoading ? loading() : emissionsComponent()}</div>; |
|
| 323 | + | }; |
|
| 332 | 324 | ||
| 333 | 325 | export default Aerial; |
|
| 334 | 326 | ``` |
|
| 338 | 330 | The first is the CO2 emissions, which the API returns as a raw number in the unit of Kg. The best way to make this number manageable in my opinion is to round up the number, and use some javascript to add the commas for each three digits. So back in our emissionsComponent, I have declared the following variable from our saved state. |
|
| 339 | 331 | ||
| 340 | 332 | ```javascript |
|
| 341 | - | const co2 = new Intl.NumberFormat().format(Math.round(emissionsData.co2)) |
|
| 333 | + | const co2 = new Intl.NumberFormat().format(Math.round(emissionsData.co2)); |
|
| 342 | 334 | ``` |
|
| 343 | 335 | ||
| 344 | 336 | Next up is the gas used for this project, and for this one it returns a whole number so no need to round it up. We’ll just format it to be readable. |
|
| 345 | 337 | ||
| 346 | 338 | ```javascript |
|
| 347 | - | const gas = new Intl.NumberFormat().format(emissionsData.gas) |
|
| 339 | + | const gas = new Intl.NumberFormat().format(emissionsData.gas); |
|
| 348 | 340 | ``` |
|
| 349 | 341 | ||
| 350 | 342 | Aerial of course also provides their unit for donations to offset the carbon emissions they call “credits.” The API can return the total credits need to make the NFT project carbon neutral, how many are purchased so far, and how much it would cost in total to offset the project in USD. To make this data more readable, we want to display how many credits have been purchased, how many are needed to offset, and how much it would cost to completely offset the project. We just need a little math to make that happen! |
|
| 352 | 344 | For the credits remaining, we just need to subtract the credits already purchased from the total credits needed to offset like so. |
|
| 353 | 345 | ||
| 354 | 346 | ```javascript |
|
| 355 | - | const creditsRemaining = new Intl.NumberFormat().format(emissionsData.credits - emissionsData.credits_purchased) |
|
| 347 | + | const creditsRemaining = new Intl.NumberFormat().format( |
|
| 348 | + | emissionsData.credits - emissionsData.credits_purchased |
|
| 349 | + | ); |
|
| 356 | 350 | ``` |
|
| 357 | 351 | ||
| 358 | 352 | Of course we want to display how many have already purchased and that’s pretty simple. |
|
| 359 | 353 | ||
| 360 | 354 | ```javascript |
|
| 361 | - | const creditsPurchased = new Intl.NumberFormat().format(emissionsData.credits_purchased) |
|
| 355 | + | const creditsPurchased = new Intl.NumberFormat().format(emissionsData.credits_purchased); |
|
| 362 | 356 | ``` |
|
| 363 | 357 | ||
| 364 | 358 | Now the more complicated part is calculating how much the remaining cost is. The API gives us the total cost, but it does not include how much has been spent. So for us to get this number we need to divide the emissions cost with the total credits needed to offset, then multiply that against the total emissions credits minus the credits already purchased. In the end it looks like this! |
|
| 365 | 359 | ||
| 366 | 360 | ```javascript |
|
| 367 | - | const cost = new Intl.NumberFormat().format((emissionsData.cost / emissionsData.credits) * (emissionsData.credits - emissionsData.credits_purchased)) |
|
| 361 | + | const cost = new Intl.NumberFormat().format( |
|
| 362 | + | (emissionsData.cost / emissionsData.credits) * |
|
| 363 | + | (emissionsData.credits - emissionsData.credits_purchased) |
|
| 364 | + | ); |
|
| 368 | 365 | ``` |
|
| 369 | 366 | ||
| 370 | 367 | Lastly, we want the number of total number of transactions already offset. |
|
| 377 | 374 | ||
| 378 | 375 | ```javascript |
|
| 379 | 376 | return ( |
|
| 380 | - | <div className="data-container"> |
|
| 381 | - | <div className="header"> |
|
| 382 | - | <h1>Deadfellaz Carbon Offset</h1> |
|
| 383 | - | </div> |
|
| 384 | - | <div className="data-grid"> |
|
| 385 | - | <div className="data-cell"> |
|
| 386 | - | <h2>{co2} Kg</h2> |
|
| 387 | - | <h3>CO2 Emissions</h3> |
|
| 388 | - | </div> |
|
| 389 | - | <div className="data-cell"> |
|
| 390 | - | <h2>{gas}</h2> |
|
| 391 | - | <h3>Gas Used</h3> |
|
| 392 | - | </div> |
|
| 393 | - | <div className="data-cell"> |
|
| 394 | - | <h2>{transactions}</h2> |
|
| 395 | - | <h3>Transactions</h3> |
|
| 396 | - | </div> |
|
| 397 | - | <div className="data-cell"> |
|
| 398 | - | <h2>${cost}</h2> |
|
| 399 | - | <h3>Cost to Offset</h3> |
|
| 400 | - | </div> |
|
| 401 | - | <div className="data-cell"> |
|
| 402 | - | <h2>{creditsRemaining}</h2> |
|
| 403 | - | <h3>Credits needed to offset</h3> |
|
| 404 | - | </div> |
|
| 405 | - | <div className="data-cell"> |
|
| 406 | - | <h2>{creditsPurchased}</h2> |
|
| 407 | - | <h3>Credits Purchased so Far</h3> |
|
| 408 | - | </div> |
|
| 409 | - | </div> |
|
| 410 | - | <a className="cta-button" target="_blank" rel="noreferrer" href="https://aerial.is/nft/0x2acab3dea77832c09420663b0e1cb386031ba17b">Offset</a> |
|
| 411 | - | </div> |
|
| 412 | - | ) |
|
| 377 | + | <div className="data-container"> |
|
| 378 | + | <div className="header"> |
|
| 379 | + | <h1>Deadfellaz Carbon Offset</h1> |
|
| 380 | + | </div> |
|
| 381 | + | <div className="data-grid"> |
|
| 382 | + | <div className="data-cell"> |
|
| 383 | + | <h2>{co2} Kg</h2> |
|
| 384 | + | <h3>CO2 Emissions</h3> |
|
| 385 | + | </div> |
|
| 386 | + | <div className="data-cell"> |
|
| 387 | + | <h2>{gas}</h2> |
|
| 388 | + | <h3>Gas Used</h3> |
|
| 389 | + | </div> |
|
| 390 | + | <div className="data-cell"> |
|
| 391 | + | <h2>{transactions}</h2> |
|
| 392 | + | <h3>Transactions</h3> |
|
| 393 | + | </div> |
|
| 394 | + | <div className="data-cell"> |
|
| 395 | + | <h2>${cost}</h2> |
|
| 396 | + | <h3>Cost to Offset</h3> |
|
| 397 | + | </div> |
|
| 398 | + | <div className="data-cell"> |
|
| 399 | + | <h2>{creditsRemaining}</h2> |
|
| 400 | + | <h3>Credits needed to offset</h3> |
|
| 401 | + | </div> |
|
| 402 | + | <div className="data-cell"> |
|
| 403 | + | <h2>{creditsPurchased}</h2> |
|
| 404 | + | <h3>Credits Purchased so Far</h3> |
|
| 405 | + | </div> |
|
| 406 | + | </div> |
|
| 407 | + | <a |
|
| 408 | + | className="cta-button" |
|
| 409 | + | target="_blank" |
|
| 410 | + | rel="noreferrer" |
|
| 411 | + | href="https://aerial.is/nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 412 | + | > |
|
| 413 | + | Offset |
|
| 414 | + | </a> |
|
| 415 | + | </div> |
|
| 416 | + | ); |
|
| 413 | 417 | ``` |
|
| 414 | 418 | ||
| 415 | 419 | At the bottom we’ve also added a button where a user can click on it and be directed to Aerials page for that particular NFT project, where they can make purchase credits to help offset that project. |
|
| 417 | 421 | To make this whole component feel clean as well as themed for the project, we took some colors and styled from DeadFellaz and added it to this page with some CSS; we also added a fun Lottie animation for the loading component instead of just a header that says loading. In the end our code looks like this! |
|
| 418 | 422 | ||
| 419 | 423 | ```javascript |
|
| 420 | - | import { useState, useEffect } from "react" |
|
| 421 | - | import "./Aerial.css" |
|
| 424 | + | import { useState, useEffect } from "react"; |
|
| 425 | + | import "./Aerial.css"; |
|
| 422 | 426 | import axios from "axios"; |
|
| 423 | - | import Lottie from "react-lottie" |
|
| 424 | - | import co2 from "./co2.json" |
|
| 425 | - | ||
| 427 | + | import Lottie from "react-lottie"; |
|
| 428 | + | import co2 from "./co2.json"; |
|
| 426 | 429 | ||
| 427 | 430 | const Aerial = () => { |
|
| 431 | + | const [emissionsData, setEmissionsData] = useState([]); |
|
| 432 | + | const [isLoading, setIsLoading] = useState(false); |
|
| 428 | 433 | ||
| 429 | - | const [emissionsData, setEmissionsData] = useState([]) |
|
| 430 | - | const [isLoading, setIsLoading] = useState(false) |
|
| 434 | + | const getEmissionsData = async () => { |
|
| 435 | + | try { |
|
| 436 | + | setIsLoading(true); |
|
| 437 | + | const response = await axios.get( |
|
| 438 | + | "https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 439 | + | ); |
|
| 440 | + | console.log(response.data); |
|
| 441 | + | setEmissionsData(response.data); |
|
| 442 | + | setIsLoading(false); |
|
| 443 | + | } catch (error) { |
|
| 444 | + | console.log(error); |
|
| 445 | + | } |
|
| 446 | + | }; |
|
| 431 | 447 | ||
| 432 | - | const getEmissionsData = async () => { |
|
| 433 | - | try{ |
|
| 434 | - | setIsLoading(true) |
|
| 435 | - | const response = await axios.get("https://aerial.is/_nft/0x2acab3dea77832c09420663b0e1cb386031ba17b") |
|
| 436 | - | console.log(response.data) |
|
| 437 | - | setEmissionsData(response.data) |
|
| 438 | - | setIsLoading(false) |
|
| 439 | - | } catch (error) { |
|
| 440 | - | console.log(error) |
|
| 441 | - | } |
|
| 442 | - | } |
|
| 448 | + | const loading = () => ( |
|
| 449 | + | <div className="loading-container"> |
|
| 450 | + | <Lottie options={{ animationData: co2 }} height={400} width={400} /> |
|
| 451 | + | </div> |
|
| 452 | + | ); |
|
| 443 | 453 | ||
| 444 | - | const loading = () => ( |
|
| 445 | - | <div className="loading-container"> |
|
| 446 | - | <Lottie options={{animationData: co2}} height={400} width={400} /> |
|
| 447 | - | </div> |
|
| 448 | - | ) |
|
| 449 | - | ||
| 450 | - | const emissionsComponent = () => { |
|
| 454 | + | const emissionsComponent = () => { |
|
| 455 | + | const co2 = new Intl.NumberFormat().format(Math.round(emissionsData.co2)); |
|
| 456 | + | const gas = new Intl.NumberFormat().format(emissionsData.gas); |
|
| 457 | + | const creditsRemaining = new Intl.NumberFormat().format( |
|
| 458 | + | emissionsData.credits - emissionsData.credits_purchased |
|
| 459 | + | ); |
|
| 460 | + | const creditsPurchased = new Intl.NumberFormat().format(emissionsData.credits_purchased); |
|
| 461 | + | const cost = new Intl.NumberFormat().format( |
|
| 462 | + | (emissionsData.cost / emissionsData.credits) * |
|
| 463 | + | (emissionsData.credits - emissionsData.credits_purchased) |
|
| 464 | + | ); |
|
| 465 | + | const transactions = new Intl.NumberFormat().format(emissionsData.transactions); |
|
| 451 | 466 | ||
| 452 | - | const co2 = new Intl.NumberFormat().format(Math.round(emissionsData.co2)) |
|
| 453 | - | const gas = new Intl.NumberFormat().format(emissionsData.gas) |
|
| 454 | - | const creditsRemaining = new Intl.NumberFormat().format(emissionsData.credits - emissionsData.credits_purchased) |
|
| 455 | - | const creditsPurchased = new Intl.NumberFormat().format(emissionsData.credits_purchased) |
|
| 456 | - | const cost = new Intl.NumberFormat().format((emissionsData.cost / emissionsData.credits) * (emissionsData.credits - emissionsData.credits_purchased)) |
|
| 457 | - | const transactions = new Intl.NumberFormat().format(emissionsData.transactions) |
|
| 458 | - | ||
| 459 | - | return ( |
|
| 460 | - | <div className="data-container"> |
|
| 461 | - | <div className="header"> |
|
| 462 | - | <h1>Deadfellaz Carbon Offset</h1> |
|
| 463 | - | </div> |
|
| 464 | - | <div className="data-grid"> |
|
| 465 | - | <div className="data-cell"> |
|
| 466 | - | <h2>{co2} Kg</h2> |
|
| 467 | - | <h3>CO2 Emissions</h3> |
|
| 468 | - | </div> |
|
| 469 | - | <div className="data-cell"> |
|
| 470 | - | <h2>{gas}</h2> |
|
| 471 | - | <h3>Gas Used</h3> |
|
| 472 | - | </div> |
|
| 473 | - | <div className="data-cell"> |
|
| 474 | - | <h2>{transactions}</h2> |
|
| 475 | - | <h3>Transactions</h3> |
|
| 476 | - | </div> |
|
| 477 | - | <div className="data-cell"> |
|
| 478 | - | <h2>${cost}</h2> |
|
| 479 | - | <h3>Cost to Offset</h3> |
|
| 480 | - | </div> |
|
| 481 | - | <div className="data-cell"> |
|
| 482 | - | <h2>{creditsRemaining}</h2> |
|
| 483 | - | <h3>Credits needed to offset</h3> |
|
| 484 | - | </div> |
|
| 485 | - | <div className="data-cell"> |
|
| 486 | - | <h2>{creditsPurchased}</h2> |
|
| 487 | - | <h3>Credits Purchased so Far</h3> |
|
| 488 | - | </div> |
|
| 489 | - | </div> |
|
| 490 | - | <a className="cta-button" target="_blank" rel="noreferrer" href="https://aerial.is/nft/0x2acab3dea77832c09420663b0e1cb386031ba17b">Offset</a> |
|
| 491 | - | </div> |
|
| 492 | - | ) |
|
| 493 | - | } |
|
| 494 | - | ||
| 467 | + | return ( |
|
| 468 | + | <div className="data-container"> |
|
| 469 | + | <div className="header"> |
|
| 470 | + | <h1>Deadfellaz Carbon Offset</h1> |
|
| 471 | + | </div> |
|
| 472 | + | <div className="data-grid"> |
|
| 473 | + | <div className="data-cell"> |
|
| 474 | + | <h2>{co2} Kg</h2> |
|
| 475 | + | <h3>CO2 Emissions</h3> |
|
| 476 | + | </div> |
|
| 477 | + | <div className="data-cell"> |
|
| 478 | + | <h2>{gas}</h2> |
|
| 479 | + | <h3>Gas Used</h3> |
|
| 480 | + | </div> |
|
| 481 | + | <div className="data-cell"> |
|
| 482 | + | <h2>{transactions}</h2> |
|
| 483 | + | <h3>Transactions</h3> |
|
| 484 | + | </div> |
|
| 485 | + | <div className="data-cell"> |
|
| 486 | + | <h2>${cost}</h2> |
|
| 487 | + | <h3>Cost to Offset</h3> |
|
| 488 | + | </div> |
|
| 489 | + | <div className="data-cell"> |
|
| 490 | + | <h2>{creditsRemaining}</h2> |
|
| 491 | + | <h3>Credits needed to offset</h3> |
|
| 492 | + | </div> |
|
| 493 | + | <div className="data-cell"> |
|
| 494 | + | <h2>{creditsPurchased}</h2> |
|
| 495 | + | <h3>Credits Purchased so Far</h3> |
|
| 496 | + | </div> |
|
| 497 | + | </div> |
|
| 498 | + | <a |
|
| 499 | + | className="cta-button" |
|
| 500 | + | target="_blank" |
|
| 501 | + | rel="noreferrer" |
|
| 502 | + | href="https://aerial.is/nft/0x2acab3dea77832c09420663b0e1cb386031ba17b" |
|
| 503 | + | > |
|
| 504 | + | Offset |
|
| 505 | + | </a> |
|
| 506 | + | </div> |
|
| 507 | + | ); |
|
| 508 | + | }; |
|
| 495 | 509 | ||
| 496 | - | useEffect(()=> { |
|
| 497 | - | getEmissionsData() |
|
| 498 | - | }, []) |
|
| 510 | + | useEffect(() => { |
|
| 511 | + | getEmissionsData(); |
|
| 512 | + | }, []); |
|
| 499 | 513 | ||
| 500 | - | return ( |
|
| 501 | - | <div className="aerial-container"> |
|
| 502 | - | {isLoading ? loading() : emissionsComponent()} |
|
| 503 | - | </div> |
|
| 504 | - | ) |
|
| 505 | - | } |
|
| 514 | + | return <div className="aerial-container">{isLoading ? loading() : emissionsComponent()}</div>; |
|
| 515 | + | }; |
|
| 506 | 516 | ||
| 507 | 517 | export default Aerial; |
|
| 508 | 518 | ``` |
|
| 7 | 7 | ||
| 8 | 8 | ## It Started with Clickbait |
|
| 9 | 9 | ||
| 10 | - | It was late September in 2020, our first son was just born and I was waiting in the car while my wife took him to the hospital for a checkup (becuase of the Covid-19 restrictions at the time only one of us could go in). I had spent the majority of my life doing multiple jobs in various fields. My college degree was in liberal arts so of course it only did so much good in the professional world, so I started small by working in the footwear department at Bass Pro Shops. From there I slowly worked up the chain and eventually ran the archery department. |
|
| 10 | + | It was late September in 2020, our first son was just born and I was waiting in the car while my wife took him to the hospital for a checkup (becuase of the Covid-19 restrictions at the time only one of us could go in). I had spent the majority of my life doing multiple jobs in various fields. My college degree was in liberal arts so of course it only did so much good in the professional world, so I started small by working in the footwear department at Bass Pro Shops. From there I slowly worked up the chain and eventually ran the archery department. |
|
| 11 | 11 | ||
| 12 | - | It was a fair job for three to four years but eventually the wear of retail grew on me and I got tired of working late hours. That's when I transitioned into banking as a teller, as I heard it was a good out from retail. After working as a teller for about a year I moved to the back office customer service position. There I worked 8.5 hours a day taking phone calls and helping customers with online banking, debit card problems, or just checking a balance. It was a pretty nice gig since I got to help people and work with some pieces of tech, and later down the road I eventually helped managed the department. That position also helped me learn how to be productive, type faster, and operate a keyboard only interface quickly. |
|
| 12 | + | It was a fair job for three to four years but eventually the wear of retail grew on me and I got tired of working late hours. That's when I transitioned into banking as a teller, as I heard it was a good out from retail. After working as a teller for about a year I moved to the back office customer service position. There I worked 8.5 hours a day taking phone calls and helping customers with online banking, debit card problems, or just checking a balance. It was a pretty nice gig since I got to help people and work with some pieces of tech, and later down the road I eventually helped managed the department. That position also helped me learn how to be productive, type faster, and operate a keyboard only interface quickly. |
|
| 13 | 13 | ||
| 14 | - | As you would expect talking to people all day every day took a toll on my mental health after four years, and that's about when my son was born. I had about three weeks of vacation and sick time off to help my wife before going back to work, and yeah I really didn't want to go back after taking a good solid break. I sat in that hospital parking lot, scrolling through YouTube, when I came across a video. This video to be precise: |
|
| 14 | + | As you would expect talking to people all day every day took a toll on my mental health after four years, and that's about when my son was born. I had about three weeks of vacation and sick time off to help my wife before going back to work, and yeah I really didn't want to go back after taking a good solid break. I sat in that hospital parking lot, scrolling through YouTube, when I came across a video. This video to be precise: |
|
| 15 | 15 | ||
| 16 | 16 | **[How I Learned to Code - And Got a Job in Less Than 3 Months](https://youtu.be/nupkQD_Mnhg)** |
|
| 17 | 17 | ||
| 18 | 18 | ## The Grind |
|
| 19 | 19 | ||
| 20 | - | Of course the title is clickbait and I was hooked. I didn't learn to code in three months, but I did get started. I bought [Head First: HTML with CSS and XHTML](https://www.amazon.com/Head-First-HTML-CSS-Standards-Based/dp/0596159900) off eBay for $10 and blew through it in a weekend; I just couldn't stop consuming knowledge about web development. This wasn't programming just yet, but the magic was there because I watched text on a screen transform into something visual. I've had a creative background with music and photography, and the ability to create something with lines of code was fascinating. The next book was [Head First Java](https://www.amazon.com/Head-First-Java-Brain-Learners/dp/0596004656?keywords=head+first+java&qid=1677605428&sr=8-5) which did not click with me at all. I barely grasped the basic programming principles. I couldn't understand how they connected with web sites and made things work, and that was likely due to using 5-10 year old books. I switched up and went to YouTube again and found some web development roadmap videos which gave me a rough guideline of what I needed to learn. |
|
| 20 | + | Of course the title is clickbait and I was hooked. I didn't learn to code in three months, but I did get started. I bought [Head First: HTML with CSS and XHTML](https://www.amazon.com/Head-First-HTML-CSS-Standards-Based/dp/0596159900) off eBay for $10 and blew through it in a weekend; I just couldn't stop consuming knowledge about web development. This wasn't programming just yet, but the magic was there because I watched text on a screen transform into something visual. I've had a creative background with music and photography, and the ability to create something with lines of code was fascinating. The next book was [Head First Java](https://www.amazon.com/Head-First-Java-Brain-Learners/dp/0596004656?keywords=head+first+java&qid=1677605428&sr=8-5) which did not click with me at all. I barely grasped the basic programming principles. I couldn't understand how they connected with web sites and made things work, and that was likely due to using 5-10 year old books. I switched up and went to YouTube again and found some web development roadmap videos which gave me a rough guideline of what I needed to learn. |
|
| 21 | 21 | ||
| 22 | - | The next year was spent grinding through some coursed by [Ed](https://developedbyed.com/), starting with basic HTML, CSS, and Javascript, and eventually React. That was a rough period, because I was still working at the bank full time. I was helping take care of a difficult newborn baby, and learning something completely new. I would wake up at 5am most days, completely exhausted yet pushing through concept after concept and project after project. After work I would come home, help around the house, and later in the evening I would keep coding. While it was a lot of work, it was totally worth it. |
|
| 22 | + | The next year was spent grinding through some coursed by [Ed](https://developedbyed.com/), starting with basic HTML, CSS, and Javascript, and eventually React. That was a rough period, because I was still working at the bank full time. I was helping take care of a difficult newborn baby, and learning something completely new. I would wake up at 5am most days, completely exhausted yet pushing through concept after concept and project after project. After work I would come home, help around the house, and later in the evening I would keep coding. While it was a lot of work, it was totally worth it. |
|
| 23 | 23 | ||
| 24 | 24 | ## The First Smart Contract |
|
| 25 | + | ||
| 25 | 26 | After about a year I was getting to a point where I was creating projects with the goal of having a portfolio I could use for applying to jobs. That's when I stumbled upon Web3. I can't remember how, but I found a project on [Buildspace](https://buildspace.so) that introduced me to blockchain, Ethereum, and smart contracts. I'll never forget the feeling of deploying my first smart contract and interacting with it from a front end website. This was it; I knew from there I wanted to work in this new internet and make it better. I built countless Web3 projects, some of them included minting NFTs, which is where I stumbled upon [Pinata](https://pinata.cloud). When I started to look for jobs I saw that Pinata was hiring a community manager, and even though I was looking to be a developer, I was fond of the idea that I could use some of my other skills like support and customer service in the industry. I applied for the job, and within a month I was hired! |
|
| 26 | 27 | ||
| 27 | 28 | As the community manager then and head of community now, I've had another year of being able to learn technical products and help people use them and understand them. I still get to write code that demonstrates what Pinata can do and snippets to help make it easier to use, which I absolutely love. Pinata took a chance on some guy who used to fetch shoes and take phone calls, and because of that I've been able to relocate to a better city where I can raise my family for which I am incredibly grateful. Sitting down to work each day is exciting because I know that I can learn just about anything and I can teach it to others. |
|
| 28 | 29 | ||
| 29 | - | I'm starting this blog to document more of what I'm learning in the Web3 and tech space in hopes that others find it beneficial. If you get anything from this post, let it be these words from the beloved Ratatouille that can apply to just about anything in life: |
|
| 30 | - | > In the past, I have made no secret of my disdain for Chef Gusteau's famous motto, 'Anyone can cook.' But I realize, only now do I truly understand what he meant. |
|
| 30 | + | I'm starting this blog to document more of what I'm learning in the Web3 and tech space in hopes that others find it beneficial. If you get anything from this post, let it be these words from the beloved Ratatouille that can apply to just about anything in life: |
|
| 31 | + | ||
| 32 | + | > In the past, I have made no secret of my disdain for Chef Gusteau's famous motto, 'Anyone can cook.' But I realize, only now do I truly understand what he meant. |
|
| 31 | 33 | ||
| 32 | 34 | > Not everyone can become a great artist; but a great artist **can** come from **anywhere**. |
| 6 | 6 | ogImage: "https://miro.medium.com/v2/resize:fit:4800/format:webp/1*Tp42Ey9Uvdb6njsaXHBOTA.jpeg" |
|
| 7 | 7 | --- |
|
| 8 | 8 | ||
| 9 | - | import medium from "../../assets/medium.png" |
|
| 10 | - | import OutLinkButton from "../../components/OutLinkButton.astro" |
|
| 9 | + | import medium from "../../assets/medium.png"; |
|
| 10 | + | import OutLinkButton from "../../components/OutLinkButton.astro"; |
|
| 11 | 11 | ||
| 12 | - | <OutLinkButton link="https://medium.com/pinata/resizing-ipfs-images-with-pinatas-image-optimization-tools-fb381bee58aa" site="Medium" image={medium} /> |
|
| 13 | - | ||
| 12 | + | <OutLinkButton |
|
| 13 | + | link="https://medium.com/pinata/resizing-ipfs-images-with-pinatas-image-optimization-tools-fb381bee58aa" |
|
| 14 | + | site="Medium" |
|
| 15 | + | image={medium} |
|
| 16 | + | /> |
|
| 14 | 17 | ||
| 15 | 18 | If you’re a developer in the NFT space, you have probably had to fetch IPFS content before, and depending what tools you use the experience is varied. Using a local IPFS node is not very practical or fast, and using a public gateway can be risky due to congestion. Dedicated Gateways on the other hand are much faster, and are great for app development. But what if you have to fetch an entire NFT project through IPFS? That could be 10,000 images at 5Mb each, awful for web page optimization, and you have to load every. single. one. How is that gonna work? And then what happens when you have another NFT project? |
|
| 16 | 19 | ||
| 64 | 67 | ||
| 65 | 68 | Check that out. Instead of being a 10,000x10,000 resolution, it’s now 1080x1080 and only 266Kb. Doesn’t get any simpler than that — and this is only the beginning of what this tool can do. Here’s a small list of other things you can do: |
|
| 66 | 69 | ||
| 67 | - | * DPR (Device Pixel Ratio) |
|
| 68 | - | * Image Fit — for scaling down, image positions and more! |
|
| 69 | - | * Image Quality — Set a scale from 1–100 to easily reduce a high quality image |
|
| 70 | - | * Auto Image Formatting — Use Webp where supported, but then fall back to jpeg or png |
|
| 71 | - | * Animation Still — Turn a gif into a still image |
|
| 72 | - | * On Error Redirect — Redirect to a different image if there is a problem |
|
| 73 | - | * Metadata Controls — Control what EXIF data is revealed with the image |
|
| 70 | + | - DPR (Device Pixel Ratio) |
|
| 71 | + | - Image Fit — for scaling down, image positions and more! |
|
| 72 | + | - Image Quality — Set a scale from 1–100 to easily reduce a high quality image |
|
| 73 | + | - Auto Image Formatting — Use Webp where supported, but then fall back to jpeg or png |
|
| 74 | + | - Animation Still — Turn a gif into a still image |
|
| 75 | + | - On Error Redirect — Redirect to a different image if there is a problem |
|
| 76 | + | - Metadata Controls — Control what EXIF data is revealed with the image |
|
| 74 | 77 | ||
| 75 | 78 | Pinata Image Resizing really gives you control as a developer in the NFT space to handle IPFS images with ease. We highly recommend checking out our [developer docs](https://docs.pinata.cloud) to see them all. |
|
| 76 | 79 | ||
| 92 | 95 | ||
| 93 | 96 | ```javascript |
|
| 94 | 97 | function App() { |
|
| 95 | - | return ( |
|
| 96 | - | <div className="App"> |
|
| 97 | - | <div className="grid"> |
|
| 98 | - | ||
| 99 | - | </div> |
|
| 100 | - | </div> |
|
| 101 | - | ); |
|
| 98 | + | return ( |
|
| 99 | + | <div className="App"> |
|
| 100 | + | <div className="grid"></div> |
|
| 101 | + | </div> |
|
| 102 | + | ); |
|
| 102 | 103 | } |
|
| 103 | 104 | export default App; |
|
| 104 | 105 | ``` |
|
| 111 | 112 | ||
| 112 | 113 | Let’s break this down again so we know what’s going on. |
|
| 113 | 114 | ||
| 114 | - | We have our gateway url ```https://stevedsimkins.mypinata.cloud/ipfs/```, then we have our CID ```QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ```, then our dynamic image id ```${id}.jpg``` and finally our image optimization ```?img-width=1080&img-height=1080```. Of course not all PFP projects are this simple, but with this formatting you can pass in multiple parameters with objects to adjust to your needs. |
|
| 115 | + | We have our gateway url `https://stevedsimkins.mypinata.cloud/ipfs/`, then we have our CID `QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ`, then our dynamic image id `${id}.jpg` and finally our image optimization `?img-width=1080&img-height=1080`. Of course not all PFP projects are this simple, but with this formatting you can pass in multiple parameters with objects to adjust to your needs. |
|
| 115 | 116 | ||
| 116 | 117 | Our image folder only has 8 images, therefore we just need a simple for loop to generate an array that will hold the numbers 1 through 8. That way we can access it later to generate our image components. Just start with an empty array, then push the numbers into it with the for loop. |
|
| 117 | 118 | ||
| 118 | 119 | ```javascript |
|
| 119 | - | let imageIds = [] |
|
| 120 | - | for (let id = 1; id <= 8; id++){ |
|
| 121 | - | imageIds.push(id) |
|
| 120 | + | let imageIds = []; |
|
| 121 | + | for (let id = 1; id <= 8; id++) { |
|
| 122 | + | imageIds.push(id); |
|
| 122 | 123 | } |
|
| 123 | 124 | ``` |
|
| 124 | 125 | ||
| 125 | 126 | Now the fun part: generating the images! We’ll take our imageId array and map over it. |
|
| 126 | 127 | ||
| 127 | 128 | ```javascript |
|
| 128 | - | {imageIds.map((id) => { |
|
| 129 | - | ||
| 130 | - | })} |
|
| 129 | + | { |
|
| 130 | + | imageIds.map((id) => {}); |
|
| 131 | + | } |
|
| 131 | 132 | ``` |
|
| 132 | 133 | ||
| 133 | 134 | Then we’ll declare our base URL with our dynamic image ID, as well as a name for the alt text later. |
|
| 134 | 135 | ||
| 135 | 136 | ```javascript |
|
| 136 | - | {imageIds.map((id) => { |
|
| 137 | - | let url = `https://stevedsimkins.mypinata.cloud/ipfs/QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ/${id}.jpg?img-width=1080&img-height=1080` |
|
| 138 | - | let name = `nft ${id}` |
|
| 139 | - | })} |
|
| 137 | + | { |
|
| 138 | + | imageIds.map((id) => { |
|
| 139 | + | let url = `https://stevedsimkins.mypinata.cloud/ipfs/QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ/${id}.jpg?img-width=1080&img-height=1080`; |
|
| 140 | + | let name = `nft ${id}`; |
|
| 141 | + | }); |
|
| 142 | + | } |
|
| 140 | 143 | ``` |
|
| 141 | 144 | ||
| 142 | 145 | Finally, we just need to create a component to hold the image, using the url as the image src and the name as the image alt! |
|
| 143 | 146 | ||
| 144 | 147 | ```javascript |
|
| 145 | - | {imageIds.map((id) => { |
|
| 146 | - | let url = `https://stevedsimkins.mypinata.cloud/ipfs/QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ/${id}.jpg?img-width=1080&img-height=1080` |
|
| 147 | - | let name = `nft ${id}` |
|
| 148 | - | return ( |
|
| 149 | - | <div className="image-container"> |
|
| 150 | - | <img src={url} alt={name}/> |
|
| 151 | - | </div> |
|
| 152 | - | ) |
|
| 153 | - | })} |
|
| 148 | + | { |
|
| 149 | + | imageIds.map((id) => { |
|
| 150 | + | let url = `https://stevedsimkins.mypinata.cloud/ipfs/QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ/${id}.jpg?img-width=1080&img-height=1080`; |
|
| 151 | + | let name = `nft ${id}`; |
|
| 152 | + | return ( |
|
| 153 | + | <div className="image-container"> |
|
| 154 | + | <img src={url} alt={name} /> |
|
| 155 | + | </div> |
|
| 156 | + | ); |
|
| 157 | + | }); |
|
| 158 | + | } |
|
| 154 | 159 | ``` |
|
| 155 | 160 | ||
| 156 | 161 | That leaves us with the final code for App.js. Add in a little CSS and we end up with a nice little image grid that loads FAST! |
|
| 157 | 162 | ||
| 158 | 163 | ```javascript |
|
| 159 | - | import './App.css'; |
|
| 164 | + | import "./App.css"; |
|
| 160 | 165 | ||
| 161 | 166 | function App() { |
|
| 167 | + | let imageIds = []; |
|
| 162 | 168 | ||
| 163 | - | let imageIds = [] |
|
| 169 | + | for (let id = 1; id <= 8; id++) { |
|
| 170 | + | imageIds.push(id); |
|
| 171 | + | } |
|
| 164 | 172 | ||
| 165 | - | for (let id = 1; id <= 8; id++){ |
|
| 166 | - | imageIds.push(id) |
|
| 167 | - | } |
|
| 173 | + | return ( |
|
| 174 | + | <div className="App"> |
|
| 175 | + | <div className="grid"> |
|
| 176 | + | {imageIds.map((id) => { |
|
| 177 | + | let url = `https://stevedsimkins.mypinata.cloud/ipfs/QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ/${id}.jpg?img-width=1080&img-height=1080`; |
|
| 178 | + | let name = `nft ${id}`; |
|
| 168 | 179 | ||
| 169 | - | return ( |
|
| 170 | - | <div className="App"> |
|
| 171 | - | <div className="grid"> |
|
| 172 | - | {imageIds.map((id) => { |
|
| 173 | - | let url = `https://stevedsimkins.mypinata.cloud/ipfs/QmTwDNr6LyRzW8H3XorFDArfKEH3GRV1SkF6bAEBF3P4GJ/${id}.jpg?img-width=1080&img-height=1080` |
|
| 174 | - | let name = `nft ${id}` |
|
| 175 | - | ||
| 176 | - | return ( |
|
| 177 | - | <div className="image-container"> |
|
| 178 | - | <img src={url} alt={name}/> |
|
| 179 | - | </div> |
|
| 180 | - | ) |
|
| 181 | - | })} |
|
| 182 | - | </div> |
|
| 183 | - | </div> |
|
| 184 | - | ); |
|
| 180 | + | return ( |
|
| 181 | + | <div className="image-container"> |
|
| 182 | + | <img src={url} alt={name} /> |
|
| 183 | + | </div> |
|
| 184 | + | ); |
|
| 185 | + | })} |
|
| 186 | + | </div> |
|
| 187 | + | </div> |
|
| 188 | + | ); |
|
| 185 | 189 | } |
|
| 186 | 190 | ||
| 187 | 191 | export default App; |
|
| 6 | 6 | ogImage: "https://global-uploads.webflow.com/629e4fe96456f8219203e7f1/6410b46677b05b001afa5ff4_2022-02-10_The-Power-of_blog-img-tiny.png" |
|
| 7 | 7 | --- |
|
| 8 | 8 | ||
| 9 | - | import pinnie from "../../assets/pinnie.png" |
|
| 10 | - | import OutLinkButton from "../../components/OutLinkButton.astro" |
|
| 9 | + | import pinnie from "../../assets/pinnie.png"; |
|
| 10 | + | import OutLinkButton from "../../components/OutLinkButton.astro"; |
|
| 11 | 11 | ||
| 12 | - | <OutLinkButton link="https://www.pinata.cloud/blog/the-power-of-dedicated-gateways" site="Pinata" image={pinnie} /> |
|
| 13 | - | ||
| 12 | + | <OutLinkButton |
|
| 13 | + | link="https://www.pinata.cloud/blog/the-power-of-dedicated-gateways" |
|
| 14 | + | site="Pinata" |
|
| 15 | + | image={pinnie} |
|
| 16 | + | /> |
|
| 14 | 17 | ||
| 15 | 18 | ## What are IPFS Gateways? |
|
| 16 | 19 | ||
| 17 | 20 | f you're in the business of creating NFTs then you are probably familiar with the InterPlanetary File System also known as IPFS. It's an incredibly powerful protocol that allows creators to host content too large for blockchains on a decentralized peer-to-peer network, leveraging cryptography to ensure content is immutable. This is ideal for NFT projects that want to decentralize their NFT media and make sure it does not change over time. IPFS also allows creators to take ownership of their content and how they share it. What most people don't understand is how IPFS and HTTP communicate to each other. |
|
| 18 | 21 | ||
| 19 | - | For instance, I have a cool html page that uses 3D libraries and creates a spinning cube. I pinned it to IPFS using Pinata which gave me a content identifier: ```QmTz8mgtvkf8fG8es5i6vr4LX7dd9vnk1XVtB6ScVuCepr```. A content identifier, or CID for short, is how we can reference content on IPFS. The direct link to that file via IPFS is |
|
| 22 | + | For instance, I have a cool html page that uses 3D libraries and creates a spinning cube. I pinned it to IPFS using Pinata which gave me a content identifier: `QmTz8mgtvkf8fG8es5i6vr4LX7dd9vnk1XVtB6ScVuCepr`. A content identifier, or CID for short, is how we can reference content on IPFS. The direct link to that file via IPFS is |
|
| 23 | + | ||
| 20 | 24 | ``` |
|
| 21 | 25 | ipfs://QmTz8mgtvkf8fG8es5i6vr4LX7dd9vnk1XVtB6ScVuCepr |
|
| 22 | 26 | ``` |
|
| 27 | + | ||
| 23 | 28 | If you paste that link into your browser, chances are you are not going to pull anything up or you'll get a random google search. However, if you have an IPFS node running or you’re using a browser with built-in native IPFS support, then you would actually see something. Why is this? |
|
| 24 | 29 | ||
| 25 | 30 | n order to see content on the IPFS protocol, you have to participate. By running a local IPFS node you can be part of the network, receiving and sending blocks of data that are hosted on the network. In turn you can view and pin your own files, too. |
|
| 36 | 41 | ||
| 37 | 42 | You got the cube right? I took the CID that was already hosted on IPFS and then fed it through a Dedicated Gateway to see the file! |
|
| 38 | 43 | ||
| 39 | - | Notice that I said "Dedicated Gateway." There are two types of gateways, private (dedicated) and public. Public gateways are convenient since they are available to everyone, and even built into Pinata's file manager so you can see your content without an IPFS node. However, it's important to note that IPFS is a public network, which means that it can be viewed or used by anyone. This can become a problem because gateways are still managed on traditional servers. Too much traffic without the right infrastructure could cause a failure, so most public gateways will not serve too much content at once by utilizing rate limits. Public gateways are also not very fast and can take a while to load content. These public gateways can be a good service for testing IPFS content, but not ideal for serving large amounts of data. |
|
| 44 | + | Notice that I said "Dedicated Gateway." There are two types of gateways, private (dedicated) and public. Public gateways are convenient since they are available to everyone, and even built into Pinata's file manager so you can see your content without an IPFS node. However, it's important to note that IPFS is a public network, which means that it can be viewed or used by anyone. This can become a problem because gateways are still managed on traditional servers. Too much traffic without the right infrastructure could cause a failure, so most public gateways will not serve too much content at once by utilizing rate limits. Public gateways are also not very fast and can take a while to load content. These public gateways can be a good service for testing IPFS content, but not ideal for serving large amounts of data. |
|
| 40 | 45 | ||
| 41 | 46 | This is where Dedicated Gateways step in! At Pinata, we want to provide blazing fast delivery of your content, and so our engineering team has developed an infrastructure which allows us to do exactly that. |
|
| 42 | 47 | ||
| 11 | 11 | title: "Blog", |
|
| 12 | 12 | path: "/posts", |
|
| 13 | 13 | }, |
|
| 14 | - | { |
|
| 15 | - | title: "Videos", |
|
| 16 | - | path: "/videos", |
|
| 17 | - | }, |
|
| 14 | + | { |
|
| 15 | + | title: "Videos", |
|
| 16 | + | path: "/videos", |
|
| 17 | + | }, |
|
| 18 | 18 | ]; |
|
| 19 | 19 | ||
| 20 | 20 | // ! Remember to add your own socials |
|
| 21 | 21 | export const SOCIAL_LINKS = { |
|
| 22 | 22 | github: "https://github.com/stevedsimkins", |
|
| 23 | 23 | twitter: "https://twitter.com/stevedsimkins", |
|
| 24 | - | medium: "https://medium.com/@stevedsimkins", |
|
| 25 | - | linkedin: "https://linkedin.com/in/steve-simkins", |
|
| 26 | - | ethereum: "https://rainbow.me/stevedsimkins.eth", |
|
| 24 | + | medium: "https://medium.com/@stevedsimkins", |
|
| 25 | + | linkedin: "https://linkedin.com/in/steve-simkins", |
|
| 26 | + | ethereum: "https://rainbow.me/stevedsimkins.eth", |
|
| 27 | 27 | }; |
| 18 | 18 | <html lang={siteConfig.lang}> |
|
| 19 | 19 | <head> |
|
| 20 | 20 | <!-- Google tag (gtag.js) --> |
|
| 21 | - | <script type="text/partytown"src="https://www.googletagmanager.com/gtag/js?id=G-QT67QEFLTG"></script> |
|
| 22 | - | <script type="text/partytown"> |
|
| 21 | + | <script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=G-QT67QEFLTG" |
|
| 22 | + | ></script> |
|
| 23 | + | <script type="text/partytown"> |
|
| 23 | 24 | window.dataLayer = window.dataLayer || []; |
|
| 24 | 25 | function gtag() { |
|
| 25 | 26 | dataLayer.push(arguments); |
| 62 | 62 | <article class="flex-grow break-words"> |
|
| 63 | 63 | <div id="blog-hero"><BlogHero content={post} /></div> |
|
| 64 | 64 | <div |
|
| 65 | - | class="prose prose-sm prose-cactus mt-12 prose-headings:font-semibold prose-headings:before:absolute prose-headings:before:-ml-4 prose-headings:before:text-accent prose-headings:before:content-['#'] prose-th:before:content-none" |
|
| 65 | + | class="prose prose-sm prose-cactus mt-12 prose-headings:font-semibold prose-headings:before:absolute prose-headings:before:-ml-4 prose-headings:before:text-accent prose-headings:before:content-['#'] prose-th:before:content-none" |
|
| 66 | 66 | > |
|
| 67 | 67 | <slot /> |
|
| 68 | 68 | </div> |
| 13 | 13 | <h1 class="title mb-6">404 | Oops something went wrong</h1> |
|
| 14 | 14 | <p class="mb-8">Please use the navigation to find your way back</p> |
|
| 15 | 15 | <div class="my-4 grid justify-center"> |
|
| 16 | - | <Image |
|
| 17 | - | src={img} |
|
| 18 | - | alt="A cartoon cactus looking at the 'Astro.build' logo" |
|
| 19 | - | loading="eager" |
|
| 20 | - | /> |
|
| 16 | + | <Image src={img} alt="A cartoon cactus looking at the 'Astro.build' logo" loading="eager" /> |
|
| 21 | 17 | </div> |
|
| 22 | 18 | </PageLayout> |
| 21 | 21 | aria-label="Link to Pinata website" |
|
| 22 | 22 | href="https://pinata.cloud">Pinata.</a |
|
| 23 | 23 | > |
|
| 24 | - | I help creators and developers in the Web3 space, and I like to help build solutions |
|
| 25 | - | to their problems. |
|
| 24 | + | I help creators and developers in the Web3 space, and I like to help build solutions to their problems. |
|
| 26 | 25 | </p> |
|
| 27 | 26 | <div class="flex justify-center"> |
|
| 28 | 27 | <Image |
|
| 33 | 32 | </div> |
|
| 34 | 33 | <p>Here's some more info about me:</p> |
|
| 35 | 34 | <ul class="list-inside list-disc"> |
|
| 36 | - | <li>Currently in Chattanooga TN</li> |
|
| 37 | - | <li>Pastimes include enjoying coffee shops, spending time with my wife and two sons, and tinkering with mechanical keyboards</li> |
|
| 38 | - | <li>Taught myself frontend web development which led to being hooked on blockchain technology</li> |
|
| 39 | - | <li>I have a killer waffle recipe handed down through my family</li> |
|
| 35 | + | <li>Currently in Chattanooga TN</li> |
|
| 36 | + | <li> |
|
| 37 | + | Pastimes include enjoying coffee shops, spending time with my wife and two sons, and |
|
| 38 | + | tinkering with mechanical keyboards |
|
| 39 | + | </li> |
|
| 40 | + | <li> |
|
| 41 | + | Taught myself frontend web development which led to being hooked on blockchain technology |
|
| 42 | + | </li> |
|
| 43 | + | <li>I have a killer waffle recipe handed down through my family</li> |
|
| 40 | 44 | </ul> |
|
| 41 | 45 | <p> |
|
| 42 | - | Feel free to |
|
| 46 | + | Feel free to |
|
| 43 | 47 | <a |
|
| 44 | 48 | class="cactus-link inline-block" |
|
| 45 | 49 | href="mailto:hello@stevedsimkins.dev" |
|
| 8 | 8 | const MAX_POSTS = 10; |
|
| 9 | 9 | const allPosts = await getCollection("post"); |
|
| 10 | 10 | const allPostsByDate = sortMDByDate(allPosts).slice(0, MAX_POSTS); |
|
| 11 | - | ||
| 12 | 11 | --- |
|
| 13 | 12 | ||
| 14 | 13 | <PageLayout meta={{ title: "Home" }}> |
|
| 15 | 14 | <section> |
|
| 16 | 15 | <h1 class="title mb-6">Hey there!</h1> |
|
| 17 | 16 | <p class="mb-4"> |
|
| 18 | - | My name is Steve. I'm a developer, technical writer, and creator with a desire to help |
|
| 19 | - | build the future of the web. Stay a while to see what I'm working on! |
|
| 17 | + | My name is Steve. I'm a developer, technical writer, and creator with a desire to help build |
|
| 18 | + | the future of the web. Stay a while to see what I'm working on! |
|
| 20 | 19 | </p> |
|
| 21 | 20 | <SocialList /> |
|
| 22 | - | <p>Or anywhere with my handle <span class="text-accent">@stevedsimkins</span></p> |
|
| 21 | + | <p>Or anywhere with my handle <span class="text-accent">@stevedsimkins</span></p> |
|
| 23 | 22 | </section> |
|
| 24 | 23 | <section aria-label="Blog post list" class="mt-16"> |
|
| 25 | 24 | <h2 class="title mb-4 text-xl">Posts</h2> |
|
| 38 | 37 | <ul class="space-y-4 sm:space-y-2"> |
|
| 39 | 38 | <li> |
|
| 40 | 39 | <a |
|
| 41 | - | href="https://stevedylanphoto.com" |
|
| 40 | + | href="https://stevedylanphoto.com" |
|
| 42 | 41 | target="_blank" |
|
| 43 | 42 | rel="noopener noreferrer" |
|
| 44 | 43 | class="cactus-link inline-block" |
|
| 48 | 47 | </li> |
|
| 49 | 48 | <li> |
|
| 50 | 49 | <a |
|
| 51 | - | href="https://pinata.cloud" |
|
| 50 | + | href="https://pinata.cloud" |
|
| 52 | 51 | target="_blank" |
|
| 53 | 52 | rel="noopener noreferrer" |
|
| 54 | 53 | class="cactus-link inline-block" |
|
| 58 | 57 | </li> |
|
| 59 | 58 | <li> |
|
| 60 | 59 | <a |
|
| 61 | - | href="https://medium.com/@stevedsimkins" |
|
| 60 | + | href="https://medium.com/@stevedsimkins" |
|
| 62 | 61 | target="_blank" |
|
| 63 | 62 | rel="noopener noreferrer" |
|
| 64 | 63 | class="cactus-link inline-block" |
|
| 65 | 64 | >Medium |
|
| 66 | 65 | </a>: |
|
| 67 | - | <p class="inline-block sm:mt-2"> |
|
| 68 | - | Technical blog posts I've written for Pinata |
|
| 69 | - | </p> |
|
| 66 | + | <p class="inline-block sm:mt-2">Technical blog posts I've written for Pinata</p> |
|
| 70 | 67 | </li> |
|
| 71 | 68 | </ul> |
|
| 72 | 69 | </section> |
|
| 7 | 7 | import { getFormattedDate } from "@/utils"; |
|
| 8 | 8 | ||
| 9 | 9 | const monoFontReg = await fetch( |
|
| 10 | - | "https://api.fontsource.org/v1/fonts/roboto-mono/latin-400-normal.ttf" |
|
| 10 | + | "https://api.fontsource.org/v1/fonts/roboto-mono/latin-400-normal.ttf" |
|
| 11 | 11 | ); |
|
| 12 | 12 | ||
| 13 | 13 | const monoFontBold = await fetch( |
|
| 14 | - | "https://api.fontsource.org/v1/fonts/roboto-mono/latin-700-normal.ttf" |
|
| 14 | + | "https://api.fontsource.org/v1/fonts/roboto-mono/latin-700-normal.ttf" |
|
| 15 | 15 | ); |
|
| 16 | 16 | ||
| 17 | 17 | const ogOptions: SatoriOptions = { |
|
| 18 | - | width: 1200, |
|
| 19 | - | height: 630, |
|
| 20 | - | // debug: true, |
|
| 21 | - | embedFont: true, |
|
| 22 | - | fonts: [ |
|
| 23 | - | { |
|
| 24 | - | name: "Roboto Mono", |
|
| 25 | - | data: await monoFontReg.arrayBuffer(), |
|
| 26 | - | weight: 400, |
|
| 27 | - | style: "normal", |
|
| 28 | - | }, |
|
| 29 | - | { |
|
| 30 | - | name: "Roboto Mono", |
|
| 31 | - | data: await monoFontBold.arrayBuffer(), |
|
| 32 | - | weight: 700, |
|
| 33 | - | style: "normal", |
|
| 34 | - | }, |
|
| 35 | - | ], |
|
| 18 | + | width: 1200, |
|
| 19 | + | height: 630, |
|
| 20 | + | // debug: true, |
|
| 21 | + | embedFont: true, |
|
| 22 | + | fonts: [ |
|
| 23 | + | { |
|
| 24 | + | name: "Roboto Mono", |
|
| 25 | + | data: await monoFontReg.arrayBuffer(), |
|
| 26 | + | weight: 400, |
|
| 27 | + | style: "normal", |
|
| 28 | + | }, |
|
| 29 | + | { |
|
| 30 | + | name: "Roboto Mono", |
|
| 31 | + | data: await monoFontBold.arrayBuffer(), |
|
| 32 | + | weight: 700, |
|
| 33 | + | style: "normal", |
|
| 34 | + | }, |
|
| 35 | + | ], |
|
| 36 | 36 | }; |
|
| 37 | 37 | ||
| 38 | 38 | const markup = (title: string, pubDate: string, description: string) => html`<div |
|
| 46 | 46 | <div tw="flex items-center justify-between w-full p-10 border-t border-[#a6e3a1] text-xl"> |
|
| 47 | 47 | <div tw="flex items-center"> |
|
| 48 | 48 | <svg height="60" fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"> |
|
| 49 | - | <path d="M20.1465 448.094H479L350.926 226.37L311.281 258.273L275.108 232.751L249.573 258.273L215.528 232.751L193.185 258.273L151.291 221.053L20.1465 448.094Z" fill="#74c7ec"/> |
|
| 50 | - | <path d="M249.573 50.9053L151.291 221.053L193.185 258.273L215.528 232.751L249.573 258.273L275.108 232.751L311.281 258.273L350.926 226.37L249.573 50.9053Z" fill="#EDEFF3"/> |
|
| 51 | - | <path d="M151.291 221.053L20.1465 448.094H479L350.926 226.37M151.291 221.053L249.573 50.9053L350.926 226.37M151.291 221.053L193.185 258.273L215.528 232.751L249.573 258.273L275.108 232.751L311.281 258.273L350.926 226.37" stroke="black" stroke-width="7"/> |
|
| 52 | - | <line x1="265.341" y1="167.541" x2="294.587" y2="218.169" stroke="black" stroke-width="5"/> |
|
| 49 | + | <path |
|
| 50 | + | d="M20.1465 448.094H479L350.926 226.37L311.281 258.273L275.108 232.751L249.573 258.273L215.528 232.751L193.185 258.273L151.291 221.053L20.1465 448.094Z" |
|
| 51 | + | fill="#74c7ec" |
|
| 52 | + | /> |
|
| 53 | + | <path |
|
| 54 | + | d="M249.573 50.9053L151.291 221.053L193.185 258.273L215.528 232.751L249.573 258.273L275.108 232.751L311.281 258.273L350.926 226.37L249.573 50.9053Z" |
|
| 55 | + | fill="#EDEFF3" |
|
| 56 | + | /> |
|
| 57 | + | <path |
|
| 58 | + | d="M151.291 221.053L20.1465 448.094H479L350.926 226.37M151.291 221.053L249.573 50.9053L350.926 226.37M151.291 221.053L193.185 258.273L215.528 232.751L249.573 258.273L275.108 232.751L311.281 258.273L350.926 226.37" |
|
| 59 | + | stroke="black" |
|
| 60 | + | stroke-width="7" |
|
| 61 | + | /> |
|
| 62 | + | <line x1="265.341" y1="167.541" x2="294.587" y2="218.169" stroke="black" stroke-width="5" /> |
|
| 53 | 63 | </svg> |
|
| 54 | 64 | <p tw="ml-3 font-semibold text-3xl">${siteConfig.title}</p> |
|
| 55 | 65 | </div> |
|
| 57 | 67 | </div>`; |
|
| 58 | 68 | ||
| 59 | 69 | export async function get({ params: { slug } }: APIContext) { |
|
| 60 | - | const post = await getEntryBySlug("post", slug!); |
|
| 61 | - | const title = post?.data.title ?? siteConfig.title; |
|
| 62 | - | const postDate = getFormattedDate(post?.data.publishDate ?? Date.now(), { |
|
| 63 | - | weekday: "long", |
|
| 64 | - | }); |
|
| 65 | - | const description = post?.data.description ?? siteConfig.title; |
|
| 66 | - | const svg = await satori(markup(title, postDate, description), ogOptions); |
|
| 67 | - | const png = new Resvg(svg).render().asPng(); |
|
| 68 | - | return { |
|
| 69 | - | body: png, |
|
| 70 | - | encoding: "binary", |
|
| 71 | - | }; |
|
| 70 | + | const post = await getEntryBySlug("post", slug!); |
|
| 71 | + | const title = post?.data.title ?? siteConfig.title; |
|
| 72 | + | const postDate = getFormattedDate(post?.data.publishDate ?? Date.now(), { |
|
| 73 | + | weekday: "long", |
|
| 74 | + | }); |
|
| 75 | + | const description = post?.data.description ?? siteConfig.title; |
|
| 76 | + | const svg = await satori(markup(title, postDate, description), ogOptions); |
|
| 77 | + | const png = new Resvg(svg).render().asPng(); |
|
| 78 | + | return { |
|
| 79 | + | body: png, |
|
| 80 | + | encoding: "binary", |
|
| 81 | + | }; |
|
| 72 | 82 | } |
|
| 73 | 83 | ||
| 74 | 84 | export async function getStaticPaths(): Promise<GetStaticPathsResult> { |
|
| 75 | - | const posts = await getCollection("post"); |
|
| 76 | - | return posts.filter(({ data }) => !data.ogImage).map(({ slug }) => ({ params: { slug } })); |
|
| 85 | + | const posts = await getCollection("post"); |
|
| 86 | + | return posts.filter(({ data }) => !data.ogImage).map(({ slug }) => ({ params: { slug } })); |
|
| 77 | 87 | } |
|
| 10 | 10 | <PageLayout meta={meta}> |
|
| 11 | 11 | <div class="space-y-6"> |
|
| 12 | 12 | <h1 class="title">Videos</h1> |
|
| 13 | - | <p>Here are some samples of video content I've produced to help users!</p> |
|
| 14 | - | <iframe class="w-full md:h-96 h-full" src="https://www.youtube.com/embed/YQMktd0llOo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe> |
|
| 13 | + | <p>Here are some samples of video content I've produced to help users!</p> |
|
| 14 | + | <iframe |
|
| 15 | + | class="h-full w-full md:h-96" |
|
| 16 | + | src="https://www.youtube.com/embed/YQMktd0llOo" |
|
| 17 | + | title="YouTube video player" |
|
| 18 | + | frameborder="0" |
|
| 19 | + | allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" |
|
| 20 | + | allowfullscreen></iframe> |
|
| 15 | 21 | </div> |
|
| 16 | 22 | </PageLayout> |