【Next.js 项目实战系列】02-创建 Issue

原文链接

CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的库点个star,关注一下吧 

上一篇【Next.js 项目实战系列】01-创建项目

创建 Issue

配置 MySQL 与 Prisma​

在数据库中可以找到相关内容,这里不再赘述

添加 model​

本节代码链接

# schema.prisma

model Issue {
  id Int @id @default(autoincrement())
  title String @db.VarChar(255)
  description String @db.Text
  status Status @default(OPEN)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt()
}

enum Status {
  OPEN
  IN_PROGRESS
  CLOSED
}

使用以下指令同步到数据库

npx prisma format
npx prisma migrate dev

编写 API​

本节代码链接

这里使用 zod 来验证表单,具体内容可参考使用 zod 验证表单

# /app/api/issues/route.ts

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import prisma from "@/prisma/client";

const createIssueSchema = z.object({
  title: z.string().min(1).max(255),
  description: z.string().min(1),
});

export async function POST(request: NextRequest) {
  const body = await request.json();
  const validation = createIssueSchema.safeParse(body);
  if (!validation.success)
    return NextResponse.json(validation.error.errors, { status: 400 });

  const newIssue = await prisma.issue.create({
    data: { title: body.title, description: body.description },
  });

  return NextResponse.json(newIssue, { status: 201 });
}

Radix-UI​

本节代码链接

radix-ui 也是一个类 DaisyUI 的组件库,使用如下指令安装

npm install @radix-ui/themes

安装好后,进行如下初始配置,将主 layout 中的所有内容用 <Theme > 标签包起来

# /app/layout.tsx

  import type { Metadata } from "next";
+ import "@radix-ui/themes/styles.css";
  import { Inter } from "next/font/google";
+ import { Theme } from "@radix-ui/themes";
  import "./globals.css";
  import NavBar from "./NavBar";

  const inter = Inter({ subsets: ["latin"] });

  export const metadata: Metadata = {
    title: "Create Next App",
    description: "Generated by create next app",
  };

  export default function RootLayout({
    children,
  }: Readonly<{
    children: React.ReactNode;
  }>) {
    return (
      <html lang="en">
        <body className={inter.className}>
+         <Theme>
            <NavBar />
            <main>{children}</main>
+         </Theme>
        </body>
      </html>
    );
  }

创建新 Issue 页面​

本节代码链接

# /app/issues/new/page.tsx

"use client";
import { Button, TextArea, TextField } from "@radix-ui/themes";

const NewIssuePage = () => {
  return (
    <div className="max-w-xl space-y-3">
      <TextField.Root>
        <TextField.Input placeholder="Title" />
      </TextField.Root>
      <TextArea placeholder="Description" />
      <Button>Submit New</Button>
    </div>
  );
};
export default NewIssuePage;

显示效果如下 

New Issue Page

Radix-UI 定义 UI 样式​

本节代码链接

在 layout.tsx 中添加 <Themepanel >

# /app/layout.tsx

+ import { Theme, ThemePanel } from "@radix-ui/themes";
  ...
  return (
    <html lang="en">
      <body className={inter.className}>
        <Theme>
          <NavBar />
          <main className="p-5">{children}</main>
+           <ThemePanel />
        </Theme>
      </body>
    </html>
  );
  ...

效果如下

Theme Panel

调整好自己想要的样式之后点击 Copy Theme,将 copy 到的 <Theme > 标签替换掉原来的即可

  #  /app/layout.tsx
  ...
  return (
    <html lang="en">
      <body className={inter.className}>
        {/*添加到这里即可*/}
        <Theme appearance="light" accentColor="violet">
          <NavBar />
          <main className="p-5">{children}</main>
        </Theme>
      </body>
    </html>
  );
  ...

设置字体​

在 Radix-UI 中设置字体需要以下步骤,可以参考 radix-ui-font

首先在 layout.tsx 中修改

