Next.js, Sanity 博客项目:如何实现评论功能?

416 阅读2分钟

前言

评论区/表单功能逻辑一般包括:输入校验 > 提交 > 提交成功提示 > 后台审核 > 前端展示。

本文将分享 React Hook Forms, Next.js 实现评论功能的步骤与方法。

效果图

image.png

技术栈

前端:React, Next.js, Tailwind CSS , TypeScript

后端-CMS:Sanity

工具安装

  • 工具参考:React Hook Forms- react-hook-form.com/
  • 根目录安装:npm install react-hook-form

评论区表格:样式

{/* Comment Form */}
<hr className="max-w-lg my-5 mx-auto border border-yellow-500" />
<form className="flex flex-col p-5 max-w-2xl mx-auto mb-10">
  <h3 className="text-sm text-yellow-500">Enjoyed this article?</h3>
  <h4 className="text-3xl font-bold">Leave a comment below</h4>
  <hr className="py-3 mt-2" />
  <label className="block mb-5">
    <span className="text-grey-700">Name</span>
    <input
      className="shadow border rounded py-2 px-3 form-input mt-1 block w-full ring-yellow-500 outline-none focus:ring"
      placeholder="John"
      type="text"
    />
  </label>
  <label className="block mb-5">
    <span className="text-grey-700">Email</span>
    <input
      className="shadow border rounded py-2 px-3 form-input mt-1 block w-full ring-yellow-500 outline-none focus:ring"
      placeholder="John"
      type="text"
    />
  </label>
  <label className="block mb-5">
    <span className="text-grey-700">Comment</span>
    <textarea
      className="shadow border rounded py-2 px-3 form-textarea mt-1 block w-full ring-yellow-500 outline-none focus:ring"
      placeholder="John"
      rows={8}
    />
  </label>
</form>

输入校验功能

打开 [slug].tsx, 加入 react-hook-form 组件

import { useForm, SubmitHandler } from "react-hook-form";

定义表格域数据类型(TypeScript)

interface IFormInput {
	_id: string;
	name: string;
	email: string;
	comment: string;
}

function Post({ post }: Props) {
	// Use react hook form
	const {
		register,
		handleSubmit,
		formState: { errors },
	} = useForm<IFormInput>(); // Form will know it can only have the defined type
}

将表格与 react-hook-form 连接,实现:输入不合法提示

  • 在 input 标签中添加{...register("<域>"), {required: true}}
{/* Connect to React Hook Form, enable easy data pull */}
<input {...register("_id")} type="hidden" name="_id" value={post._id} />
<label className="">
  <span className="">Name</span>
  <input
    {...register("name", { required: true })}
  />
</label>
{/* errors will return when field validation fails  */}
<div className="flex flex-col p-5">
  {errors.name && (
    <span className="text-red-500">The Name Field is required</span>
  )}
</div>

评论提交功能

设置表格提交按钮功能

{/* 表格提交功能 */}
<form
    onSubmit={handleSubmit(onSubmit)}
    className=""
>
  <input
      type="submit"
      className="shadow bg-yellow-500 hover:bg-yellow-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded cursor-pointer"
  />
</form>

获取表格提交数据,将数据推送至API后台

function Post({ post }: Props) {
	// Get Form Submit Data
	const onSubmit: SubmitHandler<IFormInput> = async (data) => {
		await fetch("/api/createComment", {
			method: "POST",
			body: JSON.stringify(data),
		})
			.then(() => {
				console.log(data);
			})
			.catch((err) => {
				console.log(err);
			});
	};
}

打开 /api > 新建 createComment.ts

    • 根目录安装:npm i @sanity/client
import type { NextApiRequest, NextApiResponse } from "next";
import sanityClient from "@sanity/client";

const config = {
	dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
	projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
	useCdn: process.env.NODE_ENV === "production",
	token: process.env.SANITY_API_TOKEN,
};
const client = sanityClient(config);

获取 TOKEN,得到API数据库读写权限

  • sanity.io > log in> API > Add API token
  • Permission> Editor > Save
  • 将TOKEN 复制到 /.env.local 文件:SANITY_API_TOKEN=...
  • 重启 Next.js Server:npm run dev

将前端数据传送至后台

export default async function createComment(
	req: NextApiRequest,
	res: NextApiResponse
) {
	const { _id, name, email, comment } = JSON.parse(req.body);
	try {
		await client.create({
			_type: "comment",
			post: {
				_type: "reference",
				_ref: _id,
			},
			name,
			email,
			comment,
		});
	} catch (err) {
		return res.status(500).json({ message: `Couldn't submit comment`, err });
	}
	console.log("Comment submitted");
	res.status(200).json({ message: "Comment submitted" });
}

调整后台表单条目> /schemas > 新建 comment.js> 后台网址查看

  • 表单报错:Parsing error: Cannot find module '@babel/preset-env'
export default {
	name: "comment",
	title: "Comment",
	type: "document",
	fields: [
		{
			name: "name",
			type: "string",
		},
		{
			title: "Approved",
			name: "approved",
			type: "boolean",
			decription: "Comments won't show on the site without approval",
		},
		{
			name: "email",
			type: "string",
		},
		{
			name: "comment",
			type: "text",
		},
		{
			name: "post",
			type: "reference",
			to: [{ type: "post" }],
		},
	],
};

前端提交成功提示>获取后台审核过的评论

提交成功状态提示

  • 运用React:useState
import { useState } from "react";

// To check submit state
const [submitted, setSubmitted] = useState(false);
const onSubmit: SubmitHandler<IFormInput> = async (data) => {
		await fetch("/api/createComment", {
			method: "POST",
			body: JSON.stringify(data),
		})
			.then(() => {
				setSubmitted(true);
			})
			.catch((err) => {
				setSubmitted(false);
			});
	};

// To display submit success message 
{submitted? (<成功界面>):(<未提交界面>)}

前端获取后台审核后的评论

  • 打开 /typings.d.ts,定义comment类型
export interface Post {
  comments: Comment[];
}
export interface Comment {
	approved: boolean;
	comment: string;
	email: string;
	name: string;
	post: {
		_ref: string;
		_type: string;
	};
	_createdAt: string;
	_id: string;
	_rev: string;
	_type: string;
	_updatedAt: string;
}
  • 更新 getStaticProps,添加 comment
export const getStaticProps: GetStaticProps = async ({ params }) => {
	const query = `*[_type =="post" && slug.current == $slug][0]{
    _id,
    _createdAt,
    title,
    author->{
    name,
    image
  },
	'comment': *[
		_type== "comment" && 
		post._ref ==^._id && 
		approved == true],
  description,
  mainImage,
  slug,
  body
  }`;
}

前端展示评论

{/* Comments */}
<div className="flex flex-col p-10 my-10 max-w-2xl mx-auto shadow-yellow-500 shadow space-y-2">
  <h3 className="text-4xl">Comments</h3>
  <hr className="pb-2" />
  {post.comments.map((comment) => (
    <div key={comment._id}>
      <p>
        <span className="text-yellow-500">{comment.name}: </span>
        {comment.comment}
      </p>
    </div>
  ))}
</div>