format code
748655e0
16 file(s) · +351 −338
| 2 | 2 | import type { SiteMeta } from "@/data/siteMeta"; |
|
| 3 | 3 | import siteConfig from "@/site-config"; |
|
| 4 | 4 | import "../styles/global.css"; |
|
| 5 | - | import { ViewTransitions } from 'astro:transitions'; |
|
| 5 | + | import { ViewTransitions } from "astro:transitions"; |
|
| 6 | 6 | ||
| 7 | 7 | type Props = SiteMeta; |
|
| 8 | 8 |
| 1 | - | <a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-1.5 focus:left-1" |
|
| 1 | + | <a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:left-1 focus:top-1.5" |
|
| 2 | 2 | >skip to content |
|
| 3 | 3 | </a> |
| 36 | 36 | > |
|
| 37 | 37 | <svg |
|
| 38 | 38 | id="sun-svg" |
|
| 39 | - | class="absolute top-1/2 left-1/2 h-7 w-7 -translate-x-1/2 -translate-y-1/2 scale-0 opacity-0 transition-all group-aria-pressed:scale-100 group-aria-pressed:opacity-100" |
|
| 39 | + | class="absolute left-1/2 top-1/2 h-7 w-7 -translate-x-1/2 -translate-y-1/2 scale-0 opacity-0 transition-all group-aria-pressed:scale-100 group-aria-pressed:opacity-100" |
|
| 40 | 40 | aria-hidden="true" |
|
| 41 | 41 | focusable="false" |
|
| 42 | 42 | stroke-width="1.5" |
|
| 67 | 67 | </svg> |
|
| 68 | 68 | <svg |
|
| 69 | 69 | id="moon-svg" |
|
| 70 | - | class="absolute top-1/2 left-1/2 h-7 w-7 -translate-x-1/2 -translate-y-1/2 scale-0 opacity-0 transition-all group-aria-[pressed=false]:scale-100 group-aria-[pressed=false]:opacity-100" |
|
| 70 | + | class="absolute left-1/2 top-1/2 h-7 w-7 -translate-x-1/2 -translate-y-1/2 scale-0 opacity-0 transition-all group-aria-[pressed=false]:scale-100 group-aria-[pressed=false]:opacity-100" |
|
| 71 | 71 | aria-hidden="true" |
|
| 72 | 72 | focusable="false" |
|
| 73 | 73 | stroke-width="1.5" |
|
| 8 | 8 | ||
| 9 | 9 | import { Image } from "astro:assets"; |
|
| 10 | 10 | ||
| 11 | - | ||
| 12 | 11 | IPFS has proven to be the decentralized storage protocol of choice by many blockchain developers, and one of the crucial tools used to access content on IPFS are [Gateways](https://www.pinata.cloud/blog/what-is-an-ipfs-gateway). IPFS Gateways are like bridges between the IPFS protocol and the HTTP protocol that we use everyday to browse websites. There are lots of different options to choose from when it comes to IPFS Gateways, and in this post we’ll show you how to host and build your own! |
|
| 13 | 12 | ||
| 14 | 13 | <aside> |
|
| 18 | 17 | ||
| 19 | 18 | ## Requirements |
|
| 20 | 19 | ||
| 21 | - | In order to follow this guide you’ll need a few things. First and foremost you’ll need a decent amount of experience using Linux servers and navigating around in the terminal, things like creating daemons or editing text files with vi or nano. You’ll also need a cloud server provider, and there’s plenty to choose from. In this guide we’ll use Digital Ocean and get a simple droplet. Also if you want to have a custom domain instead of using an IP address you can get something through a domain provider like Namecheap. |
|
| 20 | + | In order to follow this guide you’ll need a few things. First and foremost you’ll need a decent amount of experience using Linux servers and navigating around in the terminal, things like creating daemons or editing text files with vi or nano. You’ll also need a cloud server provider, and there’s plenty to choose from. In this guide we’ll use Digital Ocean and get a simple droplet. Also if you want to have a custom domain instead of using an IP address you can get something through a domain provider like Namecheap. |
|
| 22 | 21 | ||
| 23 | 22 | ## Setting Up the Server |
|
| 24 | 23 | ||
| 33 | 32 | alt="digital ocean droplet creation" |
|
| 34 | 33 | width={1920} |
|
| 35 | 34 | height={1080} |
|
| 36 | - | aspectRatio={ 2/1 } |
|
| 35 | + | aspectRatio={2 / 1} |
|
| 37 | 36 | /> |
|
| 38 | 37 | ||
| 39 | 38 | For the authorization select SSH Keys, then copy and paste the contents of `~/.ssh/rsa.pub` and paste it in as a new key. |
|
| 43 | 42 | alt="digital ocean ssh key creation" |
|
| 44 | 43 | width={1920} |
|
| 45 | 44 | height={1080} |
|
| 46 | - | aspectRatio={ 3/1 } |
|
| 45 | + | aspectRatio={3 / 1} |
|
| 47 | 46 | /> |
|
| 48 | 47 | ||
| 49 | 48 | After the droplet has been created, you will actually want to turn it off, go to the Network settings, and enable IPV6. Once enabled turn it back on and try to SSH into it with the following command with the IPV4 address of the server: |
|
| 120 | 119 | ||
| 121 | 120 | Now that your IPFS node is setup and we can use the gateway, these next steps will help you assign a domain to the gateway and make it public. First you will need to acquire a domain name which you can get from multiple providers like Namecheap. For this tutorial we’ll use the example `[domain.com](http://domain.com)` (very original). After purchasing the domain you will want to go into the advance DNS settings through your domain provider, and there we’ll add some records so we can use `[ipfs.domain.com](http://ipfs.domain.com)` as our gateway domain. You can get the IPV4 and IPV6 addresses from your Digital Ocean console. |
|
| 122 | 121 | ||
| 123 | - | | Type | Host | Value | TTL | |
|
| 124 | - | | --- | --- | --- | --- | |
|
| 125 | - | | A | ipfs | IPV4 Address | Automatic | |
|
| 126 | - | | A | *.ipfs | IPV4 Address | Automatic | |
|
| 127 | - | | AAAA | ipfs | IPV6 Address | Automatic | |
|
| 128 | - | | AAAA | *.ipfs | IPV6 Address | Automatic | |
|
| 122 | + | | Type | Host | Value | TTL | |
|
| 123 | + | | ---- | ------- | ------------ | --------- | |
|
| 124 | + | | A | ipfs | IPV4 Address | Automatic | |
|
| 125 | + | | A | \*.ipfs | IPV4 Address | Automatic | |
|
| 126 | + | | AAAA | ipfs | IPV6 Address | Automatic | |
|
| 127 | + | | AAAA | \*.ipfs | IPV6 Address | Automatic | |
|
| 129 | 128 | ||
| 130 | 129 | You can use a DNS checker for `[ipfs.domain.com](http://ipfs.domain.com)` to make sure everything is propagating but it can take some time depending on your provider. |
|
| 131 | 130 | ||
| 176 | 175 | ||
| 177 | 176 | As you saw in the last url we used, we’re still using http which is a no go in today’s standards. In order to fix that we need to get an SSL certificate for our domain. Thankfully its pretty straight forward with a package called certbot. You can install it with `sudo nnap install certbot --classic` then run the command `sudo certbot --nginx -d [ipfs.domain.com](http://ipfs.domain.com)`. It should walk you though some questions you can answer, then it should issue a certificate for your domain. Last step is to go back to your domain provider and add this DNS record: |
|
| 178 | 177 | ||
| 179 | - | | Type | Host | Value | |
|
| 180 | - | | --- | --- | --- | |
|
| 181 | - | | CAA | ipfs | http://letsencrypt.org/ | |
|
| 178 | + | | Type | Host | Value | |
|
| 179 | + | | ---- | ---- | ----------------------- | |
|
| 180 | + | | CAA | ipfs | http://letsencrypt.org/ | |
|
| 182 | 181 | ||
| 183 | 182 | Now you can test it out with `[https://ipfs.domain.com/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng](https://ipfs.domain.com/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng)`. Congrats!! You just setup your own public IPFS gateway. But, something isn’t quite right: its super slow isn’t it? Let’s talk about that. |
|
| 184 | 183 | ||
| 185 | 184 | ## Further Steps |
|
| 186 | 185 | ||
| 187 | - | You have your public gateway setup and it sorta works, but its also super slow. There are some things you can do to help relieve this. One of those things is setting up a cache layer or CDN to help make fetching files a second time much faster. You can also look into peering your gateway with IPFS pinning services to tap into their network and get faster speeds, or configure your IPFS node to work with the Distributed Hash Table (DHT) to assist with finding files. Even with all those things, it can be tough to maintain good speeds. |
|
| 186 | + | You have your public gateway setup and it sorta works, but its also super slow. There are some things you can do to help relieve this. One of those things is setting up a cache layer or CDN to help make fetching files a second time much faster. You can also look into peering your gateway with IPFS pinning services to tap into their network and get faster speeds, or configure your IPFS node to work with the Distributed Hash Table (DHT) to assist with finding files. Even with all those things, it can be tough to maintain good speeds. |
|
| 188 | 187 | ||
| 189 | 188 | Another thing you have to consider when hosting an IPFS gateway yourself is abuse. The unfortunately piece of a decentralized network is that there is plenty of people out there who want to abuse public gateways by hammering them with requests for files or use your gateway for phishing content. When that happens you have to keep up with a list of CIDs to block from your gateway or risk it have it taken down by domain registrars. You can check out a list of CIDs to block by IPFS [here](https://github.com/ipfs/infra/blob/master/ipfs/gateway/denylist.conf), however it is no longer being maintained making it even more difficult to keep up yourself. |
|
| 190 | 189 | ||
| 6 | 6 | ogImage: "https://assets-global.website-files.com/629e4fe96456f8219203e7f1/6545bfa112815d6340466066_20231103_How%20to%20Encrypt%20and%20Decrypt%20Files%20on%20IPFS%20Using%20Lit%20Protocol%20and%20Pinata.jpeg" |
|
| 7 | 7 | --- |
|
| 8 | 8 | ||
| 9 | - | ||
| 10 | 9 | The most popular method used for sharing files off-chain in Web3 is IPFS, and there are some [good reasons for that](https://www.pinata.cloud/blog/why-ipfs-is-the-storage-solution-for-web3-developers). However it does not come without its own share of problems, and one of those is the ability to share private files. IPFS is a public network so anyone with a CID can access and download that content, and this hinders projects that may want to token gate content or create subscriptions to content. With that said, encryption has proven to be one solution to this problem. Remarkably, the solution of [asymmetric encryption](https://www.okta.com/identity-101/asymmetric-encryption) is used in blockchain all the time and can be reused for the purpose of token gating. [Lit Protocol](https://litprotocol.com/) is a decentralized middleware client that enables access controls to help extend asymmetric encryption to token gating based on crypto ownership, such as owning an NFT, ERC-20 token balance, or simply designating a recipient address. In this post, we’ll show you how you can combine the best of both worlds and create an app that will encrypt content, upload it to IPFS, and then given an encrypted CID, decrypt it. |
|
| 11 | 10 | ||
| 12 | 11 | ## Why IPFS? |
|
| 84 | 83 | ||
| 85 | 84 | ```jsx |
|
| 86 | 85 | const uploadFile = async (fileToUpload) => { |
|
| 87 | - | try { |
|
| 88 | - | setUploading(true); |
|
| 89 | - | const formData = new FormData(); |
|
| 90 | - | formData.append("file", fileToUpload, fileToUpload.name) |
|
| 91 | - | const res = await fetch("/api/files", { |
|
| 92 | - | method: "POST", |
|
| 93 | - | body: formData, |
|
| 94 | - | }); |
|
| 95 | - | const ipfsHash = await res.text(); |
|
| 96 | - | setCid(ipfsHash); |
|
| 97 | - | setUploading(false); |
|
| 98 | - | } catch (e) { |
|
| 99 | - | console.log(e); |
|
| 100 | - | setUploading(false); |
|
| 101 | - | alert("Trouble uploading file"); |
|
| 102 | - | } |
|
| 103 | - | }; |
|
| 86 | + | try { |
|
| 87 | + | setUploading(true); |
|
| 88 | + | const formData = new FormData(); |
|
| 89 | + | formData.append("file", fileToUpload, fileToUpload.name); |
|
| 90 | + | const res = await fetch("/api/files", { |
|
| 91 | + | method: "POST", |
|
| 92 | + | body: formData, |
|
| 93 | + | }); |
|
| 94 | + | const ipfsHash = await res.text(); |
|
| 95 | + | setCid(ipfsHash); |
|
| 96 | + | setUploading(false); |
|
| 97 | + | } catch (e) { |
|
| 98 | + | console.log(e); |
|
| 99 | + | setUploading(false); |
|
| 100 | + | alert("Trouble uploading file"); |
|
| 101 | + | } |
|
| 102 | + | }; |
|
| 104 | 103 | ``` |
|
| 105 | 104 | ||
| 106 | 105 | At the top of our `try` statement but underneath our `setUploading` state, we’ll initialize the `LitNodeClient` using the `ceyenne` network, connect our app to that network, then get the `authSig`. Lit Protocol is a decentralized network middleware that helps us do some cool token gating and lets us do encryption. In these few statements, we create a client that connects to that middleware network and then gets a signature from the user. This signature will be used for signing the encrypted files. |
|
| 197 | 196 | ||
| 198 | 197 | ```jsx |
|
| 199 | 198 | // Then we turn it into a file that will be accepted by the API endpoint |
|
| 200 | - | const encryptedBlob = new Blob([encryptedZip], { type: 'text/plain' }) |
|
| 201 | - | const encryptedFile = new File([encryptedBlob], fileToUpload.name) |
|
| 199 | + | const encryptedBlob = new Blob([encryptedZip], { type: "text/plain" }); |
|
| 200 | + | const encryptedFile = new File([encryptedBlob], fileToUpload.name); |
|
| 202 | 201 | ``` |
|
| 203 | 202 | ||
| 204 | 203 | All together we should have an upload function that looks like this: |
|
| 205 | 204 | ||
| 206 | 205 | ```jsx |
|
| 207 | 206 | const uploadFile = async (fileToUpload) => { |
|
| 208 | - | try { |
|
| 209 | - | setUploading(true); |
|
| 210 | - | // Create our litNodeClient |
|
| 211 | - | const litNodeClient = new LitJsSdk.LitNodeClient({ |
|
| 212 | - | litNetwork: 'cayenne', |
|
| 213 | - | }); |
|
| 214 | - | // Then get the authSig |
|
| 215 | - | await litNodeClient.connect(); |
|
| 216 | - | const authSig = await LitJsSdk.checkAndSignAuthMessage({ |
|
| 217 | - | chain: 'ethereum' |
|
| 218 | - | }); |
|
| 219 | - | // Define our access controls, this is set to be anyone |
|
| 220 | - | const accs = [ |
|
| 221 | - | { |
|
| 222 | - | contractAddress: '', |
|
| 223 | - | standardContractType: '', |
|
| 224 | - | chain: 'ethereum', |
|
| 225 | - | method: 'eth_getBalance', |
|
| 226 | - | parameters: [':userAddress', 'latest'], |
|
| 227 | - | returnValueTest: { |
|
| 228 | - | comparator: '>=', |
|
| 229 | - | value: '0', |
|
| 230 | - | }, |
|
| 231 | - | }, |
|
| 232 | - | ]; |
|
| 233 | - | // Then we use our access controls and authSig to encrypt the file and zip it up with the metadata |
|
| 234 | - | const encryptedZip = await LitJsSdk.encryptFileAndZipWithMetadata({ |
|
| 235 | - | accessControlConditions: accs, |
|
| 236 | - | authSig, |
|
| 237 | - | chain: 'ethereum', |
|
| 238 | - | file: fileToUpload, |
|
| 239 | - | litNodeClient: litNodeClient, |
|
| 240 | - | readme: "Use IPFS CID of this file to decrypt it" |
|
| 241 | - | }); |
|
| 207 | + | try { |
|
| 208 | + | setUploading(true); |
|
| 209 | + | // Create our litNodeClient |
|
| 210 | + | const litNodeClient = new LitJsSdk.LitNodeClient({ |
|
| 211 | + | litNetwork: "cayenne", |
|
| 212 | + | }); |
|
| 213 | + | // Then get the authSig |
|
| 214 | + | await litNodeClient.connect(); |
|
| 215 | + | const authSig = await LitJsSdk.checkAndSignAuthMessage({ |
|
| 216 | + | chain: "ethereum", |
|
| 217 | + | }); |
|
| 218 | + | // Define our access controls, this is set to be anyone |
|
| 219 | + | const accs = [ |
|
| 220 | + | { |
|
| 221 | + | contractAddress: "", |
|
| 222 | + | standardContractType: "", |
|
| 223 | + | chain: "ethereum", |
|
| 224 | + | method: "eth_getBalance", |
|
| 225 | + | parameters: [":userAddress", "latest"], |
|
| 226 | + | returnValueTest: { |
|
| 227 | + | comparator: ">=", |
|
| 228 | + | value: "0", |
|
| 229 | + | }, |
|
| 230 | + | }, |
|
| 231 | + | ]; |
|
| 232 | + | // Then we use our access controls and authSig to encrypt the file and zip it up with the metadata |
|
| 233 | + | const encryptedZip = await LitJsSdk.encryptFileAndZipWithMetadata({ |
|
| 234 | + | accessControlConditions: accs, |
|
| 235 | + | authSig, |
|
| 236 | + | chain: "ethereum", |
|
| 237 | + | file: fileToUpload, |
|
| 238 | + | litNodeClient: litNodeClient, |
|
| 239 | + | readme: "Use IPFS CID of this file to decrypt it", |
|
| 240 | + | }); |
|
| 242 | 241 | ||
| 243 | - | // Then we turn it into a file that will be accepted by the Pinata API |
|
| 244 | - | const encryptedBlob = new Blob([encryptedZip], { type: 'text/plain' }) |
|
| 245 | - | const encryptedFile = new File([encryptedBlob], fileToUpload.name) |
|
| 242 | + | // Then we turn it into a file that will be accepted by the Pinata API |
|
| 243 | + | const encryptedBlob = new Blob([encryptedZip], { type: "text/plain" }); |
|
| 244 | + | const encryptedFile = new File([encryptedBlob], fileToUpload.name); |
|
| 246 | 245 | ||
| 247 | - | // Finally we upload the file by passing it to our /api/files endpoint |
|
| 248 | - | // Keep in mind this works for smaller files and you may need to do a presigned JWT and upload from the client if you're dealing with larger files |
|
| 249 | - | // Read more about that here: https://www.pinata.cloud/blog/how-to-upload-to-ipfs-from-the-frontend-with-signed-jwts |
|
| 250 | - | const formData = new FormData(); |
|
| 251 | - | formData.append("file", encryptedFile, encryptedFile.name) |
|
| 252 | - | const res = await fetch("/api/files", { |
|
| 253 | - | method: "POST", |
|
| 254 | - | body: formData, |
|
| 255 | - | }); |
|
| 256 | - | const ipfsHash = await res.text(); |
|
| 257 | - | setCid(ipfsHash); |
|
| 258 | - | setUploading(false); |
|
| 259 | - | } catch (e) { |
|
| 260 | - | console.log(e); |
|
| 261 | - | setUploading(false); |
|
| 262 | - | alert("Trouble uploading file"); |
|
| 263 | - | } |
|
| 264 | - | }; |
|
| 246 | + | // Finally we upload the file by passing it to our /api/files endpoint |
|
| 247 | + | // Keep in mind this works for smaller files and you may need to do a presigned JWT and upload from the client if you're dealing with larger files |
|
| 248 | + | // Read more about that here: https://www.pinata.cloud/blog/how-to-upload-to-ipfs-from-the-frontend-with-signed-jwts |
|
| 249 | + | const formData = new FormData(); |
|
| 250 | + | formData.append("file", encryptedFile, encryptedFile.name); |
|
| 251 | + | const res = await fetch("/api/files", { |
|
| 252 | + | method: "POST", |
|
| 253 | + | body: formData, |
|
| 254 | + | }); |
|
| 255 | + | const ipfsHash = await res.text(); |
|
| 256 | + | setCid(ipfsHash); |
|
| 257 | + | setUploading(false); |
|
| 258 | + | } catch (e) { |
|
| 259 | + | console.log(e); |
|
| 260 | + | setUploading(false); |
|
| 261 | + | alert("Trouble uploading file"); |
|
| 262 | + | } |
|
| 263 | + | }; |
|
| 265 | 264 | ``` |
|
| 266 | 265 | ||
| 267 | 266 | One thing to note is that there is a file size restriction when using the Next API routes, so if you have larger files you may want to move uploading to the client side and utilize pre-signed JWTs which we talk about in [this post.](https://www.pinata.cloud/blog/how-to-upload-to-ipfs-from-the-frontend-with-signed-jwts) |
|
| 395 | 394 | ||
| 396 | 395 | ```jsx |
|
| 397 | 396 | const accessControlConditions = [ |
|
| 398 | - | { |
|
| 399 | - | contractAddress: '0xA80617371A5f511Bf4c1dDf822E6040acaa63e71', |
|
| 400 | - | standardContractType: 'ERC721', |
|
| 401 | - | chain, |
|
| 402 | - | method: 'balanceOf', |
|
| 403 | - | parameters: [ |
|
| 404 | - | ':userAddress' |
|
| 405 | - | ], |
|
| 406 | - | returnValueTest: { |
|
| 407 | - | comparator: '>', |
|
| 408 | - | value: '0' |
|
| 409 | - | } |
|
| 410 | - | } |
|
| 411 | - | ] |
|
| 397 | + | { |
|
| 398 | + | contractAddress: "0xA80617371A5f511Bf4c1dDf822E6040acaa63e71", |
|
| 399 | + | standardContractType: "ERC721", |
|
| 400 | + | chain, |
|
| 401 | + | method: "balanceOf", |
|
| 402 | + | parameters: [":userAddress"], |
|
| 403 | + | returnValueTest: { |
|
| 404 | + | comparator: ">", |
|
| 405 | + | value: "0", |
|
| 406 | + | }, |
|
| 407 | + | }, |
|
| 408 | + | ]; |
|
| 412 | 409 | ``` |
|
| 413 | 410 | ||
| 414 | - | Or you could do DAO membership (MolochDAOv2.1, also supports DAOHaus)****[](https://developer.litprotocol.com/v3/sdk/access-control/evm/basic-examples#must-be-a-member-of-a-dao-molochdaov21-also-supports-daohaus)**** |
|
| 411 | + | Or you could do DAO membership (MolochDAOv2.1, also supports DAOHaus)\***\*[](https://developer.litprotocol.com/v3/sdk/access-control/evm/basic-examples#must-be-a-member-of-a-dao-molochdaov21-also-supports-daohaus)\*\*** |
|
| 415 | 412 | ||
| 416 | 413 | ```jsx |
|
| 417 | 414 | const accessControlConditions = [ |
|
| 418 | - | { |
|
| 419 | - | contractAddress: '0x50D8EB685a9F262B13F28958aBc9670F06F819d9', |
|
| 420 | - | standardContractType: 'MolochDAOv2.1', |
|
| 421 | - | chain, |
|
| 422 | - | method: 'members', |
|
| 423 | - | parameters: [ |
|
| 424 | - | ':userAddress', |
|
| 425 | - | ], |
|
| 426 | - | returnValueTest: { |
|
| 427 | - | comparator: '=', |
|
| 428 | - | value: 'true' |
|
| 429 | - | } |
|
| 430 | - | } |
|
| 431 | - | ] |
|
| 415 | + | { |
|
| 416 | + | contractAddress: "0x50D8EB685a9F262B13F28958aBc9670F06F819d9", |
|
| 417 | + | standardContractType: "MolochDAOv2.1", |
|
| 418 | + | chain, |
|
| 419 | + | method: "members", |
|
| 420 | + | parameters: [":userAddress"], |
|
| 421 | + | returnValueTest: { |
|
| 422 | + | comparator: "=", |
|
| 423 | + | value: "true", |
|
| 424 | + | }, |
|
| 425 | + | }, |
|
| 426 | + | ]; |
|
| 432 | 427 | ``` |
|
| 433 | 428 | ||
| 434 | 429 | You can even do a simple check if the recipient is a particular wallet address. |
|
| 435 | 430 | ||
| 436 | 431 | ```jsx |
|
| 437 | 432 | const accessControlConditions = [ |
|
| 438 | - | { |
|
| 439 | - | contractAddress: '', |
|
| 440 | - | standardContractType: '', |
|
| 441 | - | chain, |
|
| 442 | - | method: '', |
|
| 443 | - | parameters: [ |
|
| 444 | - | ':userAddress', |
|
| 445 | - | ], |
|
| 446 | - | returnValueTest: { |
|
| 447 | - | comparator: '=', |
|
| 448 | - | value: '0x50e2dac5e78B5905CB09495547452cEE64426db2' |
|
| 449 | - | } |
|
| 450 | - | } |
|
| 451 | - | ] |
|
| 433 | + | { |
|
| 434 | + | contractAddress: "", |
|
| 435 | + | standardContractType: "", |
|
| 436 | + | chain, |
|
| 437 | + | method: "", |
|
| 438 | + | parameters: [":userAddress"], |
|
| 439 | + | returnValueTest: { |
|
| 440 | + | comparator: "=", |
|
| 441 | + | value: "0x50e2dac5e78B5905CB09495547452cEE64426db2", |
|
| 442 | + | }, |
|
| 443 | + | }, |
|
| 444 | + | ]; |
|
| 452 | 445 | ``` |
|
| 453 | 446 | ||
| 454 | 447 | With these building blocks, you could easily build a standalone token gating app, or build a custom solution for your holders. The possibilities are endless! |
|
| 26 | 26 | At the core of my workflow is [Tmux.](https://github.com/tmux/tmux) This tool allows you to create multiple terminal sessions, windows, and panes in just one terminal emulator window. Instead of having a terminal open for every project I might be in, I can just have one. A Tmux session will look like any other terminal window, the difference is the ability to disconnect and then re-attach to that same session. So I can be working on something, leave it, then come right back to it. |
|
| 27 | 27 | ||
| 28 | 28 | <video |
|
| 29 | - | autoPlay |
|
| 30 | - | muted |
|
| 31 | - | loop |
|
| 32 | - | playsinline |
|
| 33 | - | className="w-full aspect-video" |
|
| 34 | - | src="https://dweb.mypinata.cloud/ipfs/Qmeb4797YyF2FhwTdoPuuQi4LXgdXaGoGcTRqU85JAEv9M?filename=video.mp4" |
|
| 29 | + | autoPlay |
|
| 30 | + | muted |
|
| 31 | + | loop |
|
| 32 | + | playsinline |
|
| 33 | + | className="aspect-video w-full" |
|
| 34 | + | src="https://dweb.mypinata.cloud/ipfs/Qmeb4797YyF2FhwTdoPuuQi4LXgdXaGoGcTRqU85JAEv9M?filename=video.mp4" |
|
| 35 | 35 | ></video> |
|
| 36 | 36 | ||
| 37 | 37 | Additionally I can create multiple panes and windows inside a session. I generally create a session per project, and each session might have two windows (e.g. one for a client side repo, the other for a server side repo). This both helps keep projects unified yet organized. |
|
| 38 | 38 | ||
| 39 | 39 | <video |
|
| 40 | - | autoPlay |
|
| 41 | - | muted |
|
| 42 | - | loop |
|
| 43 | - | playsinline |
|
| 44 | - | className="w-full aspect-video" |
|
| 45 | - | src="https://dweb.mypinata.cloud/ipfs/QmWHHCV9YVecdDVcbKXLdfcudW5XBiTt4EfLagP9cowJjC" |
|
| 40 | + | autoPlay |
|
| 41 | + | muted |
|
| 42 | + | loop |
|
| 43 | + | playsinline |
|
| 44 | + | className="aspect-video w-full" |
|
| 45 | + | src="https://dweb.mypinata.cloud/ipfs/QmWHHCV9YVecdDVcbKXLdfcudW5XBiTt4EfLagP9cowJjC" |
|
| 46 | 46 | ></video> |
|
| 47 | 47 | ||
| 48 | 48 | Where it gets really good is having a solid session manager, and that’s where [Josh Medeski’s Sesh](https://github.com/joshmedeski/sesh) comes into play. With this I can easily change between different sessions, and thus easily switch between different projects. This has become essential to my workflow as I often might be working on 2-3 different projects at a time, all with multiple windows and panes each. Each one can be unique to what I’m working on as well. |
|
| 49 | 49 | ||
| 50 | 50 | <video |
|
| 51 | - | autoPlay |
|
| 52 | - | muted |
|
| 53 | - | loop |
|
| 54 | - | playsinline |
|
| 55 | - | className="w-full aspect-video" |
|
| 56 | - | src="https://dweb.mypinata.cloud/ipfs/Qmd3SNkNTms4JQCtv4wmFUKS679A2zsrG7e58szLWS3pmM" |
|
| 51 | + | autoPlay |
|
| 52 | + | muted |
|
| 53 | + | loop |
|
| 54 | + | playsinline |
|
| 55 | + | className="aspect-video w-full" |
|
| 56 | + | src="https://dweb.mypinata.cloud/ipfs/Qmd3SNkNTms4JQCtv4wmFUKS679A2zsrG7e58szLWS3pmM" |
|
| 57 | 57 | ></video> |
|
| 58 | 58 | ||
| 59 | 59 | ## Neovim Plugins |
|
| 63 | 63 | The first is [Telescope](https://github.com/nvim-telescope/telescope.nvim), which is a no brainer for most people already using Neovim. It allows me to quickly jump to different files, search a string through my entire project, or sort through diagnostic issues. It’s incredibly powerful and extensible, and I would highly recommend getting familiar with all its abilities. |
|
| 64 | 64 | ||
| 65 | 65 | <video |
|
| 66 | - | autoPlay |
|
| 67 | - | muted |
|
| 68 | - | loop |
|
| 69 | - | playsinline |
|
| 70 | - | className="w-full aspect-video" |
|
| 71 | - | src="https://dweb.mypinata.cloud/ipfs/QmTRit4eCktJ87NjxdErZ3csXimNq43G8SueQP77mvtvkE" |
|
| 66 | + | autoPlay |
|
| 67 | + | muted |
|
| 68 | + | loop |
|
| 69 | + | playsinline |
|
| 70 | + | className="aspect-video w-full" |
|
| 71 | + | src="https://dweb.mypinata.cloud/ipfs/QmTRit4eCktJ87NjxdErZ3csXimNq43G8SueQP77mvtvkE" |
|
| 72 | 72 | ></video> |
|
| 73 | 73 | ||
| 74 | 74 | Another one I use fairly often is [Neo-tree](https://github.com/nvim-neo-tree/neo-tree.nvim). I know some people are anti-file-tree but I personally really enjoy it. It is setup to appear in the middle of my screen, and I use it to help navigate where a file might be, edit a file name, add new files, etc. Since the majority of my work is in JavaScript / Next.js, it’s helpful to distinguish which `page.tsx` or `route.ts` file I’m currently working on. |
|
| 75 | 75 | ||
| 76 | 76 | <video |
|
| 77 | - | autoPlay |
|
| 78 | - | muted |
|
| 79 | - | loop |
|
| 80 | - | playsinline |
|
| 81 | - | className="w-full aspect-video" |
|
| 82 | - | src="https://dweb.mypinata.cloud/ipfs/QmVSc4amHjygkTcd4Rrx62DaYWMXSYpJD8fBhSinGyChyp" |
|
| 77 | + | autoPlay |
|
| 78 | + | muted |
|
| 79 | + | loop |
|
| 80 | + | playsinline |
|
| 81 | + | className="aspect-video w-full" |
|
| 82 | + | src="https://dweb.mypinata.cloud/ipfs/QmVSc4amHjygkTcd4Rrx62DaYWMXSYpJD8fBhSinGyChyp" |
|
| 83 | 83 | ></video> |
|
| 84 | 84 | ||
| 85 | 85 | Having an LSP (Language Server Protocol) and completions setup is also essential for a good workflow in Neovim. This is what provides hints, completions, diagnostics, or even docs for the language you’re working in. Cannot state how helpful these are when working in a typed language like Typescript or Go. I would also say it’s beneficial to learn how to set it up manually, and [Typecraft's video](https://youtu.be/S-xzYgTLVJE?si=xG7c-Yx0fkxRHwx0) does a great job showing how it’s done. |
|
| 86 | 86 | ||
| 87 | 87 | <video |
|
| 88 | - | autoPlay |
|
| 89 | - | muted |
|
| 90 | - | loop |
|
| 91 | - | playsinline |
|
| 92 | - | className="w-full aspect-video" |
|
| 93 | - | src="https://dweb.mypinata.cloud/ipfs/QmdQSzMuPiDEcFhWLMLsabLLsHD4qG74Qh1WtvQ8SnSa1B" |
|
| 88 | + | autoPlay |
|
| 89 | + | muted |
|
| 90 | + | loop |
|
| 91 | + | playsinline |
|
| 92 | + | className="aspect-video w-full" |
|
| 93 | + | src="https://dweb.mypinata.cloud/ipfs/QmdQSzMuPiDEcFhWLMLsabLLsHD4qG74Qh1WtvQ8SnSa1B" |
|
| 94 | 94 | ></video> |
|
| 95 | 95 | ||
| 96 | 96 | These two are on the smaller side but are still really great to use. The first is [Tmux Navigator.](https://github.com/christoomey/vim-tmux-navigator) This allows you to navigate between an open Neovim pane and a Tmux pane without using a Tmux prefix. For example, instead of navigating with `Ctrl + b - l` I can just use `Ctrl - l`. It’s the same mapping for switching between Neovim panes and Tmux panes, which is a huge quality of life improvement. The other small mention is [blame.nvim.](https://github.com/FabijanZulj/blame.nvim) With this tool I can hit `Space-b` to see line by line who changed what when. This is great when working with other people on a project and you’re trying to find out what changed when. There’s also the LazyGit plugin for Neovim, but it deserves its own section. |
|
| 100 | 100 | For the longest time I just used git in the command line for handling any of my git needs, but a lot of that changed when I started working on more team oriented projects. Handling git conflicts was a nightmare, as well as going through git history to see what changed at each commit. LazyGit changed all of that. This tool really simplifies things like cherry picking commits, handling conflicts, and viewing rich history. There’s so much it can do that I haven’t even touched the surface on, and I would recommend [this video](https://youtu.be/CPLdltN7wgE?si=7XqMjlwpi5tmWFpA) by its creator to see what’s possible (if you’re like me you’ll also learn more about git itself from it). |
|
| 101 | 101 | ||
| 102 | 102 | <video |
|
| 103 | - | autoPlay |
|
| 104 | - | muted |
|
| 105 | - | loop |
|
| 106 | - | playsinline |
|
| 107 | - | className="w-full aspect-video" |
|
| 108 | - | src="https://dweb.mypinata.cloud/ipfs/QmZou8CVipfiFxYkYYm3H6BAzKk1ks6mkosTULxk7GgFUb" |
|
| 103 | + | autoPlay |
|
| 104 | + | muted |
|
| 105 | + | loop |
|
| 106 | + | playsinline |
|
| 107 | + | className="aspect-video w-full" |
|
| 108 | + | src="https://dweb.mypinata.cloud/ipfs/QmZou8CVipfiFxYkYYm3H6BAzKk1ks6mkosTULxk7GgFUb" |
|
| 109 | 109 | ></video> |
|
| 110 | 110 | ||
| 111 | 111 | ## Bringing it All Together |
|
| 113 | 113 | Let’s do a run through of what starting and managing a project might look for me with this workflow. First `t` to start my Tmux session manager, then navigate to my dev folder. There I’ll run a command like `npx create-next-app@latest`. Once the repo is created I’ll `cd` into it and run `nvim` which will greet me with a telescope window of all the files that I can fuzzy find through. Then I might open a split pane to the right with Tmux so I can have a terminal to run commands like `npm run dev`. |
|
| 114 | 114 | ||
| 115 | 115 | <video |
|
| 116 | - | autoPlay |
|
| 117 | - | muted |
|
| 118 | - | loop |
|
| 119 | - | playsinline |
|
| 120 | - | className="w-full aspect-video" |
|
| 121 | - | src="https://dweb.mypinata.cloud/ipfs/QmUW8QWY1ug3uHzLWtc6F9pFHAurgbjG3qmnc9o6whZDoZ" |
|
| 116 | + | autoPlay |
|
| 117 | + | muted |
|
| 118 | + | loop |
|
| 119 | + | playsinline |
|
| 120 | + | className="aspect-video w-full" |
|
| 121 | + | src="https://dweb.mypinata.cloud/ipfs/QmUW8QWY1ug3uHzLWtc6F9pFHAurgbjG3qmnc9o6whZDoZ" |
|
| 122 | 122 | ></video> |
|
| 123 | 123 | ||
| 124 | 124 | If the project has multiple repos like a server/client combo or I’m referencing another repo, I’ll create a new window with the same pane setup. As I work and make changes, I’ll open LazyGit in one of my Tmux panes and run `ctrl-b + z` to make it full screen. From there I’ll add my commits and push them up, or make a branch that I can merge main into if I’m already working on a shared project. |
|
| 125 | 125 | ||
| 126 | 126 | <video |
|
| 127 | - | autoPlay |
|
| 128 | - | muted |
|
| 129 | - | loop |
|
| 130 | - | playsinline |
|
| 131 | - | className="w-full aspect-video" |
|
| 132 | - | src="https://dweb.mypinata.cloud/ipfs/QmdrPRHBkMNCvcbCEaQp9PeUKQsuCHKHgEecBMG6tRnLyB" |
|
| 127 | + | autoPlay |
|
| 128 | + | muted |
|
| 129 | + | loop |
|
| 130 | + | playsinline |
|
| 131 | + | className="aspect-video w-full" |
|
| 132 | + | src="https://dweb.mypinata.cloud/ipfs/QmdrPRHBkMNCvcbCEaQp9PeUKQsuCHKHgEecBMG6tRnLyB" |
|
| 133 | 133 | ></video> |
|
| 134 | 134 | ||
| 135 | 135 | Back in my main Next.js window I might open another split pane below the right one so while the dev server is running I can make test API calls from the terminal with httpie, maybe pipe the results into jq then into a file. |
|
| 136 | 136 | ||
| 137 | 137 | <video |
|
| 138 | - | autoPlay |
|
| 139 | - | muted |
|
| 140 | - | loop |
|
| 141 | - | playsinline |
|
| 142 | - | className="w-full aspect-video" |
|
| 143 | - | src="https://dweb.mypinata.cloud/ipfs/Qmc1Lfd6KmrWSU1kTtrnT59nyPx1QpTvhEzPY9taagWCuv" |
|
| 138 | + | autoPlay |
|
| 139 | + | muted |
|
| 140 | + | loop |
|
| 141 | + | playsinline |
|
| 142 | + | className="aspect-video w-full" |
|
| 143 | + | src="https://dweb.mypinata.cloud/ipfs/Qmc1Lfd6KmrWSU1kTtrnT59nyPx1QpTvhEzPY9taagWCuv" |
|
| 144 | 144 | ></video> |
|
| 145 | 145 | ||
| 146 | 146 | This is the flexibility of a terminal based workflow that is hard to replicate on something like VSCode or Zed. It’s not even an editor issue in my opinion: it’s a development environment issue. Do code editors like VSCode take out out of that environment? Sorta, not totally, but it’s definitely not the same. |
|
| 46 | 46 | When it comes to enabling writing in an app beyond just a text area there are many library choices out there. For Snippets I went with `@uiw/codemirror` for several reasons. For starters it was pretty easy to use and setup, had it running in no time. |
|
| 47 | 47 | ||
| 48 | 48 | ```typescript |
|
| 49 | - | import React from 'react'; |
|
| 50 | - | import CodeMirror from '@uiw/react-codemirror'; |
|
| 51 | - | import { javascript } from '@codemirror/lang-javascript'; |
|
| 49 | + | import React from "react"; |
|
| 50 | + | import CodeMirror from "@uiw/react-codemirror"; |
|
| 51 | + | import { javascript } from "@codemirror/lang-javascript"; |
|
| 52 | 52 | ||
| 53 | 53 | function App() { |
|
| 54 | - | const [value, setValue] = React.useState("console.log('hello world!');"); |
|
| 55 | - | const onChange = React.useCallback((val, viewUpdate) => { |
|
| 56 | - | console.log('val:', val); |
|
| 57 | - | setValue(val); |
|
| 58 | - | }, []); |
|
| 59 | - | return <CodeMirror value={value} height="200px" extensions={[javascript({ jsx: true })]} onChange={onChange} />; |
|
| 54 | + | const [value, setValue] = React.useState("console.log('hello world!');"); |
|
| 55 | + | const onChange = React.useCallback((val, viewUpdate) => { |
|
| 56 | + | console.log("val:", val); |
|
| 57 | + | setValue(val); |
|
| 58 | + | }, []); |
|
| 59 | + | return ( |
|
| 60 | + | <CodeMirror |
|
| 61 | + | value={value} |
|
| 62 | + | height="200px" |
|
| 63 | + | extensions={[javascript({ jsx: true })]} |
|
| 64 | + | onChange={onChange} |
|
| 65 | + | /> |
|
| 66 | + | ); |
|
| 60 | 67 | } |
|
| 61 | 68 | export default App; |
|
| 62 | 69 | ``` |
|
| 218 | 225 | ||
| 219 | 226 | async function fetchData(cid: string) { |
|
| 220 | 227 | try { |
|
| 221 | - | const req = await fetch( |
|
| 222 | - | `https://${process.env.GATEWAY_DOMAIN}/ipfs/${cid}`, |
|
| 223 | - | ); |
|
| 228 | + | const req = await fetch(`https://${process.env.GATEWAY_DOMAIN}/ipfs/${cid}`); |
|
| 224 | 229 | const res = await req.json(); |
|
| 225 | 230 | return res; |
|
| 226 | 231 | } catch (error) { |
|
| 233 | 238 | const cid = params.cid; |
|
| 234 | 239 | const data = await fetchData(cid); |
|
| 235 | 240 | return ( |
|
| 236 | - | <main className="flex min-h-screen flex-col items-center sm:justify-center justify-start"> |
|
| 241 | + | <main className="flex min-h-screen flex-col items-center justify-start sm:justify-center"> |
|
| 237 | 242 | <Header /> |
|
| 238 | - | <ReadOnlyEditor |
|
| 239 | - | content={data.content} |
|
| 240 | - | name={data.name} |
|
| 241 | - | cid={cid} |
|
| 242 | - | lang={data.lang} |
|
| 243 | - | /> |
|
| 243 | + | <ReadOnlyEditor content={data.content} name={data.name} cid={cid} lang={data.lang} /> |
|
| 244 | 244 | <Footer /> |
|
| 245 | 245 | </main> |
|
| 246 | 246 | ); |
|
| 253 | 253 | ||
| 254 | 254 | ```typescript |
|
| 255 | 255 | <CodeMirror |
|
| 256 | - | className="text-md opacity-75 p-2 sm:w-[600px] sm:h-[700px] w-[350px] h-[450px] font-commitMono" |
|
| 256 | + | className="text-md font-commitMono h-[450px] w-[350px] p-2 opacity-75 sm:h-[700px] sm:w-[600px]" |
|
| 257 | 257 | height="100%" |
|
| 258 | 258 | width="100%" |
|
| 259 | 259 | value={content} |
|
| 273 | 273 | ||
| 274 | 274 |  |
|
| 275 | 275 | ||
| 276 | - | ||
| 277 | 276 | ## API + CLI |
|
| 278 | 277 | ||
| 279 | 278 | Since there isn't really any authentication in this app and anyone can upload snippets as much as they want, I figured "why not make the API accessible?" Anyone can make an API request to the app to make a snippet and use the data returned to make the link! |
|
| 280 | 279 | ||
| 281 | 280 | Request: |
|
| 281 | + | ||
| 282 | 282 | ```bash |
|
| 283 | 283 | curl --location 'https://www.snippets.so/api/upload' \ |
|
| 284 | 284 | --header 'Content-Type: application/json' \ |
|
| 290 | 290 | ``` |
|
| 291 | 291 | ||
| 292 | 292 | Returns: |
|
| 293 | + | ||
| 293 | 294 | ```json |
|
| 294 | 295 | { |
|
| 295 | - | "IpfsHash": "bafkreiccdt64k6d4wjgz5ebqee4rvmkauoiygc5egwtssl2zqq3o74zlti", |
|
| 296 | - | "PinSize": 81, |
|
| 297 | - | "Timestamp": "2024-07-10T02:25:51.052Z", |
|
| 298 | - | "isDuplicate": true |
|
| 296 | + | "IpfsHash": "bafkreiccdt64k6d4wjgz5ebqee4rvmkauoiygc5egwtssl2zqq3o74zlti", |
|
| 297 | + | "PinSize": 81, |
|
| 298 | + | "Timestamp": "2024-07-10T02:25:51.052Z", |
|
| 299 | + | "isDuplicate": true |
|
| 299 | 300 | } |
|
| 300 | 301 | ``` |
|
| 301 | 302 | ||
| 302 | 303 | Link: |
|
| 304 | + | ||
| 303 | 305 | ``` |
|
| 304 | 306 | https://snippets.so/snip/bafkreiccdt64k6d4wjgz5ebqee4rvmkauoiygc5egwtssl2zqq3o74zlti |
|
| 305 | 307 | ``` |
|
| 23 | 23 | Immediately I was mesmerized by the speed and powers demonstrated by The Primeagen's early videos. I was already a keyboard maximalist from [previous jobs](/posts/why-i-learned-vim) where I learned speed = productivity, so it was a no brainer that I had to learn it. Started with the basic motions and Vim tutor, and I had the advantage that I was just learning programming on the side instead of doing it full time. Within a few weeks I was in Vim consistently, writing and learning to code. The tweaking of my Vim RC eventually led to discovering Neovim thanks to [chris@machine](https://www.youtube.com/@chrisatmachine) and his early videos. |
|
| 24 | 24 | ||
| 25 | 25 | For the next several years I stuck with Neovim and I loved it, and I owe a mass amount of my productivity to it. There were countless hours spent configuring it like many of us do. I eventually got to a point where I didn't adjust my config much, but that soon didn't matter. |
|
| 26 | + | ||
| 26 | 27 | ## What Changed |
|
| 27 | 28 | ||
| 28 | 29 | Every now and then I would update a plugin in Neovim and everything would break, and I would have to spend time fixing it instead of getting work done. This resulted in slimming down my config more and more, but there was still so much that went into making all the basics work. I stuck with it because it was still better than using VSCode, which I did try for a two week sprint to see if it could be any better. It was also key to a [terminal based workflow](/posts/a-terminal-based-workflow) that other editors couldn't really match. |
|
| 39 | 40 | ||
| 40 | 41 | One of the biggest things that has stood out to me using Zed so far is how “everything just works.” There are so many features of an IDE or text editor that people take for granted until they have to set it up themselves in something lower level like Neovim. LSP (language server protocol) is certainly one of them. If you’re not familiar it’s the hints or errors that show up while you’re writing up your code, giving you deep insights to your repo on a language level. When you setup LSP in Neovim it’s a lot of work, and sometimes it can be a bit harder to figure out why it might be bugging out. However it does give you way more control and the option to do a lot of customization. With Zed LSP just works. There are configurations you can make to edit some things, but as a whole it just zips out of the box. There are already keybindings for things like “show definition”, “go to definition”, or even code actions. The only downside is outside of an extension you can’t use your own LSP that’s installed on your machine, but there’s always a pretty large language support that I haven’t had this issue yet. |
|
| 41 | 42 | ||
| 42 | - | ||
| 43 | 43 | <Image |
|
| 44 | 44 | src="https://dweb.mypinata.cloud/ipfs/QmNkysZ5Roy723sphUST8abmHakKR86K2YHXeeVTU22ASH" |
|
| 45 | 45 | alt="header image" |
|
| 48 | 48 | aspectRatio={9 / 16} |
|
| 49 | 49 | /> |
|
| 50 | 50 | ||
| 51 | - | Another piece that’s related to LSP is completions. This is when you’re typing some code and get suggestions for auto completions that quickly fill the rest of the code out. LSPs usually have great auto-completion because they’re aware of the patterns used in that language. Just to be clear we’re not talking about Copilot yet, this is just completions for snippets and LSP. Once again with Zed it just works out of the box, unlike Neovim which ends up requiring several plugins to make it work right. |
|
| 51 | + | Another piece that’s related to LSP is completions. This is when you’re typing some code and get suggestions for auto completions that quickly fill the rest of the code out. LSPs usually have great auto-completion because they’re aware of the patterns used in that language. Just to be clear we’re not talking about Copilot yet, this is just completions for snippets and LSP. Once again with Zed it just works out of the box, unlike Neovim which ends up requiring several plugins to make it work right. |
|
| 52 | 52 | ||
| 53 | 53 | <video |
|
| 54 | - | autoPlay |
|
| 55 | - | muted |
|
| 56 | - | loop |
|
| 57 | - | playsinline |
|
| 58 | - | className="w-full aspect-video" |
|
| 59 | - | src="https://dweb.mypinata.cloud/ipfs/QmVEqwPxAoWgDwCLM3PrVxseLtLjCDK8LMxxz6DyD5fjxz" |
|
| 54 | + | autoPlay |
|
| 55 | + | muted |
|
| 56 | + | loop |
|
| 57 | + | playsinline |
|
| 58 | + | className="aspect-video w-full" |
|
| 59 | + | src="https://dweb.mypinata.cloud/ipfs/QmVEqwPxAoWgDwCLM3PrVxseLtLjCDK8LMxxz6DyD5fjxz" |
|
| 60 | 60 | ></video> |
|
| 61 | 61 | ||
| 62 | 62 | Finally there’s Git integrations. What normally required multiple plugins in Neovim is again ready out of the box with Zed, including feature like toggling Git Blame, viewing diffs, and gutter symbols showing the status of edited lines. |
|
| 63 | 63 | ||
| 64 | 64 | <video |
|
| 65 | - | autoPlay |
|
| 66 | - | muted |
|
| 67 | - | loop |
|
| 68 | - | playsinline |
|
| 69 | - | className="w-full aspect-video" |
|
| 70 | - | src="https://dweb.mypinata.cloud/ipfs/Qmcqv6kXnHHVbX8RxWpK6coPZHyNcPU7bxPfZ2Zwbsm6bz" |
|
| 65 | + | autoPlay |
|
| 66 | + | muted |
|
| 67 | + | loop |
|
| 68 | + | playsinline |
|
| 69 | + | className="aspect-video w-full" |
|
| 70 | + | src="https://dweb.mypinata.cloud/ipfs/Qmcqv6kXnHHVbX8RxWpK6coPZHyNcPU7bxPfZ2Zwbsm6bz" |
|
| 71 | 71 | ></video> |
|
| 72 | 72 | ||
| 73 | 73 | If I had to make a crude comparison, it’s similar to Linux and Apple. Linux will give you far more control over every piece of your software and hardware at the cost of spending time to configure it. Apple will give you less control but it will likely run smoother. |
|
| 95 | 95 | Zed also features an assistant panel where you can access several AI models via API, including OpenAI, Ollama, and Anthropic. Just requires a few lines of config to get started. |
|
| 96 | 96 | ||
| 97 | 97 | <video |
|
| 98 | - | autoPlay |
|
| 99 | - | muted |
|
| 100 | - | loop |
|
| 101 | - | playsinline |
|
| 102 | - | className="w-full aspect-video" |
|
| 103 | - | src="https://dweb.mypinata.cloud/ipfs/QmPo8nXP8w9hJfxnhCpggrGKP89VbtiV1Rf3mFRHj9fUqA" |
|
| 98 | + | autoPlay |
|
| 99 | + | muted |
|
| 100 | + | loop |
|
| 101 | + | playsinline |
|
| 102 | + | className="aspect-video w-full" |
|
| 103 | + | src="https://dweb.mypinata.cloud/ipfs/QmPo8nXP8w9hJfxnhCpggrGKP89VbtiV1Rf3mFRHj9fUqA" |
|
| 104 | 104 | ></video> |
|
| 105 | 105 | ||
| 106 | 106 | One feature that I think is particularly nice is the inline assistant, where you can select some lines of code and use `ctrl-enter` to trigger a request to be made to your code via the AI assistance configuration mentioned previously. If you like the results then you can confirm and keep coding. |
|
| 107 | 107 | ||
| 108 | 108 | <video |
|
| 109 | - | autoPlay |
|
| 110 | - | muted |
|
| 111 | - | loop |
|
| 112 | - | playsinline |
|
| 113 | - | className="w-full aspect-video" |
|
| 114 | - | src="https://dweb.mypinata.cloud/ipfs/QmTTGznMRr64oqJG7hXFiCrqrTqXiY3kaPuQnhWbCCSAaL" |
|
| 109 | + | autoPlay |
|
| 110 | + | muted |
|
| 111 | + | loop |
|
| 112 | + | playsinline |
|
| 113 | + | className="aspect-video w-full" |
|
| 114 | + | src="https://dweb.mypinata.cloud/ipfs/QmTTGznMRr64oqJG7hXFiCrqrTqXiY3kaPuQnhWbCCSAaL" |
|
| 115 | 115 | ></video> |
|
| 116 | 116 | ||
| 117 | 117 | ### Zed ≠ Neovim |
|
| 153 | 153 | "shift-j": "editor::MoveLineDown", |
|
| 154 | 154 | "shift-k": "editor::MoveLineUp" |
|
| 155 | 155 | } |
|
| 156 | - | }, |
|
| 156 | + | } |
|
| 157 | 157 | ] |
|
| 158 | 158 | ``` |
|
| 159 | 159 | ||
| 169 | 169 | "ctrl-k": ["workspace::ActivatePaneInDirection", "Up"], |
|
| 170 | 170 | "ctrl-j": ["workspace::ActivatePaneInDirection", "Down"] |
|
| 171 | 171 | } |
|
| 172 | - | }, |
|
| 172 | + | } |
|
| 173 | 173 | ] |
|
| 174 | 174 | ``` |
|
| 175 | 175 | ||
| 176 | 176 | Something else I would recommend to anyone who is trying to migrate from Vim/Neovim to Zed is checking out the [default Vim keymap](https://github.com/zed-industries/zed/blob/340a1d145ed15e39a4a27afc5a189851308fb91d/assets/keymaps/vim.json#L4). There's so much there that acts as a helpful reference of what's supported and what you may want to adjust! |
|
| 177 | 177 | ||
| 178 | 178 | ### Reduced UI |
|
| 179 | + | ||
| 179 | 180 | Zed already has a pretty nice minimal UI, but I prefer something closer to my Neovim setup. Thankfully Zed offers these options, such as disabling the tab bar, scroll bar, reduced toolbar, and relative line numbers |
|
| 180 | 181 | ||
| 181 | 182 | ```json settings.json |
|
| 190 | 191 | "show": false |
|
| 191 | 192 | }, |
|
| 192 | 193 | "toolbar": { |
|
| 193 | - | "breadcrumbs": true, |
|
| 194 | - | "quick_actions": false |
|
| 195 | - | }, |
|
| 194 | + | "breadcrumbs": true, |
|
| 195 | + | "quick_actions": false |
|
| 196 | + | } |
|
| 196 | 197 | } |
|
| 197 | 198 | ``` |
|
| 198 | 199 | ||
| 212 | 213 | { |
|
| 213 | 214 | "context": "Editor && VimControl && !VimWaiting && !menu", |
|
| 214 | 215 | "bindings": { |
|
| 215 | - | "space o": "tab_switcher::Toggle", |
|
| 216 | + | "space o": "tab_switcher::Toggle" |
|
| 216 | 217 | } |
|
| 217 | 218 | } |
|
| 218 | 219 | ``` |
|
| 219 | 220 | ||
| 220 | 221 | <video |
|
| 221 | - | autoPlay |
|
| 222 | - | muted |
|
| 223 | - | loop |
|
| 224 | - | playsinline |
|
| 225 | - | className="w-full aspect-video" |
|
| 226 | - | src="https://dweb.mypinata.cloud/ipfs/QmQntcJQkJKoh2bmreRYun4BfDuLb8rXry6ZFmiVYPaeRx" |
|
| 222 | + | autoPlay |
|
| 223 | + | muted |
|
| 224 | + | loop |
|
| 225 | + | playsinline |
|
| 226 | + | className="aspect-video w-full" |
|
| 227 | + | src="https://dweb.mypinata.cloud/ipfs/QmQntcJQkJKoh2bmreRYun4BfDuLb8rXry6ZFmiVYPaeRx" |
|
| 227 | 228 | ></video> |
|
| 228 | 229 | ||
| 229 | 230 | Speaking of Telescope, one big replacement is project wide search. While Zed doesn't have a fuzzy find feature, the project wide search is excellent. It will show all results in a multibuffer view which is pretty slick, and allows you to jump between that view and the buffer itself pretty easily. |
|
| 230 | 231 | ||
| 231 | 232 | <video |
|
| 232 | - | autoPlay |
|
| 233 | - | muted |
|
| 234 | - | loop |
|
| 235 | - | playsinline |
|
| 236 | - | className="w-full aspect-video" |
|
| 237 | - | src="https://dweb.mypinata.cloud/ipfs/QmY2Cs7zBk7bEa7skNLBcA5dFnSmTS7CotFjftcMA2r3m1" |
|
| 233 | + | autoPlay |
|
| 234 | + | muted |
|
| 235 | + | loop |
|
| 236 | + | playsinline |
|
| 237 | + | className="aspect-video w-full" |
|
| 238 | + | src="https://dweb.mypinata.cloud/ipfs/QmY2Cs7zBk7bEa7skNLBcA5dFnSmTS7CotFjftcMA2r3m1" |
|
| 238 | 239 | ></video> |
|
| 239 | 240 | ||
| 240 | 241 | The terminal toggle is pretty similar to something like VSCode but there are some other hidden ways to get a better terminal experience. One of them is a shortcut to toggle the bottom terminal to be full screen, but even better is opening a terminal as a buffer in the main editing view. |
|
| 243 | 244 | { |
|
| 244 | 245 | "context": "Editor && VimControl && !VimWaiting && !menu", |
|
| 245 | 246 | "bindings": { |
|
| 246 | - | "space t": "workspace::NewCenterTerminal", |
|
| 247 | + | "space t": "workspace::NewCenterTerminal" |
|
| 247 | 248 | } |
|
| 248 | 249 | } |
|
| 249 | 250 | ``` |
|
| 250 | 251 | ||
| 251 | 252 | <video |
|
| 252 | - | autoPlay |
|
| 253 | - | muted |
|
| 254 | - | loop |
|
| 255 | - | playsinline |
|
| 256 | - | className="w-full aspect-video" |
|
| 257 | - | src="https://dweb.mypinata.cloud/ipfs/QmYGcEqane6cpVPJj9H7qjgYmV75GPhmy7hkKob6YPVscY" |
|
| 253 | + | autoPlay |
|
| 254 | + | muted |
|
| 255 | + | loop |
|
| 256 | + | playsinline |
|
| 257 | + | className="aspect-video w-full" |
|
| 258 | + | src="https://dweb.mypinata.cloud/ipfs/QmYGcEqane6cpVPJj9H7qjgYmV75GPhmy7hkKob6YPVscY" |
|
| 258 | 259 | ></video> |
|
| 259 | 260 | ||
| 260 | 261 | One of the big things I had to leave behind was Tmux and switching projects. While it isn't a perfect replacement, Zed has a "switch projects" feature which works really well and makes it pretty easy to switch contexts. You just won't get the exact same control and layout setup that you can get with Tmux |
|
| 274 | 275 | ``` |
|
| 275 | 276 | ||
| 276 | 277 | <video |
|
| 277 | - | autoPlay |
|
| 278 | - | muted |
|
| 279 | - | loop |
|
| 280 | - | playsinline |
|
| 281 | - | className="w-full aspect-video" |
|
| 282 | - | src="https://dweb.mypinata.cloud/ipfs/QmXh5mDRJyQRSCfmHNeZ5DDwnzPr3uo5mqCMQN1mLzGh6T" |
|
| 278 | + | autoPlay |
|
| 279 | + | muted |
|
| 280 | + | loop |
|
| 281 | + | playsinline |
|
| 282 | + | className="aspect-video w-full" |
|
| 283 | + | src="https://dweb.mypinata.cloud/ipfs/QmXh5mDRJyQRSCfmHNeZ5DDwnzPr3uo5mqCMQN1mLzGh6T" |
|
| 283 | 284 | ></video> |
|
| 284 | 285 | ||
| 285 | 286 | ## Should You Use Zed? |
|
| 24 | 24 | ||
| 25 | 25 | Of course, my time in retail was limited. It’s a hard job to sustain and justify over a long period of time, and I wanted something more regular. I heard banking was a good transition from retail, so I got a position as a teller at a local bank. Just like retail, banks work with data. A lot of data. From day one I had to learn how to use their software to access all that data, and it was not the best experience at first. We used a web GUI nicknamed "white screen" where you had to do a whole bunch of clicking to navigate or get access to anything. It wasn't the keyboard speed I was used to at my previous job. I found ways to get sort of fast, but nothing amazing. |
|
| 26 | 26 | ||
| 27 | - | Then one day, a coworker showed me "black screen." It was the exact same access to the data but through the familiar IBM style terminal UI, all driven by keyboard with no mouse! I was ecstatic, and quickly learned to master it. I was fast again, boosting productivity at incredible rates. I could pull up anything in a few key strokes faster than anyone. I loved it. Once again the hard work ethic and productivity paid off, and I climbed through the ranks of the bank and later became a manager in a back office support role. Through my speed and accuracy, I became the guy who knew a lot and was infamous for getting stuff done. |
|
| 27 | + | Then one day, a coworker showed me "black screen." It was the exact same access to the data but through the familiar IBM style terminal UI, all driven by keyboard with no mouse! I was ecstatic, and quickly learned to master it. I was fast again, boosting productivity at incredible rates. I could pull up anything in a few key strokes faster than anyone. I loved it. Once again the hard work ethic and productivity paid off, and I climbed through the ranks of the bank and later became a manager in a back office support role. Through my speed and accuracy, I became the guy who knew a lot and was infamous for getting stuff done. |
|
| 28 | 28 | ||
| 29 | 29 | Several years go by in that role and it started to take its toll on my mental health. It was 2020, my first son was just born, and while waiting in the car for my wife finish to a doctor's appointment (no plus ones back then), I stumbled upon the opportunity of programming via YouTube. I wanted a career change, and programming sounded interesting. After just one weekend of deep diving into web development basics, I was hooked. This began my journey of becoming a self-taught developer, which you can read more about [here](https://stevedylan.dev/posts/my-developer-journey). It was a rough but exciting time, as I would wake up early to learn and write code, go to work, help out my wife with our new son, then stay up late into the night to keep going. |
|
| 30 | 30 |
| 53 | 53 | title: "Raycaster Extension", |
|
| 54 | 54 | description: |
|
| 55 | 55 | "The fastest way to send a cast on Farcaster. A Raycast extension that allows you to sign into your Farcaster account and send casts with optional images via IPFS. ", |
|
| 56 | - | image: |
|
| 57 | - | "https://dweb.mypinata.cloud/ipfs/QmSsY6QnhdwbWunrgzTDkpvRd7oWx5nUp8v7UiMeGRFeZ1", |
|
| 56 | + | image: "https://dweb.mypinata.cloud/ipfs/QmSsY6QnhdwbWunrgzTDkpvRd7oWx5nUp8v7UiMeGRFeZ1", |
|
| 58 | 57 | link: "https://www.raycast.com/stevedylandev/raycaster", |
|
| 59 | 58 | tags: ["raycast", "developer tools", "productivity"], |
|
| 60 | 59 | }, |
|
| 79 | 78 | title: "Pinata-go-cli", |
|
| 80 | 79 | description: |
|
| 81 | 80 | "A Go rewrite of the Node.js CLI for Pinata, allows fast and extensive uploads to Pinata. Also includes helpful features for listing files and other API functionalities. ", |
|
| 82 | - | image: |
|
| 83 | - | "https://dweb.mypinata.cloud/ipfs/QmasHAZJ2kb9k3AqkQP4yzYbZn8zxFGsrygNv6HBdMn1uE", |
|
| 81 | + | image: "https://dweb.mypinata.cloud/ipfs/QmasHAZJ2kb9k3AqkQP4yzYbZn8zxFGsrygNv6HBdMn1uE", |
|
| 84 | 82 | link: "https://github.com/PinataCloud/pinata-go-cli", |
|
| 85 | 83 | tags: ["developer tools", "ipfs"], |
|
| 86 | 84 | }, |
|
| 55 | 55 | <body> |
|
| 56 | 56 | <SkipLink /> |
|
| 57 | 57 | <Header /> |
|
| 58 | - | <main id="main" class="flex-1"> |
|
| 58 | + | <main id="main" class="flex-1"> |
|
| 59 | 59 | <slot /> |
|
| 60 | 60 | </main> |
|
| 61 | 61 | <Footer /> |
| 37 | 37 | target="_blank" |
|
| 38 | 38 | rel="noopener noreferrer" |
|
| 39 | 39 | aria-label="Link to repos" |
|
| 40 | - | href="/projects" |
|
| 41 | - | >developer tools</a |
|
| 40 | + | href="/projects">developer tools</a |
|
| 42 | 41 | >, to writing |
|
| 43 | 42 | <a |
|
| 44 | 43 | class="cactus-link inline-block" |
| 41 | 41 | <section class="mt-16"> |
|
| 42 | 42 | <h2 class="title mb-4 text-xl">Extras</h2> |
|
| 43 | 43 | <ul class="space-y-4 sm:space-y-2"> |
|
| 44 | - | <li> |
|
| 45 | - | <a |
|
| 46 | - | href="https://pi.stevedylan.dev" |
|
| 47 | - | target="_blank" |
|
| 48 | - | rel="noopener noreferrer" |
|
| 49 | - | class="cactus-link inline-block" |
|
| 50 | - | ><Image src="https://api.iconify.design/cib:raspberry-pi.svg?color=%23888888" class="w-4 h-4 inline-block" height="100" width="100" alt="rasp pi logo"/> Steve's Pi |
|
| 51 | - | </a>: |
|
| 52 | - | <p class="inline-block sm:mt-2">See a live view of the Raspberry Pi on my desk</p> |
|
| 53 | - | </li> |
|
| 44 | + | <li> |
|
| 45 | + | <a |
|
| 46 | + | href="https://pi.stevedylan.dev" |
|
| 47 | + | target="_blank" |
|
| 48 | + | rel="noopener noreferrer" |
|
| 49 | + | class="cactus-link inline-block" |
|
| 50 | + | ><Image |
|
| 51 | + | src="https://api.iconify.design/cib:raspberry-pi.svg?color=%23888888" |
|
| 52 | + | class="inline-block h-4 w-4" |
|
| 53 | + | height="100" |
|
| 54 | + | width="100" |
|
| 55 | + | alt="rasp pi logo" |
|
| 56 | + | /> Steve's Pi |
|
| 57 | + | </a>: |
|
| 58 | + | <p class="inline-block sm:mt-2">See a live view of the Raspberry Pi on my desk</p> |
|
| 59 | + | </li> |
|
| 54 | 60 | <li> |
|
| 55 | 61 | <a |
|
| 56 | 62 | href="https://ethglobal.com/showcase/cosmic-cowboys-3q0co" |
|
| 57 | 63 | target="_blank" |
|
| 58 | 64 | rel="noopener noreferrer" |
|
| 59 | 65 | class="cactus-link inline-block" |
|
| 60 | - | ><Image height="100" width="100" src="https://api.iconify.design/ph:cowboy-hat-fill.svg?color=%23888888" class="w-4 h-4 inline-block" alt="cowboy logo"/> Cosmic Cowboys |
|
| 66 | + | ><Image |
|
| 67 | + | height="100" |
|
| 68 | + | width="100" |
|
| 69 | + | src="https://api.iconify.design/ph:cowboy-hat-fill.svg?color=%23888888" |
|
| 70 | + | class="inline-block h-4 w-4" |
|
| 71 | + | alt="cowboy logo" |
|
| 72 | + | /> Cosmic Cowboys |
|
| 61 | 73 | </a>: |
|
| 62 | 74 | <p class="inline-block sm:mt-2">EthGlobal 2023 hackathon winning project</p> |
|
| 63 | 75 | </li> |
|
| 67 | 79 | target="_blank" |
|
| 68 | 80 | rel="noopener noreferrer" |
|
| 69 | 81 | class="cactus-link inline-block" |
|
| 70 | - | ><Image height="100" width="100" src="https://api.iconify.design/material-symbols:photo-camera.svg?color=%23888888" class="h-4 w-4 inline-block" alt="camera icon" /> Photos |
|
| 82 | + | ><Image |
|
| 83 | + | height="100" |
|
| 84 | + | width="100" |
|
| 85 | + | src="https://api.iconify.design/material-symbols:photo-camera.svg?color=%23888888" |
|
| 86 | + | class="inline-block h-4 w-4" |
|
| 87 | + | alt="camera icon" |
|
| 88 | + | /> Photos |
|
| 71 | 89 | </a>: |
|
| 72 | 90 | <p class="inline-block sm:mt-2">My personal photography portfolio</p> |
|
| 73 | 91 | </li> |
|
| 77 | 95 | target="_blank" |
|
| 78 | 96 | rel="noopener noreferrer" |
|
| 79 | 97 | class="cactus-link inline-block" |
|
| 80 | - | ><Image height="100" width="100" src="https://dweb.mypinata.cloud/ipfs/QmXexbA6Raw4sq79NfXNrLesXNwXYpHUVNRSccF59ArGfo" class="w-4 h-4 inline-block" alt="pinata logo" /> Pinata |
|
| 98 | + | ><Image |
|
| 99 | + | height="100" |
|
| 100 | + | width="100" |
|
| 101 | + | src="https://dweb.mypinata.cloud/ipfs/QmXexbA6Raw4sq79NfXNrLesXNwXYpHUVNRSccF59ArGfo" |
|
| 102 | + | class="inline-block h-4 w-4" |
|
| 103 | + | alt="pinata logo" |
|
| 104 | + | /> Pinata |
|
| 81 | 105 | </a>: |
|
| 82 | 106 | <p class="inline-block sm:mt-2"> |
|
| 83 | 107 | Where I'm currently working as Head of Developer Relations |
|
| 13 | 13 | <div class="space-y-6"> |
|
| 14 | 14 | <h1 class="title">Videos</h1> |
|
| 15 | 15 | <p>Here are some samples of video content I've produced to help users!</p> |
|
| 16 | - | {videoIds.map((id) => ( |
|
| 17 | - | <div class="relative w-full" style="padding-bottom: 73.17%;"> |
|
| 18 | - | <iframe |
|
| 19 | - | class="absolute top-0 left-0 w-full h-full" |
|
| 20 | - | src={`https://www.youtube.com/embed/${id}`} |
|
| 21 | - | title="YouTube video player" |
|
| 22 | - | frameborder="0" |
|
| 23 | - | allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" |
|
| 24 | - | referrerpolicy="strict-origin-when-cross-origin" |
|
| 25 | - | allowfullscreen |
|
| 26 | - | ></iframe> |
|
| 27 | - | </div> |
|
| 28 | - | ))} |
|
| 16 | + | { |
|
| 17 | + | videoIds.map((id) => ( |
|
| 18 | + | <div class="relative w-full" style="padding-bottom: 73.17%;"> |
|
| 19 | + | <iframe |
|
| 20 | + | class="absolute left-0 top-0 h-full w-full" |
|
| 21 | + | src={`https://www.youtube.com/embed/${id}`} |
|
| 22 | + | title="YouTube video player" |
|
| 23 | + | frameborder="0" |
|
| 24 | + | allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" |
|
| 25 | + | referrerpolicy="strict-origin-when-cross-origin" |
|
| 26 | + | allowfullscreen |
|
| 27 | + | /> |
|
| 28 | + | </div> |
|
| 29 | + | )) |
|
| 30 | + | } |
|
| 29 | 31 | </div> |
|
| 30 | 32 | </PageLayout> |
| 1 | 1 | @font-face { |
|
| 2 | - | font-family: 'Commit Mono'; |
|
| 3 | - | src: url("/CommitMono-400-Regular.otf") format("opentype"); |
|
| 4 | - | font-weight: 400; |
|
| 5 | - | font-style: normal; |
|
| 6 | - | font-display: swap; |
|
| 2 | + | font-family: "Commit Mono"; |
|
| 3 | + | src: url("/CommitMono-400-Regular.otf") format("opentype"); |
|
| 4 | + | font-weight: 400; |
|
| 5 | + | font-style: normal; |
|
| 6 | + | font-display: swap; |
|
| 7 | 7 | } |
|
| 8 | 8 | @font-face { |
|
| 9 | - | font-family: 'Commit Mono'; |
|
| 10 | - | src: url("/CommitMono-700-Regular.otf") format("opentype"); |
|
| 11 | - | font-weight: 700; |
|
| 12 | - | font-style: normal; |
|
| 13 | - | font-display: swap; |
|
| 9 | + | font-family: "Commit Mono"; |
|
| 10 | + | src: url("/CommitMono-700-Regular.otf") format("opentype"); |
|
| 11 | + | font-weight: 700; |
|
| 12 | + | font-style: normal; |
|
| 13 | + | font-display: swap; |
|
| 14 | 14 | } |
|
| 15 | 15 | @tailwind base; |
|
| 16 | 16 | @layer base { |
|
| 17 | 17 | :root { |
|
| 18 | 18 | color-scheme: light; |
|
| 19 | 19 | --theme-bg: #000000; |
|
| 20 | - | --theme-link: #FFFFFF; |
|
| 21 | - | --theme-text: #FFFFFF; |
|
| 22 | - | --theme-accent: #FFFFFF; |
|
| 23 | - | --theme-accent-2: #FFFFFF; |
|
| 24 | - | --theme-quote: #FFFFFF; |
|
| 20 | + | --theme-link: #ffffff; |
|
| 21 | + | --theme-text: #ffffff; |
|
| 22 | + | --theme-accent: #ffffff; |
|
| 23 | + | --theme-accent-2: #ffffff; |
|
| 24 | + | --theme-quote: #ffffff; |
|
| 25 | 25 | --theme-menu-bg: rgb(0, 0, 0 / 0.85); |
|
| 26 | 26 | } |
|
| 27 | 27 | ||
| 28 | 28 | :root.dark { |
|
| 29 | 29 | color-scheme: dark; |
|
| 30 | 30 | --theme-bg: #000000; |
|
| 31 | - | --theme-link: #FFFFFF; |
|
| 32 | - | --theme-text: #FFFFFF; |
|
| 33 | - | --theme-accent: #FFFFFF; |
|
| 34 | - | --theme-accent-2: #FFFFFF; |
|
| 35 | - | --theme-quote: #FFFFFF; |
|
| 31 | + | --theme-link: #ffffff; |
|
| 32 | + | --theme-text: #ffffff; |
|
| 33 | + | --theme-accent: #ffffff; |
|
| 34 | + | --theme-accent-2: #ffffff; |
|
| 35 | + | --theme-quote: #ffffff; |
|
| 36 | 36 | --theme-menu-bg: rgb(0, 0, 0 / 0.85); |
|
| 37 | 37 | } |
|
| 38 | 38 |
| 3 | 3 | "compilerOptions": { |
|
| 4 | 4 | "baseUrl": ".", |
|
| 5 | 5 | "paths": { |
|
| 6 | - | "@/components/*": ["src/components/*.astro"], |
|
| 7 | - | "@/layouts/*": ["src/layouts/*.astro"], |
|
| 8 | - | "@/utils": ["src/utils/index.ts"], |
|
| 9 | - | "@/stores/*": ["src/stores/*"], |
|
| 10 | - | "@/data/*": ["src/data/*"], |
|
| 11 | - | "@/site-config": ["src/site.config.ts"] |
|
| 6 | + | "@/*": ["src/*"] |
|
| 12 | 7 | } |
|
| 13 | 8 | }, |
|
| 14 | 9 | "exclude": ["node_modules", "**/node_modules/*", ".vscode", "dist"] |