# /app/layout.tsx

  import { Theme } from "@radix-ui/themes";
  import "@radix-ui/themes/styles.css";
  import type { Metadata } from "next";
  import { Inter } from "next/font/google";
  import NavBar from "./NavBar";
  import "./globals.css";
- const inter = Inter({ subsets: ["latin"] });
+ const inter = Inter({
+   subsets: ['latin'],
+   variable: '--font-inter',
+ });
  export const metadata: Metadata = {
    title: "Create Next App",
    description: "Generated by create next app",
  };

  export default function RootLayout({
    children,
  }: Readonly<{
    children: React.ReactNode;
  }>) {
    return (
      <html lang="en">
-       <body className={inter.className}>
+       <body className={inter.variable}>
          <Theme appearance="light" accentColor="violet">
            <NavBar />
            <main className="p-5">{children}</main>
          </Theme>
        </body>
      </html>
    );
  }

然后添加 /app/theme-config.css 并添加以下内容

/app/theme-config.css

.radix-themes {
  --default-font-family: var(--font-inter);
}

最后在 layout.tsx 中 import 进来即可

···
import "./theme-config.css";
···

MarkDown Editor​

本节代码链接

react-simlemde-editor 是一款集成式 MarkDown 编辑器,使用如下命令安装

npm install --save react-simplemde-editor easymde

效果如下:

Simple MarkDown Editor

提交表单​

本节代码链接

我们使用 react-hook-form 和 axios 进行表单提交

npm i react-hook-form
npm i axios
# /app/issues/new/page.tsx

  "use client";
  import { Button, TextField } from "@radix-ui/themes";
  import { useRouter } from "next/navigation";
  // import
+ import axios from "axios";
+ import "easymde/dist/easymde.min.css";
+ import { Controller, useForm } from "react-hook-form";
+ import SimpleMDE from "react-simplemde-editor";

  // 使用 interface 表明 form 中有哪些内容
+ interface IssueForm {
+   title: string;
+   description: string;
+ }

  const NewIssuePage = () => {
    // 使用 React Hook Form
+   const { register, control, handleSubmit } = useForm<IssueForm>();
    // 使用 router 进行页面跳转
+   const router = useRouter();

    return (
      {/* 将最外层 div 换为 form */}
+     <form className="max-w-xl space-y-3"
+       onSubmit={handleSubmit(async (data) => {
          {/* 使用 axios 进行 post */}
+         await axios.post("/api/issues", data);
+         router.push("/issues");
+       })}>
        <TextField.Root>
          {/* 将该组件注册为 form 中的 title 字段 */}
+         <TextField.Input placeholder="Title" {...register("title")} />
        </TextField.Root>
        {/* 由于 simpleMDE 不能直接像上面的 Input 一样传入参数,我们这里使用 React Hook Form 中的 Controller */}
-       <SimpleMDE placeholder="Description" />
+       <Controller
+         name="description"
+         control={control}
+         render={({ field }) => (
+           <SimpleMDE placeholder="Description" {...field} />
+         )}
+       />
        <Button>Submit New</Button>
+     </form>
    );
  };
  export default NewIssuePage;

完整代码(非 git diff 版)

# /app/issues/new/page.tsx

"use client";
import { Button, TextField } from "@radix-ui/themes";
import axios from "axios";
import "easymde/dist/easymde.min.css";
import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
import SimpleMDE from "react-simplemde-editor";

interface IssueForm {
  title: string;
  description: string;
}

const NewIssuePage = () => {
  const { register, control, handleSubmit } = useForm<IssueForm>();
  const router = useRouter();

  return (
    <form
      className="max-w-xl space-y-3"
      onSubmit={handleSubmit(async (data) => {
        await axios.post("/api/issues", data);
        router.push("/issues");
      })}
    >
      <TextField.Root>
        <TextField.Input placeholder="Title" {...register("title")} />
      </TextField.Root>
      <Controller
        name="description"
        control={control}
        render={({ field }) => (
          <SimpleMDE placeholder="Description" {...field} />
        )}
      />
      <Button>Submit New</Button>
    </form>
  );
};
export default NewIssuePage;

