ホームページ > ウェブフロントエンド > jsチュートリアル > Next.js を使用した動的なブログ ダッシュボードの構築

Next.js を使用した動的なブログ ダッシュボードの構築

Barbara Streisand
リリース: 2024-12-08 17:04:10
オリジナル
692 人が閲覧しました

導入

こんにちは、お元気ですか?こちらは Vítor です。プログラミング スキルの向上に役立つ新しいプロジェクトを携えて戻ってきました。最後にチュートリアルを公開してからしばらく時間が経ちました。過去数か月間、私は休息を取り、他の活動に集中する時間を取りました。この期間中に、私は小さな Web プロジェクト、つまりこのチュートリアルの焦点となるブログを開発しました。

このガイドでは、Markdown をレンダリングできるブログ ページのフロントエンドを作成します。このアプリケーションには、パブリックおよびプライベート ルート、ユーザー認証、Markdown テキストの作成、写真の追加、記事の表示などの機能が含まれます。

お好みに合わせてアプリケーションを自由にカスタマイズしてください。私はそれをお勧めします。

このアプリケーションのリポジトリにはここからアクセスできます:

Building a Dynamic Blog Dashboard with Next.js ゴンドラク08 / ブログプラットフォーム

Next.js/typescript で作られたブログ プラットフォーム。

Platforma パラブログ

  • テキストによるチュートリアル

材料

  • next-auth - Next.js
  • の認証文書
  • github.com/markdown-it/markdown-it - マークダウン図書館。
  • github.com/sindresorhus/github-markdown-css- 最も重要なエディターのマークダウン。
  • github.com/remarkjs/react-markdown - レンダリング マークダウンとそのコンポーネントの反応に関する参考文献。
  • github.com/remarkjs/remark-react/tree/4722bdf - React の Markdown を変換するプラグイン
  • codemirror.net - ウェブ用エディターコンポーネント
  • 反応アイコン - 反応するアイコンのライブラリ

コモ・ユーザー

npm i
npm run start
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

サーバー

サーバーアプリケーションのサーバーを起動します


GitHub で表示


このチュートリアルには、このガイドで使用される Node.js サーバーの作成も含まれています。

楽しんでいただければ幸いです。

コーディングを楽しんでください!

図書館

このプロジェクトで使用されるライブラリの概要は次のとおりです:

  • next-auth - Next.js
  • の認証ライブラリ
  • github.com/markdown-it/markdown-it - マークダウン ライブラリ。
  • github.com/sindresorhus/github-markdown-css - Markdown エディターのスタイル設定用。
  • github.com/remarkjs/react-markdown - React コンポーネントで Markdown をレンダリングするためのライブラリ。
  • github.com/remarkjs/remark-react/tree/4722bdf - Markdown を React に変換するプラグイン
  • codemirror.net - Web コンポーネント エディター。
  • act-icons - React 用のアイコン ライブラリ

React プロジェクトの作成

Next.js フレームワークの最新バージョンを使用します。このチュートリアルの作成時点ではバージョン 13.4 です。

次のコマンドを実行してプロジェクトを作成します:

npm i
npm run start
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

インストール中に、テンプレート設定を選択します。このチュートリアルでは、プログラミング言語として TypeScript を使用し、アプリケーションのスタイル設定に Tailwind CSS フレームワークを使用します。

構成

次に、使用するすべてのライブラリをインストールしましょう。

マークダウン
npx create-next-app myblog
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
リアクションコメント
npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
コードミラー
remark remark-gfm remark-react
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
アイコン
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

次に、使用しないものをすべて削除して、インストールの初期構造をクリーンアップします。

建築

これがアプリケーションの最終的な構造です。

npm i react-icons @types/react-icons
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

最初のステップ

next.config の構成

プロジェクトのルートのファイル next.config.js で、記事の画像にアクセスするドメイン アドレスを設定しましょう。このチュートリアルでは、またはローカル サーバーを使用している場合は、localhost を使用します。

アプリケーションに画像が正しく読み込まれるように、必ずこの構成を含めてください。

src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

ミドルウェアの構成

アプリケーション src/ のルート フォルダーに、プライベート ルートへのアクセスを確認するための middleware.ts を作成します。

