Usando Next.js e Pinata.
Ao construir aplicativos, os desenvolvedores geralmente só querem ir para a codificação. Eles não querem configurar todos os suportes necessários para começar, mas isso faz parte da vida. É nesse ponto que os templates de iniciação fazem uma grande diferença.
Hoje, vamos criar um aplicativo simples que permite aos usuários fazer upload de arquivos do navegador para o IPFS. Usaremos o plano gratuito do Pinata para lidar com os uploads e também para gerar um link para o upload para compartilhamento. Para nos ajudar com o problema de estruturação, usaremos o Pinata Next.js Starter Template.
Iniciando
Antes de começarmos, vamos nos certificar de que estamos prontos para escrever algum código. Você precisará do seguinte:
- Node.js versão 16 ou superior
- NPM versão 8 ou superior
- Um bom editor de código
- Uma conta Pinata
Você pode verificar a versão do Node na linha de comando assim:
node --version
Você pode verificar sua versão do NPM de forma semelhante:
npm --version
Quando estiver pronto para se inscrever em sua conta Pinata gratuita, vá para a página de preços do Pinata e selecione o plano gratuito. E é isso. Você está pronto para codificar.
Criando a Estrutura
Na linha de comando, certifique-se de mudar para o diretório em que você tem todos os seus projetos de desenvolvimento. Agora, vamos criar um novo projeto chamado simple-ipfs. Na linha de comando, execute o seguinte comando:
npx create-pinata-app
Isso iniciará a ferramenta CLI e você será solicitado a responder a algumas perguntas. Primeiro, dê um nome ao seu aplicativo (simple-ipfs). Em seguida, decida se deseja trabalhar em TypeScript ou JavaScript. Escolherei JavaScript para este tutorial a fim de manter a simplicidade. Por fim, você será perguntado se deseja usar o Tailwind em seu projeto. Vou escolher “Yes”, mas você não precisa.
Em alguns segundos, você deverá ter um novo projeto pronto para codificar. Vá para o diretório do novo projeto:
cd simple-ipfs
Em seguida, abra o projeto no editor de código de sua preferência. Precisamos configurar nosso arquivo de variáveis de ambiente. Você notará que há um arquivo .env.sample. Vamos apenas copiar esse arquivo e renomeá-lo para .env.local. No arquivo, você verá três variáveis. As duas primeiras são as duas únicas variáveis necessárias, mas falaremos sobre a outra variável em breve.
Vamos começar obtendo nosso Pinata JWT. Para fazer isso, faça login em sua conta do Pinata. Depois de fazer login, acesse o link API Key na barra lateral esquerda. Aqui, você pode criar uma nova chave. Você precisará copiar o JWT recebido e colá-lo após o sinal = em seu arquivo .env.local para a variável PINATA_JWT. Você pode criar uma chave de administrador para este projeto, mas se quiser saber mais sobre a criação de chaves de API com escopos granulares, você pode ler mais sobre isso aqui.
Em seguida, você deverá obter o URL para seu Portal IPFS exclusivo. Este guia explica como fazer isso e o que os Portais Exclusivos podem fazer. Toda conta Pinata vem com um Portal Exclusivo. Nos planos pagos, os níveis de acesso e as restrições de largura de banda são muito maiores, mas, para este aplicativo, o plano Free (gratuito) deve funcionar bem. Quando você tiver o URL do Portal Exclusivo, adicione-o ao arquivo .env.local da mesma forma que adicionou o JWT.
A última variável no arquivo .env.local é opcional, a menos que você esteja buscando conteúdo da rede IPFS que não tenha sido fixado por você mesmo. Este aplicativo exigirá o upload e a fixação de conteúdo, portanto, não precisamos usar a variável NEXT_PUBLIC_GATEWAY_TOKEN.
Com essas variáveis no lugar, estamos prontos para iniciar o aplicativo. Execute o seguinte em sua linha de comando:
npm run dev
Ao acessar localhost:3000 em seu navegador, você deverá ver uma página como esta:
Isso é bom, mas vamos criar nosso próprio uploader simples e um sistema de roteamento para compartilhar detalhes de arquivos.
Construindo o App
Nosso aplicativo é um uploader simples que permite que você compartilhe um link com outras pessoas para fazer o download do arquivo compartilhado. Com isso em mente, vamos projetar nossa página de entrada. Ela deve ter um botão de upload e devemos permitir que o usuário dê um nome e uma descrição ao upload.
Se você abrir o arquivo pages/index.js, verá que já há muita coisa boa aqui, inclusive um elemento de entrada de arquivo e uma função para lidar com o upload. Não queremos destruir tudo, mas definitivamente precisamos mudar algumas coisas. Portanto, vamos substituir esse arquivo inteiro por:
import { useState, useRef } from "react";
import Head from "next/head";
import Files from "@/components/Files";
export default function Home() {
const [file, setFile] = useState("");
const [cid, setCid] = useState("");
const [uploading, setUploading] = useState(false);
const [form, setForm] = useState({
name: "",
description: "",
});
const inputFile = useRef(null);const uploadFile = async (e) => {
try {
e.preventDefault();
setUploading(true);
const formData = new FormData();
formData.append("file", file, { filename: file.name });
formData.append("name", form.name);
formData.append("description", form.description);
const res = await fetch("/api/files", {
method: "POST",
body: formData,
});
const ipfsHash = await res.text();
setCid(ipfsHash);
setUploading(false);
} catch (e) {
console.log(e);
setUploading(false);
alert("Trouble uploading file");
}
};
const handleChange = (e) => {
setFile(e.target.files[0]);
};
const loadRecent = async () => {
try {
const res = await fetch("/api/files");
const json = await res.json();
setCid(json.ipfs_pin_hash);
} catch (e) {
console.log(e);
alert("trouble loading files");
}
};
return (
<>
<Head>
<title>Simple IPFS</title>
<meta name="description" content="Generated with create-pinata-app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/pinnie.png" />
</Head>
<main className="m-auto flex min-h-screen w-full flex-col items-center justify-center">
<div className="m-auto flex h-full w-full flex-col items-center justify-center bg-cover bg-center">
<div className="h-full max-w-screen-xl">
<div className="m-auto flex h-full w-full items-center justify-center">
<div className="m-auto w-3/4 text-center">
<h1>Share files easily</h1>
<p className="mt-2">
Com o Simple IPFS, você pode carregar um arquivo,
obter um link e compartilhá-lo com qualquer pessoa que precise acessar o
arquivo. O link é permanente, mas só será compartilhado uma vez.
</p>
<input
type="file"
id="file"
ref={inputFile}
onChange={handleChange}
style={{ display: "none" }}
/>
<div className="mt-8 flex flex-col items-center justify-center rounded-lg bg-light p-2 text-center text-secondary">
<button
disabled={uploading}
onClick={() => inputFile.current.click()}
className="align-center flex h-64 w-3/4 flex-row items-center justify-center rounded-3xl bg-secondary px-4 py-2 text-light transition-all duration-300 ease-in-out hover:bg-accent hover:text-light">
{uploading ? (
"Uploading..."
) : (
<div>
<p className="text-lg font-light">
Selecione um arquivo para fazer upload para a rede IPFS
</p>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="m-auto mt-4 h-12 w-12 text-white">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5">
</path>
</svg>
</div>
)}
</button>
</div>
{file && (
<form onSubmit={uploadFile}>
<div className="mb-2">
<label htmlForm="name">Name</label><br/>
<input onChange={(e) => setForm({
...form,
name: e.target.value
})} className="border border-secondary rounded-md p-2 outline-none" id="name" value={form.name} placeholder="Name" />
</div>
<div>
<label htmlForm="description">Description</label><br />
<textarea
className="border border-secondary rounded-md p-2 outline-none"
value={form.description}
onChange={(e) => setForm({
...form,
description: e.target.value
})}
placeholder="Description..."
/>
</div>
<button className="rounded-lg bg-secondary text-white w-auto p-4" type="submit">Upload</button>
</form>
)}
{cid && <Files cid={cid} />}
</div>
</div>
</div>
</div>
</main>
</>
);
}
Reutilizamos grande parte das funcionalidades que vieram prontas para uso, mas reformulamos o aplicativo para combinar com nosso estilo pessoal e alteramos a funcionalidade de upload para aceitar o envio de um formulário com um nome e uma descrição do arquivo. Não vou perder tempo analisando o código da interface do usuário porque ele é, bem, uma interface do usuário. No entanto, preste atenção na função uploadFile e na função loadRecent (que não está sendo usada no momento). Essas funções estão chamando nosso backend sem servidor Next.js. A função loadRecent será usada mais tarde em uma página diferente, mas vamos ficar com ela aqui por um tempo.
Como estamos passando metadados sobre nosso arquivo para nossa função sem servidor, precisamos fazer um pequeno ajuste no código pronto para uso existente que vem com a API de modelo inicial. Abra o arquivo pages/api/files.js e encontre a linha que diz:
const response = await saveFile(files.file);
Vamos atualizar para:
const response = await saveFile(files.file, fields);
Isso está pegando os campos que foram passados como parte do upload de dados de formulário de várias partes do frontend e usando-os na função saveFile. Também precisaremos atualizar essa função. Portanto, encontre-a no código e atualize-a para que fique assim:
const saveFile = async (file, fields) => {
try {
const stream = fs.createReadStream(file.filepath);
const options = {
pinataMetadata: {
name: fields.name,
keyvalues: {
description: fields.description
}
},
};
const response = await pinata.pinFileToIPFS(stream, options);
fs.unlinkSync(file.filepath);
return response;
} catch (error) {
throw error;
}
}
Tudo o que mudamos aqui foi adicionar o nome e um par de valores-chave para a descrição. Esses dados não são armazenados no IPFS, mas são uma boa camada de conveniência fornecida pelo Pinata. Quando carregamos o arquivo, podemos mostrar essas informações no aplicativo.
Agora, você deve ter notado que no arquivo pages/index.js havia um componente chamado Files. Ainda não o alteramos, mas vamos fazê-lo. Esse componente exibe um identificador de conteúdo (CID) para o arquivo carregado e um link para visualizar o arquivo. Queremos alterar isso para exibir um link que possa ser compartilhado com outras pessoas, que é uma página dentro do nosso aplicativo.
Vamos abrir o arquivo components/Files.jsx. Queremos mostrar o CID do arquivo, mas também incluir um botão copy (cópia) que compartilhará o link para o arquivo. Atualmente, esse componente tem o CID e um link para visualizar ou fazer download do arquivo diretamente de um Portal Exclusivo. Vamos fazer algumas alterações. Atualize o componente para que ele tenha a seguinte aparência:
import React from "react";
export default function Files(props) {
const copyLink = async () => {
const copyText = `${window.location.origin}/${props.cid}`;
await navigator.clipboard.writeText(copyText);
alert("Copied: " + copyText);
};
return (
<div
onClick={copyLink}
className="m-auto mt-8 flex w-3/4 cursor-pointer flex-row justify-around rounded-lg"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.217 10.907a2.25 2.25 0 100 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186l9.566-5.314m-9.566 7.5l9.566 5.314m0 0a2.25 2.25 0 103.935 2.186 2.25 2.25 0 00-3.935-2.186zm0-12.814a2.25 2.25 0 103.933-2.185 2.25 2.25 0 00-3.933 2.185z"
/>
</svg>
<p>{props.cid}</p>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 7.5V6.108c0-1.135.845-2.098 1.976-2.192.373-.03.748-.057 1.123-.08M15.75 18H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08M15.75 18.75v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5A3.375 3.375 0 006.375 7.5H5.25m11.9-3.664A2.251 2.251 0 0015 2.25h-1.5a2.251 2.251 0 00-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5a9 9 0 00-9-9z"
/>
</svg>
</div>
);
}
A primeira coisa que você deve notar são os elementos SVG. Estamos usando alguns ícones SVG da Heroicons, uma excelente biblioteca de ícones de código aberto, para ajudar a dar uma boa aparência a esse componente. Temos um ícone de compartilhamento e um ícone de cópia com o CID do arquivo no meio.
Estamos quebrando as regras do HTML semântico para concluir este tutorial rapidamente, adicionando um manipulador de cliques ao nosso elemento DIV. Você deve usar um botão element (elemento) ou as propriedades Aria adequadas para acessibilidade na produção. Nosso manipulador de cliques DIV simplesmente copia o URL para compartilhar com outras pessoas.
Observe que, na função copyLink, estamos criando o link presumindo que haverá outra página em nosso aplicativo que aponta para o CID. Vamos criar essa página.
Na pasta pages de seu projeto, adicione um novo arquivo chamado [cid].js. Essa é uma maneira de informar ao Next.js que o arquivo usará o roteador dinâmico. Basicamente, qualquer coisa após a barra oblíqua no seu domínio, neste caso, usaria esta página.
Dentro do seu arquivo pages/[cid].js, acrescente o seguinte:
import Head from 'next/head';
import React, { useRef, useState, useEffect } from 'react'
import mime from 'mime';
const GATEWAY_URL = process.env.NEXT_PUBLIC_GATEWAY_URL
? process.env.NEXT_PUBLIC_GATEWAY_URL
: "https://gateway.pinata.cloud";
const CID = ({ fileData }) => {
const [href, setHref] = useState("");
const downloadRef = useRef(null);
useEffect(() => {
if(href) {
downloadRef.current.click();
}
}, [href]);
const download = async () => {
const res = await fetch(`${GATEWAY_URL}/ipfs/${fileData.ipfs_pin_hash}?download=true`);
const extension = mime.getExtension(res.headers.get('content-type'))
const blob = await res.blob();
const supportsFileSystemAccess =
'showSaveFilePicker' in window &&
(() => {
try {
return window.self === window.top;
} catch {
return false;
}
})();
// Se a API de Acesso ao Sistema de Arquivos for suportada…
if (supportsFileSystemAccess) {
try {
const handle = await showSaveFilePicker({
suggestedName: `${fileData.ipfs_pin_hash}.${extension}`,
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return;
} catch (err) {
if (err.name !== 'AbortError') {
console.error(err.name, err.message);
const blobUrl = URL.createObjectURL(blob);
setHref(blobUrl);
}
}
}
return (
<>
<Head>
<title>Simple IPFS</title>
<meta name="description" content="Generated with create-pinata-app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/pinnie.png" />
</Head>
<main className="m-auto flex min-h-screen w-full flex-col items-center justify-center">
<div className="m-auto flex h-full w-full flex-col items-center justify-center bg-cover bg-center">
<div className="h-full max-w-screen-xl">
<div className="m-auto flex h-full w-full items-center justify-center">
<div className="m-auto w-3/4 text-center">
<h1>Download file</h1>
<p className="mt-2">
Certifique-se de que você confia na fonte deste link. Se você não souber quem lhe enviou o link e não tiver certeza do que será baixado, não clique no botão de download.
</p>
<a className="hidden" href={href} ref={downloadRef} download={fileData.originalName} />
<div className="mt-8 flex flex-col items-center justify-center rounded-lg bg-light p-2 text-center align-center flex h-64 w-3/4 m-auto flex-row items-center justify-center rounded-3xl bg-secondary px-4 py-2 text-light transition-all duration-300 ease-in-out hover:bg-accent hover:text-light">
<h2 className="text-3xl">{fileData.metadata.name}</h2>
<h3 className="mb-8">{fileData.metadata.keyvalues.description}</h3>
<button
onClick={download}
className="underline"
>
Download
</button>
</div>
</div>
</div>
</div>
</div>
</main>
</>
)
}
export async function getServerSideProps(context) {
const pinataSDK = require("@pinata/sdk");
const pinata = new pinataSDK({ pinataJWTKey: process.env.PINATA_JWT });
// Buscar dados de uma API externa
const response = await pinata.pinList(
{
hashContains: context.query.cid
}
);
const fileData = response.rows[0];
return { props: { fileData } }
}
export default CID
Há muita coisa acontecendo nesse arquivo, mas vamos examiná-lo. Vamos começar pela parte inferior, pois o código na função getServerSideProps é executado no lado do servidor antes que qualquer código do cliente seja renderizado. Esta é uma função Next.js que lhe permite fazer requisições de dados que irão alimentar o frontend com a resposta.
Em nossa função getServerSideProps, estamos importando o SDK Pinata e usando uma variável de ambiente não pública (assim como fizemos em nossa rota de API de função sem servidor) para usar o SDK. Estamos consultando o CID do arquivo que compartilhamos e esse CID está contido no URL.
Com nosso resultado, nós o passamos como props que estão disponíveis em nosso componente no lado do cliente. Usamos os props para renderizar o nome e a descrição do nosso arquivo a partir dos metadados do Pinata. Também temos um link de download que aciona a função de download.
Essa função primeiro faz uma solicitação ao Portal, que você definiu em suas variáveis de ambiente anteriormente, para fazer o download do arquivo na memória. Em seguida, pegamos os cabeçalhos para identificar o tipo de arquivo por meio da propriedade content-type. Com isso, usamos uma biblioteca chamada mime para mapear esse tipo de conteúdo para uma extensão de arquivo. Em seguida, a função verifica se o navegador suporta a API do Sistema de Arquivos. Em caso afirmativo, exibimos um modal de download com um nome de arquivo e uma extensão pré-preenchidos. E se o navegador não for compatível com a API do sistema de arquivos, acionamos um link oculto no componente e redirecionamos para o link do arquivo no navegador. Ele exibirá o arquivo, se for compatível com o navegador, ou fará o download do arquivo no computador do usuário.
Quando um usuário abre o link de compartilhamento enviado por você em um navegador, ele verá uma descrição e a opção de fazer download. Se ele baixar o arquivo, poderá renomeá-lo, se desejar, e escolher onde ele será armazenado no computador.
E é isso! Toda a criação do aplicativo foi extremamente acelerada ao começar com o modelo Next.js do Pinata.
Concluindo
O Next.js é uma das estruturas mais populares para a criação de aplicativos React. O IPFS é a solução de armazenamento número um para dados off-chain. Agora, os dois estão combinados em um template inicial fácil de usar. Esse exemplo específico usa funções sem servidor para fazer upload de arquivos. Isso pode ser complicado devido aos limites que plataformas como Vercel e AWS têm sobre o tamanho da carga útil das funções sem servidor. Se você quiser fazer upload de arquivos maiores, fique atento a um tutorial futuro em que mostraremos como criar um token assinado para fazer uploads diretamente do cliente com segurança.
Se quiser ver o código completo desse aplicativo, você pode encontrá-lo no Github aqui.
Até lá, boa sorte!
Esse artigo foi escrito por Justin Hunter e traduzido por Fátima Lima. O original pode ser lido aqui.
Latest comments (0)