效果如下:

submit form

Handle Error​

本节代码链接

表单验证​

之前说到,我们使用 zod 进行表单验证,可以在使用 zod 时,自定义报错内容

# /app/api/issues/new/route.tsx

  ...
  const createIssueSchema = z.object({
    // 在定义时,可以加第二个参数,表示如果未满足该项时的报错
+   title: z.string().min(1, "Title is required!").max(255),
+   description: z.string().min(1, "Description is required!"),
  });

  export async function POST(request: NextRequest) {
    ...
    if (!validation.success)
    // 改为调用 validation.error.format()
-     return NextResponse.json(validation.error.errors, { status: 400 });
+     return NextResponse.json(validation.error.format(), { status: 400 });
    ...
  }

报错显示​

接下来实现一个这样的 Error Callout

Error Callout

在 /app/issues/new/page.tsx 中修改。把 axios 的相关内容放到一个 try-catch block 里

# /app/issues/new/page.tsx

  "use client";
  ...
  const NewIssuePage = () => {
    ...
    // 添加 useState 变量
+   const [error, setError] = useState("");

    return (
        ...
        {/*若报错则显示一个 CallOut*/}
+       {error && (
+         <Callout.Root color="red" className="mb-5">
+           <Callout.Text>{error}</Callout.Text>
+         </Callout.Root>
+       )}
        <form
          className="space-y-3"
          onSubmit={handleSubmit(async (data) => {
            // 报错时设置 error
+           try {
+             await axios.post("/api/issues", data);
+             router.push("/issues");
+           } catch (error) {
+             setError("An unexpected Error occured!");
+           }
          })}
        >
        ...
  };
  export default NewIssuePage;

用户端验证​

本节代码链接

Zod schema​

我们在用户端验证时,也需要用到刚刚 zod 中编辑的 schema,为此我们应该将其移动到一个单独的文件中。在 VS Code 中 可以方便的进行重构,将 createIssueSchema 移动到一个新文件中,并自动更新引用

首先右键想要重构的变量,点击 重构

Refactor 1

然后选择 move to a new file

Refactor 2

使用 Zod Schema 推断 interface​

将刚刚移出的 schema 移动到 /app 目录下,重命名为 validationSchema.ts

之前在 new page 中,我们定义了一个 interface,用于定义表单,但其实与我们在 zod 中定义的内容是重复的,如果我们之后还需要增删内容,需要在两边修改,较为麻烦。我们可以直接使用刚刚的 zod schema 来定义 interface ,如下所示

# /app/issues/new/page.tsx

+  import { createIssueSchema } from "@/app/validationSchema";
+  import { z } from "zod";
- interface IssueForm {
-   title: string;
-   description: string;
- }
+  type IssueForm = z.infer<typeof createIssueSchema>;

使用 hookform 集成 zod 验证表单​

安装 hookform/resolvers,用于将 React Hook Form 插件使用表单验证插件(比如 zod)

npm i @hookform/resolvers
# /app/issues/new/page.tsx
  
  "use client";
  ...
  // import
+ import { Button, Callout, Text, TextField } from "@radix-ui/themes";
+ import { zodResolver } from "@hookform/resolvers/zod";

  type IssueForm = z.infer<typeof createIssueSchema>;

  const NewIssuePage = () => {
    const {
      register,
      control,
      handleSubmit,
      // errors 则为验证结果
+     formState: { errors },
    } = useForm<IssueForm>({
      // 将 zodResoler 传入,以验证表单
+     resolver: zodResolver(createIssueSchema),
    });
    ...

    return (
      <div className="max-w-xl">
        ...
        <TextField.Root>
          <TextField.Input placeholder="Title" {...register("title")} />
        </TextField.Root>
        {/* 根据验证结果来显示提示,此处为 title 字段的信息 */}
+       {errors.title && (
+         <Text color="red" as="p">
+           {errors.title.message}
+         </Text>
+       )}
        <Controller
          name="description"
          control={control}
          render={({ field }) => (
            <SimpleMDE placeholder="Description" {...field} />
          )}
        />
        {/* 根据验证结果来显示提示,此处为 description 字段的信息 */}
+       {errors.description && (
+         <Text color="red" as="p">
+           {errors.description.message}
+         </Text>
+       )}
        ...
      </div>
    );
  };
  export default NewIssuePage;

最终效果如下:

Client Side Validation

将 ErrorMessage 封装​

# /app/components/ErrorMessage.tsx

import { Text } from "@radix-ui/themes";
import { PropsWithChildren } from "react";

const ErrorMessage = ({ children }: PropsWithChildren) => {
  if (!children) return null;
  return (
    <Text color="red" as="p">
      {children}
    </Text>
  );
};
export default ErrorMessage;

 然后我们可以在 new Page 中直接调用

# /app/issues/new/page.tsx

  "use client";
  ...
  // import
+ import ErrorMessage from "@/app/components/ErrorMessage";

    return (
      <div className="max-w-xl">
        ...
        {/* 根据验证结果来显示提示,此处为 title 字段的信息 */}
-       {errors.title && (
-         <Text color="red" as="p">
-           {errors.title.message}
-         </Text>
-       )}
+       <ErrorMessage>{errors.title?.message}</ErrorMessage>
        ...
-       {errors.description && (
-         <Text color="red" as="p">
-           {errors.description.message}
-         </Text>
-       )}
+        <ErrorMessage>{errors.description?.message}</ErrorMessage>
        ...
      </div>
    );
  };
  export default NewIssuePage;