const nextConfig = {
   images: {
    domains: ["localhost"],
  },
};
ログイン後にコピー
ログイン後にコピー

ミドルウェアとそれを使用して実行できるすべてのことについて詳しくは、ドキュメントを確認してください。

認証ルートの構成

/app フォルダー内の api/auth/[...nextauth] に、route.ts という名前のファイルを作成します。これには、CredentialsProvider を使用して認証 API に接続するルートの構成が含まれます。

CredentialsProvider を使用すると、ユーザー名とパスワード、ドメイン、2 要素認証、ハードウェア デバイスなどの任意の資格情報を使用してログインを処理できます。

まず、プロジェクトのルートに .env.local ファイルを作成し、シークレットとして使用されるトークンを追加します。

npm i
npm run start
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

次に、認証システムを作成しましょう。この NEXTAUTH_SECRET が src/app/auth/[...nextauth]/routes.ts ファイル内のシークレットに追加されます。

npx create-next-app myblog
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

認証プロバイダー

プライベート ルートのページ間でユーザーのデータを共有する認証プロバイダー、つまりコンテキストを作成しましょう。後でこれを使用して、layout.tsx ファイルの 1 つをラップします。

次の内容を含むファイルを src/context/auth-provider.tsx に作成します。

npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

グローバルスタイル

全体として、私たちのアプリケーションでは、Tailwind CSS を使用してスタイルを作成します。ただし、一部の場所では、ページとコンポーネント間でカスタム CSS クラスを共有します。

remark remark-gfm remark-react
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

レイアウト

次に、プライベートとパブリックの両方のレイアウトを書きましょう。

app/layout.tsx

npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

ページ/レイアウト.tsx

npm i react-icons @types/react-icons
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

API呼び出し

私たちのアプリケーションは API を複数回呼び出します。このアプリケーションを任意の外部 API を使用するように適応させることができます。この例では、ローカル アプリケーションを使用しています。バックエンドのチュートリアルとサーバーの作成をまだ見ていない場合は、チェックしてください。

src/services/ に次の関数を書きましょう:

  1. authService.ts: サーバー上でユーザーを認証する機能。
src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

2.refreshAccessToken.tsx:

const nextConfig = {
   images: {
    domains: ["localhost"],
  },
};
ログイン後にコピー
ログイン後にコピー
  1. getArticles.tsx: データベースに保存されているすべての記事を取得する関数:
export { default } from "next-auth/middleware";
export const config = {
  matcher: ["/", "/newArticle/", "/article/", "/article/:path*"],
};
ログイン後にコピー
  1. postArticle.tsx: 記事データをサーバーに送信する機能。
.env.local
NEXTAUTH_SECRET = SubsTituaPorToken
ログイン後にコピー
  1. editArticle.tsx: データベース内の特定の記事を変更する関数。
