亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

MemoryLane:利用Pinata FILE API構建動態時光膠囊應用的項目

MemoryLane 是一个数字时光胶囊应用程序,允许用户保存瞬间和想法,将它们存储为文件,并在未来重新发现它们。它结合了传统时光胶囊的怀旧感和现代云存储技术的便利性、可访问性和安全性。MemoryLane 的特点包括但不限于数字保存、未来的揭示、灵活的时间设定、多媒体体验、个人和团队协作功能、安全和隐私以及基于云的可靠性。通过这些功能,MemoryLane 通过创建共享连接、自省和数字遗赠来触动用户的情感和需求,所有这些都将在 Pinata 的支持下实现。

Pinata 是一个基于云的文件上传服务,常被称为“互联网的文件 API”,它是一个分布式的云存储解决方案,常用于 Web3 应用程序,但不仅限于 dApps。它也可以用于 Web2 应用程序。Pinata 为开发人员提供了通过其 API 上传和管理文件的无缝体验,包括私密文件存储、CDN 支持和可定制的访问控制,如预签名 URL。它还具有图像优化、插件兼容、TypeScript SDK 以实现快速集成、星际文件系统(IPFS)、用于存储和共享公共文件的点对点网络(P2P),主要用于 Web3 应用程序,以及文件 API,专注于提供安全和私人的文件管理。

在本文中,我们将通过构建 MemoryLane 使用 NextJS 来探索 Pinata 的多个功能。

技术实施方案

项目启动

  1. 创建一个新的 Next 应用程序并选择您喜欢的选项。本文使用 Next 的 TypeScript 版本。
# 使用 npx 安装最新版的 create-next-app,并创建名为 memorylane 的项目
npx create-next-app@latest memorylane
# 切换到 memorylane 项目目录
cd memorylane

进入全屏 退出全屏

  1. 安装Pinata以及其他应用所需的依赖项。axios将在客户端用来调用我们NextJS应用所设置的Pinata API。react-dropzone将用于处理文件上传表单输入,而date-fns将帮助我们将JavaScript Date对象格式化为所需的格式。
使用npm安装以下依赖包:pinata, axios, react-dropzone和date-fns.

切换到全屏 退出全屏

  1. 创建一个 .env.local 文件来存储 Pinata 环境变量。通过使用 .env.local 而不是 .env,我们将避免在提交和推送至 GitHub 时暴露我们的秘密环境变量。GitHub 会自动忽略 .env.local 文件,因为它仅存在于我们的本地开发环境中。
  2. 继续前往 Pinata.cloud,如果你是新用户,先注册,然后登录,进入开发人员仪表板,在 API 密钥选项卡,为 MemoryLane 应用程序生成一个新的密钥。将密钥命名为 memorylane 并将其作用域设置为 admin。admin 作用域让我们可以访问所有端点和账户设置。

Pinata控制板 - 创建API密钥:

  1. 创建密钥之后,Pinata 会给我们提供一个 API KEY、API SECRET 和 JWT。我们将把 JWT 存储在我们之前创建的 .env.local 文件中。
  2. 切换到网关标签并复制域名如下。将域名存储在 .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类接收两个参数,即pinataJwtpinataGateway。由于我们已经在环境变量中定义了这两个参数,我们可以使用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钩子来管理fileopenDate,以及表单的验证的isLoadingerror状态。该表单向/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_atopenDateURL 作为属性。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帐户关联,然后在仪表板中添加一个新的项目。

在 Vercel 添加新项目

从 GitHub 导入 MemoryLane 项目,并在环境变量设置部分输入你的环境变量。请按照 .env.local 文件中的设定输入这些变量,然后点击部署按钮。Vercel 将为你构建应用程序,并会自动提供一个实时链接,让你可以查看你的应用程序。

配置您的项目 - 设置环境变量

结论

本文探讨了Pinata,一个云文件上传服务,并利用它开发了MemoryLane,一个数字时光胶囊应用。Pinata的使用不仅限于Web2应用程序,尤其是Web3开发者也在构建dApps时利用其IPFS SDK享受其带来的好处。Pinata不仅适用于服务器端,如我们在应用中展示的,也适用于客户端,尤其在需要上传大文件时。
更多详情请访问文档

封面图片来自ChatGPT

點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消