Button 优化技巧​

本节代码链接

首先我们可以添加一个 Spinner 给 Button。其次,我们可以给 Button 添加一个 disabled 属性,使得其只能被点击一次,避免多次提交表单

Spinner 代码

# /app/issues/new/page.tsx

+ import Spinner from "@/app/components/Spinner";

  const NewIssuePage = () => {
+   const [isSubmitting, setSubmitting] = useState(false);

    return (
      <div className="max-w-xl">
        ...
        <form
          className="space-y-3"
          onSubmit={handleSubmit(async (data) => {
            try {
+             setSubmitting(true);
              await axios.post("/api/issues", data);
              router.push("/issues");
            } catch (error) {
+             setSubmitting(false);
              setError("An unexpected Error occured!");
            }
          })}
        >
          ...
+         <Button disabled={isSubmitting}>
+           Submit New Issue {isSubmitting && <Spinner />}
+         </Button>
        </form>
      </div>
    );
  };

最终版本​

本节代码链接

/app/issues/new/page.tsx

"use client";
import { Button, Callout, Text, TextField } from "@radix-ui/themes";
import axios from "axios";
import "easymde/dist/easymde.min.css";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import SimpleMDE from "react-simplemde-editor";
import { zodResolver } from "@hookform/resolvers/zod";
import { createIssueSchema } from "@/app/validationSchema";
import { z } from "zod";
import ErrorMessage from "@/app/components/ErrorMessage";

type IssueForm = z.infer<typeof createIssueSchema>;

const NewIssuePage = () => {
  const {
    register,
    control,
    handleSubmit,
    formState: { errors },
  } = useForm<IssueForm>({
    resolver: zodResolver(createIssueSchema),
  });
  const router = useRouter();
  const [error, setError] = useState("");

  return (
    <div className="max-w-xl">
      {error && (
        <Callout.Root color="red" className="mb-5">
          <Callout.Text>{error}</Callout.Text>
        </Callout.Root>
      )}
      <form
        className="space-y-3"
        onSubmit={handleSubmit(async (data) => {
          try {
            await axios.post("/api/issues", data);
            router.push("/issues");
          } catch (error) {
            setError("An unexpected Error occured!");
          }
        })}
      >
        <TextField.Root>
          <TextField.Input placeholder="Title" {...register("title")} />
        </TextField.Root>
        <ErrorMessage>{errors.title?.message}</ErrorMessage>
        <Controller
          name="description"
          control={control}
          render={({ field }) => (
            <SimpleMDE placeholder="Description" {...field} />
          )}
        />
        <ErrorMessage>{errors.description?.message}</ErrorMessage>
        <Button>Submit New</Button>
      </form>
    </div>
  );
};
export default NewIssuePage;