import NextAuth from "next-auth/next";
import type { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { authenticate } from "@/services/authService";
import refreshAccessToken from "@/services/refreshAccessToken";

export const authOptions: AuthOptions = {
  providers: [
    CredentialsProvider({
      name: "credentials",
      credentials: {
        email: {
          name: "email",
          label: "email",
          type: "email",
          placeholder: "Email",
        },
        password: {
          name: "password",
          label: "password",
          type: "password",
          placeholder: "Password",
        },
      },
      async authorize(credentials, req) {
        if (typeof credentials !== "undefined") {
          const res = await authenticate({
            email: credentials.email,
            password: credentials.password,
          });
          if (typeof res !== "undefined") {
            return { ...res };
          } else {
            return null;
          }
        } else {
          return null;
        }
      },
    }),
  ],

  session: { strategy: "jwt" },
  secret: process.env.NEXTAUTH_SECRET,
  callbacks: {
    async jwt({ token, user, account }: any) {
      if (user && account) {
        return {
          token: user?.token,
          accessTokenExpires: Date.now() + parseInt(user?.expiresIn, 10),
          refreshToken: user?.tokenRefresh,
        };
      }

      if (Date.now() < token.accessTokenExpires) {
        return token;
      } else {
        const refreshedToken = await refreshAccessToken(token.refreshToken);
        return {
          ...token,
          token: refreshedToken.token,
          refreshToken: refreshedToken.tokenRefresh,
          accessTokenExpires:
            Date.now() + parseInt(refreshedToken.expiresIn, 10),
        };
      }
    },
    async session({ session, token }) {
      session.user = token;
      return session;
    },
  },

  pages: {
    signIn: "/login",
    signOut: "/login",
  },
};

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
ログイン後にコピー
  1. deleteArticle.tsx: データベースから特定の記事を削除する関数。
'use client';
import React from 'react';
import { SessionProvider } from "next-auth/react";
export default function Provider({
    children,
    session
}: {
    children: React.ReactNode,
    session: any
}): React.ReactNode {
    return (
        <SessionProvider session={session} >
            {children}
        </SessionProvider>
    )
};
ログイン後にコピー

コンポーネント

次に、アプリケーション全体で使用される各コンポーネントを記述しましょう。

コンポーネント/Navbar.tsx

2 つのナビゲーション リンクを持つ単純なコンポーネント。

/*global.css*/
.container {
  max-width: 1100px;
  width: 100%;
  margin: 0px auto;
}

.image-container {
  position: relative;
  width: 100%;
  height: 5em;
  padding-top: 56.25%; /* Aspect ratio 16:9 (dividindo a altura pela largura) */
}

.image-container img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@keyframes spinner {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.loading-spinner {
  width: 50px;
  height: 50px;
  border: 10px solid #f3f3f3;
  border-top: 10px solid #293d71;
  border-radius: 50%;
  animation: spinner 1.5s linear infinite;
}
ログイン後にコピー

コンポーネント/Loading.tsx

API 呼び出しが完了するのを待機している間に使用される、単純な読み込みコンポーネント。

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Provider from "@/context/auth-provider";
import { getServerSession } from "next-auth";
import { authOptions } from "./api/auth/[...nextauth]/route";
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Markdown Text Editor",
  description: "Created by <@vitorAlecrim>",
};

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await getServerSession(authOptions);
  return (
    <Provider session={session}>
      <html lang="en">
        <body className={inter.className}>{children}</body>
      </html>
    </Provider>
  );
}
ログイン後にコピー

コンポーネント/ページネーション.tsx

プライベート ルートで、すべての記事を表示するページで使用されるページネーション コンポーネント。このコンポーネントの作成方法に関する詳細な記事は、こちらでご覧いただけます

npm i
npm run start
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

コンポーネント/ArticleCard.tsx

書かれた記事を表示するためのカードコンポーネント。

このコンポーネントには、記事表示ページと以前に書いた記事を編集するページの両方にアクセスするリンクも含まれています。

npx create-next-app myblog
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

コンポーネント/ArticleList.tsx

API 呼び出しを実行し、応答を表示するコンポーネント。

ここでは、作成した関数を通じて 2 つの API 呼び出しを使用します。

  1. getArticles.ts - コンポーネントに表示されるすべての記事を返します。
  2. RemoveArticle - リストとサーバーから特定の記事を削除します。

以前に作成した Pagination.tsx コンポーネントを使用して、記事の数をページ間で分割します。

npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

ページ

次に、それぞれのルートごとに分けて、各ページを見ていきます。

公開ページ

ログイン

これは私たちのアプリケーションのホームページです。シンプルなページなので、必要に応じて変更してください。このページでは、next-auth ナビゲーション ライブラリが提供するサインイン機能を使用します。

ファイル src/app/pages/public/login/page.tsx.

remark remark-gfm remark-react
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

記事ページ

記事閲覧ページを作成するために、動的ページを開発します。

あなたがアクセスしたことのあるすべてのブログ プラットフォームには、記事を読むための専用ページがあり、URL からアクセスできます。この理由は、動的なページ ルートです。幸いなことに、Next.js の新しい AppRouter メソッドでこれが簡単になり、私たちの生活がはるかにシンプルになりました。

最初に: [id] フォルダーを追加して、構造内にルートを作成する必要があります。これにより、pages/(public)/articles/[id]/pages.tsx.

という構造になります。
  • ID はナビゲーション ルートのスラッグに対応します。
  • params は、ナビゲーション スラッグを含むアプリケーションのツリーを介して渡されるプロパティです。
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

