MemoryLane 是一个数字时光胶囊应用程序,允许用户保存瞬间和想法,将它们存储为文件,并在未来重新发现它们。它结合了传统时光胶囊的怀旧感和现代云存储技术的便利性、可访问性和安全性。MemoryLane 的特点包括但不限于数字保存、未来的揭示、灵活的时间设定、多媒体体验、个人和团队协作功能、安全和隐私以及基于云的可靠性。通过这些功能,MemoryLane 通过创建共享连接、自省和数字遗赠来触动用户的情感和需求,所有这些都将在 Pinata 的支持下实现。
Pinata 是一个基于云的文件上传服务,常被称为“互联网的文件 API”,它是一个分布式的云存储解决方案,常用于 Web3 应用程序,但不仅限于 dApps。它也可以用于 Web2 应用程序。Pinata 为开发人员提供了通过其 API 上传和管理文件的无缝体验,包括私密文件存储、CDN 支持和可定制的访问控制,如预签名 URL。它还具有图像优化、插件兼容、TypeScript SDK 以实现快速集成、星际文件系统(IPFS)、用于存储和共享公共文件的点对点网络(P2P),主要用于 Web3 应用程序,以及文件 API,专注于提供安全和私人的文件管理。
在本文中,我们将通过构建 MemoryLane 使用 NextJS 来探索 Pinata 的多个功能。
技术实施方案项目启动
- 创建一个新的 Next 应用程序并选择您喜欢的选项。本文使用 Next 的 TypeScript 版本。
# 使用 npx 安装最新版的 create-next-app,并创建名为 memorylane 的项目
npx create-next-app@latest memorylane
# 切换到 memorylane 项目目录
cd memorylane
进入全屏 退出全屏
- 安装Pinata以及其他应用所需的依赖项。
axios
将在客户端用来调用我们NextJS应用所设置的Pinata API。react-dropzone
将用于处理文件上传表单输入,而date-fns
将帮助我们将JavaScript Date对象格式化为所需的格式。
使用npm安装以下依赖包:pinata, axios, react-dropzone和date-fns.
切换到全屏 退出全屏
- 创建一个
.env.local
文件来存储 Pinata 环境变量。通过使用.env.local
而不是.env
,我们将避免在提交和推送至 GitHub 时暴露我们的秘密环境变量。GitHub 会自动忽略.env.local
文件,因为它仅存在于我们的本地开发环境中。 - 继续前往 Pinata.cloud,如果你是新用户,先注册,然后登录,进入开发人员仪表板,在 API 密钥选项卡,为 MemoryLane 应用程序生成一个新的密钥。将密钥命名为 memorylane 并将其作用域设置为 admin。admin 作用域让我们可以访问所有端点和账户设置。
- 创建密钥之后,Pinata 会给我们提供一个 API KEY、API SECRET 和 JWT。我们将把 JWT 存储在我们之前创建的
.env.local
文件中。 - 切换到网关标签并复制域名如下。将域名存储在
.env.local
文件中作为 NEXT_PUBLIC_PINATA_GATEWAY_URL=。完成这些步骤后,我们的.env.local
文件中应该有如下两个环境变量。
完成第六步后,我们完成了项目设置,现在可以开始构建MemoryLane应用。
打造MemoryLane
回到代码编辑器,只有7个对我们重要的文件。我们已经在项目设置部分创建并设置了env.local
文件。在这个部分,我们将设置其余的6个文件,并把Pinata集成到MemoryLane应用的逻辑中。
utils/配置.ts
(utils/config.ts)
这是我们的应用将用来与Pinata Files API交互的接口的文件。在这里,我们将从Pinata导入PinataSDK并创建一个将要导出的类的实例。PinataSDK类接收两个参数,即pinataJwt
和pinataGateway
。由于我们已经在环境变量中定义了这两个参数,我们可以使用process.env.VARIABLE_NAME
来获取它们。
"仅服务器端"
import { PinataSDK } from "pinata"
export const pinata = new PinataSDK({
pinataJwt: `${process.env.PINATA_JWT}`,
pinataGateway: `${process.env.NEXT_PUBLIC_PINATA_GATEWAY_URL}`
})
// 在文件顶部声明 "仅服务器端",以确保此类实例仅在服务器端可执行。
全屏 退出全屏
components/文件上传.tsx
这里我们将定义react-dropzone
,我们之前已经安装了它。它用于处理和简化React应用程序中的文件上传过程。可以通过打开计算机库选择文件或通过拖放上传任何类型的文件,并为组件UI提供有关文件的有用信息。您可以配置它以上传一个或多个文件,并限制文件类型为特定类型。
import { convertFileToUrl } from "@/utils";
import Image from "next/image";
import { Dispatch, SetStateAction } from "react";
import Dropzone, {
DropzoneInputProps,
DropzoneRootProps,
} from "react-dropzone";
interface DropzonePropTypes {
getRootProps: (props?: DropzoneRootProps) => DropzoneRootProps;
getInputProps: (props?: DropzoneInputProps) => DropzoneInputProps;
isDragActive: boolean;
}
export default function FileUpload({
handleFile,
file,
}: {
handleFile: Dispatch<SetStateAction<File | undefined>>;
file: File | undefined;
}) {
return (
<Dropzone
onDrop={(acceptedFiles: File[]) => {
handleFile(acceptedFiles[0]);
}}
accept={{ "image/*": [".png", ".jpg", ".jpeg"] }}
multiple={false}
>
{({ getRootProps, getInputProps, isDragActive }: DropzonePropTypes) => (
<section className="flex flex-col gap-y-2">
<label className="text-sm text-gray-500" htmlFor="file-input">
选择文件
</label>
<div
{...getRootProps()}
className="border border-dotted bg-gray-100 h-[200px] flex flex-col items-center justify-center rounded-md"
>
<input {...getInputProps()} className="sr-only" id="file-input" />
{!file ? (
<>
{isDragActive ? (
<p>将文件拖放到此处 ...</p>
) : (
<p className="font-medium cursor-pointer">
将文件拖放到此处或{" "}
<span className="underline">点击浏览</span>
</p>
)}
<p className="text-gray-500 text-xs">JPG, PNG, PDF - 最大5MB</p>
</>
) : (
<Image
src={convertFileToUrl(file)}
alt="图片"
height={200}
width={320}
className="object-cover h-[200px] w-full rounded-md"
/>
)}
</div>
</section>
)}
</Dropzone>
);
}
// FileUpload 表单仅接受图像文件
全屏 → 退出全屏
components/时间胶囊组件.tsx
这是您输入将被接收的表单。它使用useState
钩子来管理file
和openDate
,以及表单的验证的isLoading
和error
状态。该表单向/api/files
发送POST请求,Pinata将处理文件,并返回文件URL。此URL和openDate
将被存入timeCapsules
,这是用于管理所有已创建的时间胶囊的另一个状态。
"use client";
import { Dispatch, SetStateAction, useState } from "react";
import { format, parseISO } from "date-fns";
import axios from "axios";
import FileUpload from "./FileUpload";
import { TimeCapsuleStateType } from "@/types";
export default function TimeCapsuleForm({
setTimeCapsules,
timeCapsules,
}: {
setTimeCapsules: Dispatch<SetStateAction<TimeCapsuleStateType[]>>;
timeCapsules: TimeCapsuleStateType[];
}) {
const [file, setFile] = useState<File | undefined>();
const [openDate, setOpenDate] = useState<Date | undefined>();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError("");
if (!file) {
setError("请选择一个文件。");
return;
}
if (!openDate) {
setError("请选择一个开启日期。");
return;
}
setIsLoading(true);
const formData = new FormData();
formData.append("file", file);
try {
const res = await axios.post(`/api/files`, formData);
const url = res?.data?.url;
setTimeCapsules([
...timeCapsules,
{ url, openDate, created_at: new Date() },
]);
setFile(undefined);
setOpenDate(undefined);
} catch (error: unknown) {
console.error(error);
setError(
"在创建时光胶囊时出错,请重试。"
);
} finally {
setIsLoading(false);
}
};
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedDate = parseISO(e.target.value);
setOpenDate(selectedDate);
};
const isFormValid = file && openDate;
return (
<form className="flex flex-col gap-y-5 max-w-[480px]" onSubmit={onSubmit}>
<FileUpload handleFile={setFile} file={file} />
<div className="flex flex-col gap-y-2">
<label className="text-sm text-gray-500" htmlFor="open-date">
开放日期
</label>
<input
type="date"
id="open-date"
placeholder="请选择一个开启日期"
onChange={handleDateChange}
value={openDate ? format(openDate, "yyyy-MM-dd") : ""}
min={format(new Date(), "yyyy-MM-dd")}
className="border py-1.5 px-3 rounded-md"
/>
</div>
{error && <p className="text-red-600 text-sm">{error}</p>}
<button
type="submit"
disabled={isLoading || !isFormValid}
className="bg-black text-white py-2 px-4 rounded-md disabled:opacity-50"
>
{isLoading ? "提交..." : "提交"}
</button>
</form>
);
}
全屏模式 退出全屏
api/files/route.ts
文件路径
在该文件中,我们创建了一个POST请求处理程序来从前端表单接收一个文件,使用Pinata SDK将文件上传到云端并获取并返回一个签名的URL。
import { NextResponse, NextRequest } from "next/server";
import { pinata } from "@/utils/config";
export async function POST(req: NextRequest) {
try {
const data = await req.formData();
const file: File | null = data.get("file") as unknown as File;
const uploadData = await pinata.upload.file(file);
const url = await pinata.gateways.createSignedURL({
cid: uploadData.cid,
expires: 360000,
});
return NextResponse.json({ url }, { status: 200 });
} catch (e) {
console.error(e);
return NextResponse.json(
{ error: "服务器内部错误" },
{ status: 500 }
);
}
}
// 非常重要的是,在 createSignedURL 方法的对象中设置 expires 属性。如果不设置,就无法获取 URL。此外,expires 属性的值是以秒为单位的。
点击这里全屏开启 点击这里全屏关闭
时间胶囊组件文件 components/TimeCapsule.tsx
TimeCapsule 组件显示每个创建的时间胶囊。它接收 created_at
、openDate
和 URL
作为属性。created_at
顾名思义是时间胶囊创建的日期。它帮助用户回忆起他们为未来保存的那个时刻,从而产生一种怀旧的感觉。通过比较 openDate
和当前日期,来决定何时揭晓时间胶囊。图片只有在 openDate
到来,或者当前日期大于 JavaScript new Date()
对象时才会显示。img 标签被一个85%透明度的黑色覆盖层和位于中间的锁图标遮盖,以表明它是锁定状态。
import Image from "next/image";
import { format } from "date-fns";
import InfoIcon from "@/assets/icons/info-icon";
import LockIcon from "@/assets/icons/lock-icon";
import { TimeCapsuleStateType } from "@/types";
export default function TimeCapsule({
url,
openDate,
created_at,
}: TimeCapsuleStateType) {
return (
<div className="flex flex-col gap-y-2 max-w-[240px]">
<div className="relative">
<Image
src={url}
width={240}
height={240}
alt="时光胶囊"
className="object-cover aspect-square"
/>
{new Date() < new Date(openDate) && (
<div className="absolute bg-black/85 inset-0 flex items-center justify-center">
<LockIcon />
</div>
)}
</div>
<p className="text-sm font-medium text-gray-700">
将在 {format(openDate, "yyyy年MM月dd日")} 开启
</p>
<div className="bg-gray-200 flex space-x-1.5 p-2 rounded-xl">
<InfoIcon />
<p className="text-xs text-gray-500">
此时光胶囊创建于 {format(created_at, "yyyy年MM月dd日")}
</p>
</div>
</div>
);
}
全屏模式(按ESC退出).
这是一个使用TypeScript编写的React页面文件: page.tsx
最后,这里展示了TimeCapsuleForm
以及所有的时间胶囊。它利用浏览器的localStorage
API来保存更新过的时间胶囊的状态。每当创建一个新时间胶囊时,useEffect
钩子会收到通知,并运行一个函数来序列化并存储这个新创建的时间胶囊到localStorage
中,这样就可以保持一致性并提高流畅度。由于我们没有使用任何外部存储或数据库来保存时间胶囊,localStorage
API就成了我们最好的选择。它帮助我们持久化时间胶囊数据,因此即使在页面刷新或关闭浏览器会话后,时间胶囊数据仍然可以被保留下来。
"use client";
import { useState, useEffect } from "react";
import TimeCapsule from "@/components/TimeCapsule";
import TimeCapsuleForm from "@/components/TimeCapsuleForm";
import { TimeCapsuleStateType } from "@/types";
import { deserializeTimeCapsule, serializeTimeCapsule } from "@/utils";
export default function Home() {
const [timeCapsules, setTimeCapsules] = useState<TimeCapsuleStateType[]>(
() => {
if (typeof window !== "undefined") {
const storedCapsules = localStorage.getItem("timeCapsules");
if (storedCapsules) {
const parsedCapsules = JSON.parse(storedCapsules);
return parsedCapsules.map(deserializeTimeCapsule);
}
}
return [];
}
);
useEffect(() => {
const serializedCapsules = timeCapsules.map(serializeTimeCapsule);
localStorage.setItem("timeCapsules", JSON.stringify(serializedCapsules));
}, [timeCapsules]);
return (
<main className="container mx-auto my-10 flex flex-col gap-y-10">
<div>
<h2 className="text-2xl md:text-3xl font-semibold mb-5">
新建时光胶囊
</h2>
<TimeCapsuleForm
setTimeCapsules={setTimeCapsules}
timeCapsules={timeCapsules}
/>
</div>
<div>
<h2 className="text-2xl md:text-3xl font-semibold mb-5">
查看你的时光胶囊
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{timeCapsules &&
timeCapsules?.map((capsule) => (
<TimeCapsule
key={capsule.url}
url={capsule.url}
openDate={capsule.openDate}
created_at={capsule.created_at}
/>
))}
</div>
</div>
</main>
);
}
全屏模式 退出全屏
现在已经正确设置了主要文件,我们需要更新 next.config.mjs
文件。这能确保来自 PINATA_GATEWAY_URL 的图像被 NextJS 的 Image 组件正确显示。
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "your-gateway-url.mypinata.cloud",
},
],
},
};
// 配置 Next.js 图像加载远程模式
export default nextConfig;
进入全屏,退出全屏
在Vercel上部署MemoryLane
现在一切都说完了,我们需要将MemoryLane部署到Vercel,以便其他人可以使用它来创建时间胶囊。Vercel是一个云端托管平台,专用于托管网络应用。它提供了构建、扩展和保护应用的工具,帮助应用运行得更快。
登录到您的Vercel帐户,如果没有帐户就创建一个,将它与GitHub帐户关联,然后在仪表板中添加一个新的项目。
从 GitHub 导入 MemoryLane 项目,并在环境变量设置部分输入你的环境变量。请按照 .env.local
文件中的设定输入这些变量,然后点击部署按钮。Vercel 将为你构建应用程序,并会自动提供一个实时链接,让你可以查看你的应用程序。
本文探讨了Pinata,一个云文件上传服务,并利用它开发了MemoryLane,一个数字时光胶囊应用。Pinata的使用不仅限于Web2应用程序,尤其是Web3开发者也在构建dApps时利用其IPFS SDK享受其带来的好处。Pinata不仅适用于服务器端,如我们在应用中展示的,也适用于客户端,尤其在需要上传大文件时。
更多详情请访问文档。
封面图片来自ChatGPT
共同學習,寫下你的評論
評論加載中...
作者其他優質文章