CSDN 的排版/样式可能有问题,去我的博客查看原文系列吧,觉得有用的话,给我的库点个star,关注一下吧 

下一篇讲查看 Issue

下一篇【Next.js 项目实战系列】03-查看 Issue

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/899121.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】【xmake】安装 + C/C++常用项目配置

文章目录 0. 环境准备1. 子命令create - 快速创建项目build - 构建程序config - 配置编译需要的参数show - 查看当前工程基本信息update - 程序自更新 2. C/C 项目常用配置2.1 项目目标类型2.2 添加宏定义2.3 头文件路径和链接库配置2.4 设置语言标准2.5 设置编译优化2.6 添加源…

《YOLO 目标检测》—— YOLO v3 详细介绍

&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;还未写完&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xf…

vscode插件live server无法在手机预览调试H5网页

环境 Window10、vscode&#xff1a;1.94.2、Live Server&#xff1a;v5.7.9、Live Server (Five Server)&#xff1a;v0.3.1 问题 PC端预览没有问题&#xff0c;但是在手机点击链接显示访问失败 排查 1. 是否同一局域网 意思就是电脑、手机是不是访问同一个网络。电脑插得…

【设计模式-原型】

**原型模式&#xff08;Prototype Pattern&#xff09;**是一种创建型设计模式&#xff0c;旨在通过复制现有对象的方式来创建新对象&#xff0c;而不是通过实例化类来创建对象。该模式允许对象通过克隆&#xff08;复制&#xff09;来创建新的实例&#xff0c;因此避免了重新创…

Git核心概念图例与最常用内容操作(reset、diff、restore、stash、reflog、cherry-pick)

文章目录 简介前置概念.git目录objects目录refs目录HEAD文件 resetreflog 与 reset --hardrevert(撤销指定提交)stashdiff工作区与暂存区差异暂存区与HEAD差异工作区与HEAD差异其他比较 restore、checkout(代码撤回)merge、rebase、cherry-pick 简介 本文将介绍Git几个核心概念…

赛氪提供专业技术支持,首届“天翼云息壤杯”高校AI大赛正式开启

2024年9月25日&#xff0c;在ICT中国2024高层论坛暨国际信息通信展主论坛上&#xff0c;首届“天翼云息壤杯”高校AI大赛正式拉开帷幕。中国电信总经理梁宝俊出席并发表了致辞。此次大赛由国务院国资委、工业和信息化部、教育部等部委指导&#xff0c;中国电信集团有限公司和华…

【排序】快排思想以及例子

思想 使用分治法来处理数据 例题 19 97 09 17 01 08 首先确定一个pivot 一般是首位&#xff0c;把比p小的放p的左边&#xff0c;比p大的放p的右边。L是左指 R是右指 首轮排序 p 19 __ 97 09 17 01 08 L R 首先应从R开始判断 08<19 08替换到p所在位置&#xff0c;R移动 p 19…

【AIGC】AI时代降临,AI文案写作、AI绘画、AI数据处理

目录 1、ChatGPTAI文案与写作108招2、AI短视频生成与剪辑实战108招3、AI绘画与摄影实战108招4、AI商业广告设计实战108招5、AI数据处理实战108招6、AI智能办公实战108招 传送门&#xff1a;清华大学出版社AI实战108招 全6册 1、ChatGPTAI文案与写作108招 《ChatGPTAI文案与写…

DDD重构-实体与限界上下文重构

DDD重构-实体与限界上下文重构 概述 DDD 方法需要不同类型的类元素&#xff0c;例如实体或值对象&#xff0c;并且几乎所有这些类元素都可以看作是常规的 Java 类。它们的总体结构是 Name: 类的唯一名称 Properties&#xff1a;属性 Methods: 控制变量的变化和添加行为 一…

MySQL 基础查询

1、DISTINCT select DISTINCT EMPLOYEE_ID ,FIRST_NAME from employees 按照ID去重&#xff0c;DISTINCT的字段要放在前面&#xff0c;不会再继续在FIRST_NAME上去重判断&#xff1b; 如果需要多字段去重&#xff0c;需要用到group by&#xff0c;这个后面讲&#xff1b; …

Redis-04 Redis管道

Redis 管道&#xff08;Pipelining&#xff09;是一种技术&#xff0c;它允许客户端一次发送多个命令给服务器&#xff0c;而无需等待每个命令的响应。这样可以减少网络延迟和提高命令的执行效率。 创建txt文件&#xff0c;里面写需要执行的操作&#xff0c;然后使用cat命令一次…

【C++打怪之路Lv11】-- stack、queue和优先级队列

&#x1f308; 个人主页&#xff1a;白子寰 &#x1f525; 分类专栏&#xff1a;重生之我在学Linux&#xff0c;C打怪之路&#xff0c;python从入门到精通&#xff0c;数据结构&#xff0c;C语言&#xff0c;C语言题集&#x1f448; 希望得到您的订阅和支持~ &#x1f4a1; 坚持…

基于SSM+微信小程序的家庭记账本管理系统(家庭1)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 1、管理员端功能有首页、个人中心、用户管理&#xff0c;消费详情管理、收入详情管理、系统管理等。 2、用户端功能有首页、消费详情、收入详情、论坛信息、我的等功能。 2、项目技术 …

C语言教程——数组(1)

目录 系列文章目录 前言 1、一维数组的创建和初始化 1.1数组的创建 1.2数组的初始化 1.3一维数组的使用 总结 1.4一维数组在内存中的存储 2、二维数组的创建和初始化 2.1二维数组的创建 2.2二维数组的初始化 2.3二维数组的使用 2.4二维数组在内存中的存储 3、数组…

时空智友企业流程化管控系统uploadStudioFile接口存在任意文件上传漏洞

免责声明&#xff1a;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该文章仅供学习用途使用。 1. 时空智友…

【Vulnhub靶场】Kioptrix Level 3

目标 本机IP&#xff1a;192.168.118.128 目标IP&#xff1a;192.168.118.0/24 信息收集 常规 nmap 扫存活主机&#xff0c;扫端口 根据靶机IP容易得出靶机IP为 192.168.118.133 nmap -sP 192.168.118.0/24nmap -p- 192.168.118.133 Getshell 开放22端口和80 端口 访问web…

C++进阶之路:日期类的实现、const成员(类与对象_中篇)

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

如何选择合适的云服务商,看IDC怎么说

01 | 2份IDC报告&#xff0c;评估了Covid后的云增长与云服务市场的变化 《IDC MarketScape&#xff1a;全球公有云基础设施即服务提供商评估》&#xff08;2022&#xff09;共评估了 13 家云提供商&#xff0c;其中既有超大规模云服务商&#xff0c;如亚马逊云科技&#xff1b…

论文阅读-三维结构几何修复(导-4)

摘要 解决了3D数字模型中大洞修复的问题。 通过基于字典学习的方法解决了缺失区域推断问题&#xff0c;该方法利用从单个自相似结构和在线深度数据库中得出的几何先验。利用几何先验提供的线索&#xff0c;从洞的边界周围自适应地传播局部3D表面平滑性来恢复底层表面。在合成…

word中的内容旋转90度

在vsto、Aspose.Words 中&#xff0c;默认没有直接的 API 可以让表格整体旋转 90 度。然而&#xff0c;我们可以通过一些方式来实现类似的效果&#xff0c;具体思路如下&#xff1a; 将表格插入到一个形状&#xff08;Shape&#xff09;或文本框中&#xff0c;然后旋转该形状。…