2 番目: MarkdownIt ライブラリを使用して、ページに Markdown 形式でテキストを表示できるようにします。

npm i react-icons @types/react-icons
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

そして最後に、

ページの準備ができたら、たとえばブラウザで localhost:3000/articles/1 にアクセスすると、指定された ID で記事を表示できるようになります。

この場合、ArticleCards.tsx コンポーネントのいずれかをクリックすると、ID がナビゲーションを介して渡され、プライベート ルートのメイン ページにレンダリングされます。

src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

プライベートページ

ここにプライベート ページがあります。このページには、ユーザーがアプリケーションで認証された場合にのみアクセスできます。

app/pages/ フォルダー内で、ファイルが () 内で宣言されている場合、ルートが /.

に対応することを意味します。

この場合、(ホーム) フォルダーはプライベート ルートのホームページを指します。これは、ユーザーがシステムに認証されたときに最初に表示されるページです。このページには、データベースの記事のリストが表示されます。

データは ArticlesList.tsx コンポーネントによって処理されます。このコードをまだ書いていない場合は、コンポーネントのセクションに戻って参照してください。

app/(pages)/(private)/(home)/page.tsx 内

npm i
npm run start
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

新しい記事

これは、記事を登録できるため、アプリケーションの最も重要なページの 1 つです。

このページにより、ユーザーは次のことが可能になります:

  1. Markdown 形式で記事を書きます。
  2. 記事に画像を割り当てます。
  3. サーバーに送信する前に、Markdown テキストをプレビューします。

このページでは複数の フック が使用されています:

  1. useCallback - 関数をメモ化するために使用されます。
  2. useState - コンポーネントに状態変数を追加できます。
  3. useSession - ユーザーが認証されているかどうかを確認し、認証トークンを取得します。

このために、2 つのコンポーネントを使用します:

  1. TextEditor.tsx: 以前に作成したテキスト エディター。
  2. Preview.tsx: Markdown 形式でファイルを表示するためのコンポーネント。

このページを構築する際、次の API を使用します:

  1. POST: 関数 postArticle を使用して、記事をサーバーに送信します。

また、next-auth ライブラリによって提供される useSession フックを使用して、サーバーに記事を登録するために使用されるユーザーの認証トークンを取得します。

これには 3 つの異なる API 呼び出しが含まれます。
app/pages/(private)/newArticle/page.tsx.

「クライアントを使用する」;
import React, { ChangeEvent, useCallback, useState } from "react";
import { useSession } から "next-auth/react";
import { リダイレクト } から "next/navigation";
"@/services/postArticle" から postArtical をインポートします。
import { AiOutlineFolderOpen } from "react-icons/ai";
import { RiImageEditLine } から "react-icons/ri";

「next/image」から画像をインポートします。
"@/components/textEditor" から TextEditor をインポートします。
"@/components/PreviewText" からプレビューをインポートします。
import { AiOutlineSend } から "react-icons/ai";
import { BsBodyText } から "react-icons/bs";

