format code 748655e0
Steve · 2024-09-18 00:10 16 file(s) · +351 −338
src/components/BaseHead.astro +1 −1
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
src/components/SkipLink.astro +1 −1
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>
src/components/ThemeToggle.astro +2 −2
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"
src/content/post/How To Run Your Own IPFS Gateway.mdx +13 −14
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
src/content/post/How to Encrypt and Decrypt Files on IPFS Using Lit.md +111 −118
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!
src/content/post/a-terminal-based-workflow.mdx +60 −60
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.
src/content/post/building-snippets-so.mdx +27 −25
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
![gif of share page](https://res.cloudinary.com/df9dofjus/image/upload/v1722565319/Screenshot-Arc-08-01-2024-22-24_muxrrg.gif)
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
```
src/content/post/leaving-neovim-for-zed.mdx +58 −57
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?
src/content/post/why-i-learned-vim.mdx +1 −1
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
src/data/projects.ts +2 −4
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
	},
src/layouts/Base.astro +1 −1
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 />
src/pages/about.astro +1 −2
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"
src/pages/index.astro +37 −13
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
src/pages/videos.astro +15 −13
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>
src/styles/global.css +20 −20
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
tsconfig.json +1 −6
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"]