デフォルト関数をエクスポート NewArticle(params:any) {
  const { データ: セッション }: any = useSession({
    必須: true、
    onUnauthenticated() {
      リダイレクト("/ログイン");
    }、
  });
  const [imageUrl, setImageUrl] = useState<object>({});
  const [previewImage, setPreviewImage] = useState<string>("");
  const [previewText, setPreviewText] = useState<boolean>(false);
  const [title, setTitle] = useState<string>("");
  const [doc, setDoc] = useState<string>("# Escreva o seu texto... n");
  const handleDocChange = useCallback((newDoc: any) => {
    setDoc(newDoc);
  }、[]);

  (!session?.user) が null を返す場合。

  const handleArticleSubmit = async (e:any) => {
        e.preventDefault();
    const トークン: 文字列 = session.user.token;
    試す {
      const res = await postArtical({
        id: session.user.userId.toString()、
        トークン: トークン、
        画像 URL: 画像 URL、
        タイトル: "タイトル"、
        ドキュメント: ドキュメント、
      });
      console.log('re--->', res);
      リダイレクト('/成功');
    } キャッチ (エラー) {
      console.error('記事の送信エラー:', error);
      // 必要に応じてエラーを処理します
      エラーをスローします。
    }
  };

  const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files && e.target.files.length > 0) {
      const ファイル = e.target.files[0];
      const url = URL.createObjectURL(ファイル);
      setPreviewImage(url);
      setImageUrl(ファイル);
    }
  };

  const handleTextPreview = (e: any) => {
    e.preventDefault();
    setPreviewText(!previewText);
  };
  戻る (
    <section className="w-full h-full min-h-screen相対 py-8">
      {プレビューテキスト && (
        <div className="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100rounded-xl w-full max-w-[33em] z-30">
          <プレビュー
            ドキュメント={ドキュメント}
            タイトル={タイトル}
            プレビュー画像={プレビュー画像}
            onPreview={() => setPreviewText(!previewText)}
          />
        </div>
      )}

      <form className="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200rounded-md bg-slate-50ドロップシャドウ-xl フレックス フレックスコル ギャップ-2 ">
        {" "}
        <div className="flex justify-between items-center">
          <ボタン
            className="border-b-2rounded-md border-slate-500 p-2 flex items-center gap-2 hover:border-slate-400 hover:text-slate-800"
            onClick={handleTextPreview}
          >
            <BsBodyText />
            プレビュー
          </ボタン>{" "}
          <ボタン
            className="グループ ボーダー ボーダー-b-2 ボーダー-スレート-500 ラウンド-MD p-2 フレックス アイテム-センター ギャップ-2 hover:border-slate-400 hover:text-slate-800 "
            onClick={handleArticleSubmit}
          >
            エンビア テキスト
            <AiOutlineSend className="w-5 h-5 group-hover:text-red-500" />
          </ボタン>
        </div>
        <div className="header-wrapper flex flex-col gap-2 ">
          <div className="画像ボックス">
            {previewImage.length === 0 && (
              <div className="選択画像">
                
                  <AiOutlineFolderOpen className="w-7 h-7" />
                  ドラッグ アンド ドロップ画像
                </ラベル>
                



<h4>
  
  
  記事を編集する
</h4>

<p><em>新しい記事</em> (newArticle) に似たページですが、いくつかの違いがあります。</p>

<p>まず、ナビゲーション パラメーターとして ID を受け取る動的ルートを定義します。これは、記事閲覧ページで行われたことと非常によく似ています。 <br>
app/(pages)/(private)/editArticle/[id]/page.tsx<br>
</p><pre class="brush:php;toolbar:false">「クライアントを使用する」;
import React, { useState, useEffect, useCallback, useRef, ChangeEvent } from "react";
import { useSession } から "next-auth/react";
import { リダイレクト } から "next/navigation";
'next/image' から画像をインポートします。

import { IArticle } から "@/interfaces/article.interface";
import { AiOutlineEdit } から "react-icons/ai";
import { BsBodyText } から "react-icons/bs";
import { AiOutlineFolderOpen } from "react-icons/ai";
import { RiImageEditLine } から "react-icons/ri";

"@/components/PreviewText" からプレビューをインポートします。
"@/components/textEditor" から TextEditor をインポートします。
import Loading from '@/components/Loading';
editArtical を "@/services/editArticle" からインポートします。

デフォルト関数をエクスポート EditArticle({ params }: { params: any }) {
 const { データ: セッション }: any = useSession({
    必須: true、
    onUnauthenticated() {
      リダイレクト("/ログイン");
    }、
  });
  定数 ID: 数値 = params.id;
  const [article, setArticle] = useState<iarticle null>(null);
  const [imageUrl, setImageUrl] = useState<object>({});
  const [previewImage, setPreviewImage] = useState<string>("");
  const [previewText, setPreviewText] = useState<boolean>(false)
  const [title, setTitle] = useState<string>("");
  const [doc, setDoc] = useState<string>('');
  const handleDocChange = useCallback((newDoc: any) => {
    setDoc(newDoc);
  }、[]);
  const inputRef= useRef<htmlinputelement>(null);

  const fetchArticle = async (id:number) => {
    試す {
      const 応答 = fetch を待ちます(
        `http://localhost:8080/articles/getById/${id}`、
      );
      const jsonData = 応答を待ちます.json();
      setArticle(jsonData);
    } キャッチ (エラー) {
      console.log("何か問題が発生しました:", err);
    }
  };
  useEffect(() => {
    if (記事 !== null || 記事 !== 未定義) {
      fetchArticle(id);
    }
  }, [id]);

  useEffect(()=>{
    if(記事 != null && 記事.コンテンツ){
        setDoc(article.content)
    }

    if(記事 !=null && 記事.画像){
      setPreviewImage(`http://localhost:8080/`article.image)
    }
  }、[記事])

  const handleArticleSubmit = async (e:any) => {
     e.preventDefault();
    const トークン: 文字列 = session.user.token;
    試す{
      const res = await editArtical({
      ID: ID、
      トークン: トークン、
      画像URL:画像URL、
      タイトル: タイトル、
      ドキュメント: ドキュメント、
      });
        console.log('re--->',res)
        応答を返します。
    キャッチ(エラー){
    console.log("エラー:", エラー)
    }
  };
  const handleImageClick = ()=>{
      console.log('hiii')
    if(inputRef.current){
      inputRef.current.click();
    }
  }const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files && e.target.files.length > 0) {
      const ファイル = e.target.files[0];
      const url = URL.createObjectURL(ファイル);
      setPreviewImage(url);
      setImageUrl(ファイル);
    }

  };
   const handleTextPreview = (e: any) => {
    e.preventDefault();
    setPreviewText(!previewText);
    console.log('プレビューからこんにちは!')
  };

  if(!article) return <Loading/>
  if(記事?.コンテンツ)
  戻る (
    <section className='w-full h-full min-h-screen相対 py-8'>
      {プレビューテキスト && (
        <div className="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100rounded-xl w-full max-w-[33em] z-30">
          <プレビュー
            ドキュメント={ドキュメント}
            タイトル={タイトル}
            プレビュー画像={プレビュー画像}
            onPreview={() => setPreviewText(!previewText)}
          />
        </div>
      )}

      <div className='relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200rounded-md bg-whitedrop-シャドウ-MD フレックス フレックス-コル ギャップ-2'>
        <form className='relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200rounded-md bg-slate-50ドロップシャドウ-MD フレックス フレックスコル ギャップ-2 '>
          {" "}
          <div className='flex justify-between items-center'>
            <ボタン
              className='border-b-2rounded-md border-slate-500 p-2 flex items-center gap-2 hover:border-slate-400 hover:text-slate-800'
              onClick={handleTextPreview}
            >
              <BsBodyText />
              プレビュー
            </ボタン>{" "}
            <ボタン
              className='グループ ボーダー ボーダー-b-2 ボーダー-スレート-500 ラウンド-MD p-2 フレックス アイテム-センター ギャップ-2 hover:border-slate-400 hover:text-slate-800 '
              onClick={handleArticleSubmit}
            >
                アーティゴを編集する 
              <AiOutlineEdit className='w-5 h-5 group-hover:text-red-500' />
            </ボタン>
          </div>
          <div className='header-wrapper flex flex-col gap-2 '>
            <div className='画像ボックス'>
              {previewImage.length === 0 && (
                <div className='select-image'>
                  
                    <AiOutlineFolderOpen className='w-7 h-7' />;
                    ドラッグ アンド ドロップ画像
                  </ラベル>
                  



<h2>
  
  
  結論
</h2>

<p>まず、このチュートリアルをお読みいただくために時間を割いていただきありがとうございます。また、このチュートリアルが完了したことをお祝いしたいと思います。ご参考になり、ステップごとの手順が分かりやすかったら幸いです。</p>

<p>次に、構築したものについていくつかの点を強調したいと思います。これはブログ システムの基礎であり、すべての記事を表示する公開ページ、ユーザー登録ページ、さらにはカスタム 404 エラー ページなど、追加すべき点はまだたくさんあります。チュートリアル中にこれらのページについて疑問に思って見逃した場合は、これが意図的なものであることを知ってください。このチュートリアルでは、これらの新しいページを自分で作成し、他のページを追加し、新機能を実装するための十分な経験を提供しました。</p>

<p>次回まで、本当にありがとうございました。 o/</p>


          

            
        </htmlinputelement></iarticle>
ログイン後にコピー

以上がNext.js を使用した動的なブログ ダッシュボードの構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート