首頁 > web前端 > js教程 > 使用 Next.js 建立動態部落格儀表板

使用 Next.js 建立動態部落格儀表板

Barbara Streisand
發布: 2024-12-08 17:04:10
原創
638 人瀏覽過

介紹

你好,你好嗎?我是 Vítor,帶著一個新專案回來了,可以幫助您提高程式設計技能。自從我上次發布教程以來已經有一段時間了。在過去的幾個月裡,我花了一些時間休息並專注於其他活動。在此期間,我開發了一個小型網路專案:博客,它成為本教程的重點。

在本指南中,我們將建立能夠渲染 Markdown 的部落格頁面的前端。該應用程式將包括公共和私人路由、用戶身份驗證以及編寫 Markdown 文字、新增照片、顯示文章等功能。

隨意自訂您的應用程序,無論您喜歡什麼——我甚至鼓勵這樣做。

您可以在此處存取此應用程式的儲存庫:

Building a Dynamic Blog Dashboard with Next.js 岡德拉克08 / 部落格平台

使用 Next.js/typescript 製作的部落格平台。

部落格平台

  • 文字教學

成分

  • next-auth - Next.js 的autenticação 圖書館
  • github.com/markdown-it/markdown-it - markdown biblioteca。
  • github.com/sindresorhus/github-markdown-css- Para dar estilo ao nosso markdown 編輯器。
  • github.com/remarkjs/react-markdown - Biblioteca para renderizar markdown em nosso 元件react。
  • github.com/remarkjs/remark-react/tree/4722bdf - React 中 Markdown 轉換插件。
  • codemirror.net - 網路編輯器元件。
  • react-icons - 反應圖示庫。

科莫美國

npm i
npm run start
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

伺服器

você pode encontrar o server dessa aplicação em server


在 GitHub 上查看


本教學還包括本指南中將使用的 Node.js 伺服器的編寫:

希望您喜歡。

編碼愉快!

圖書館

以下是此項目中使用的庫的摘要:

  • next-auth - Next.js 的驗證庫
  • github.com/markdown-it/markdown-it - Markdown 函式庫。
  • 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 元件編輯器。
  • react-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 可讓您處理使用任意憑證的登錄,例如使用者名稱和密碼、網域、雙重認證、硬體設備等。

首先,在專案的根目錄中,建立一個 .env.local 檔案並新增一個令牌,該令牌將用作我們的秘密

npm i
npm run start
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

接下來,讓我們來寫我們的驗證系統,這個 NEXTAUTH_SECRET 將會被加入到 src/app/auth/[...nextauth]/routes.ts 檔案中的秘密中。

npx create-next-app myblog
登入後複製
登入後複製
登入後複製

認證提供者

讓我們建立一個身份驗證提供程序,一個上下文,它將在我們的私有路由的頁面上共享使用者的資料。稍後我們將使用它來包裝我們的layout.tsx 檔案之一。

在 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
登入後複製
登入後複製
登入後複製

佈局

現在讓我們來寫私有和公有的佈局。

應用程式/佈局.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
登入後複製
登入後複製
登入後複製

頁面/layout.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

一個有兩個導航連結的簡單元件。

/*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 呼叫並顯示回應的元件。

在這裡,我們將透過我們編寫的函數使用兩個 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 對應於我們導航路線的 slug。
  • params 是透過包含導航 slug 的應用程式樹傳遞的屬性。
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view
登入後複製
登入後複製
登入後複製

第二:使用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/資料夾中,當在()內宣告一個檔案時,就表示該路由對應於/。

在我們的例子中,(Home) 資料夾指的是我們私人路線的主頁。這是使用者在系統中進行身份驗證後看到的第一個頁面。此頁面將顯示我們資料庫中的文章清單。

資料將由我們的 ArticlesList.tsx 元件處理。如果您還沒有編寫此程式碼,請參閱元件部分。

在應用程式/(頁面)/(私人)/(主頁)/page.tsx。

npm i
npm run start
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

新文章

這是我們應用程式中最重要的頁面之一,因為它允許我們註冊我們的文章。

此頁面將使用戶能夠:

  1. 以 Markdown 格式寫一篇文章。
  2. 為文章分配圖像。
  3. 在將 Markdown 文字提交到伺服器之前預覽它。

頁面使用了多個鉤子

  1. useCallback - 用於記憶函數。
  2. useState - 允許您為我們的元件新增狀態變數。
  3. useSession - 讓我們檢查使用者是否經過身份驗證並取得身份驗證令牌。

為此,我們將使用兩個組件:

  1. TextEditor.tsx:我們之前寫的文字編輯器。
  2. Preview.tsx:用於顯示 Markdown 格式檔案的元件。

在建立此頁面時,我們將使用我們的 API:

  1. POST:使用我們的函數 postArticle,我們將把文章送到伺服​​器。

我們也將使用 next-auth 函式庫提供的 useSession 鉤子來取得使用者的驗證令牌,該令牌將用於在伺服器上註冊文章。

這將涉及三個不同的 API 呼叫。
在 app/pages/(private)/newArticle/page.tsx.

「使用客戶端」;
從「react」匯入 React, { ChangeEvent, useCallback, useState };
從“next-auth/react”導入{useSession};
從“下一步/導航”導入{重定向};
從“@/services/postArticle”匯入 postArtical;
從“react-icons/ai”導入{AiOutlineFolderOpen};
從“react-icons/ri”導入 { RiImageEditLine };

從“下一個/圖像”導入圖像;
從“@/components/textEditor”導入文字編輯器;
從“@/components/PreviewText”導入預覽;
從“react-icons/ai”導入{AiOutlineSend};
從“react-icons/bs”導入{BsBodyText};

匯出預設函數 NewArticle(params:any) {
  const { 資料:會話 }:任何 = useSession({
    要求:真實,
    onUnauthenticated(){
      重定向(“/登入”);
    },
  });
  const [imageUrl, setImageUrl] = useState<object>({});
  const [previewImage, setPreviewImage] = useState<string>("");
  const [previewText, setPreviewText] = useState<boolean>(false);
  const [標題,setTitle] = useState<string>("");
  const [doc, setDoc] = useState<string>("# Escreva o seu texto... n");
  const handleDocChange = useCallback((newDoc: any) => {
    setDoc(newDoc);
  }, []);

  if (!session?.user) 回傳 null;

  const handleArticleSubmit = async (e:any) =>; {
        e.preventDefault();
    const token: string = session.user.token;
    嘗試 {
      const res = 等待 postArtical({
        id: session.user.userId.toString(),
        令牌:令牌,
        圖片網址: 圖片網址,
        標題:“標題”
        文檔: 文檔,
      });
      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(文件);
      設定預覽影像(網址);
      setImageUrl(文件);
    }
  };

  const handleTextPreview = (e: 任意) => {
    e.preventDefault();
    setPreviewText(!previewText);
  };
  返回 (
    <section classname="w-full h-full min-h-screenrelative py-8">
      {預覽文字&&(
        <div classname="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">;
          ; setPreviewText(!previewText)}
          >>
        </div>;
      )}

      <form classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop -shadow-xl flex flex-col 間隙-2“>
        {“”}
        <div className=" flex justify- between items-center>
          
            <bsbodytext></bsbodytext>>
            預覽
          按鈕>{" "}
          
            恩維亞爾·特克斯托
            <aioutlinesend classname="w-5 h-5 group-hover:text-red-500"></aioutlinesend>
          按鈕>
        ;
        <div classname="header-wrapper flex flex-col gap-2">
          <div classname="image-box">
            {previewImage.length === 0 && (
              <div classname="select-image">
                
                  <aioutlinefolderopen classname="w-7 h-7"></aioutlinefolderopen>
                  拖放影像
                標籤>
                



<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">「使用客戶端」;
從「react」匯入 React, { useState, useEffect, useCallback, useRef, ChangeEvent };
從“next-auth/react”導入{useSession};
從“下一步/導航”導入{重定向};
從“下一個/圖像”導入圖像;

從“@/interfaces/article.interface”導入{IArticle};
從“react-icons/ai”導入{AiOutlineEdit};
從“react-icons/bs”導入{BsBodyText};
從“react-icons/ai”導入{AiOutlineFolderOpen};
從“react-icons/ri”導入 { RiImageEditLine };

從“@/components/PreviewText”導入預覽;
從“@/components/textEditor”導入文字編輯器;
從'@/components/Loading'導入載入;
從“@/services/editArticle”導入 editArtical;

匯出預設函數 EditArticle({ params }: { params: any }) {
 const { 資料:會話 }:任何 = useSession({
    要求:真實,
    onUnauthenticated(){
      重定向(“/登入”);
    },
  });
  const id: 數字 = params.id;
  const [文章,setArticle] = useState<iarticle>(空);
  const [imageUrl, setImageUrl] = useState<object>({});
  const [previewImage, setPreviewImage] = useState<string>("");
  const [previewText, setPreviewText] = useState<boolean>(false)
  const [標題,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) =>; {
    嘗試 {
      常量響應 = 等待獲取(
        `http://localhost:8080/articles/getById/${id}`,
      );
      const jsonData = 等待回應.json();
      setArticle(jsonData);
    } 捕獲(錯誤){
      console.log("出了點問題:", err);
    }
  };
  useEffect(() => {
    if (文章 !== null || 文章 !== 未定義) {
      取得文章(id);
    }
  }, [ID]);

  useEffect(()=>{
    if(文章!= null && 文章.內容){
        setDoc(文章.內容)
    }

    if(文章!=null && 文章.image){
      setPreviewImage(`http://localhost:8080/` 文章.image)
    }
  },[文章])

  const handleArticleSubmit = async (e:any) =>; {
     e.preventDefault();
    const token: string = session.user.token;
    嘗試{
      const res = 等待 editArtical({
      身分證字號: 身分證號,
      令牌:令牌,
      圖片網址:圖片網址,
      標題: 標題,
      文檔: 文檔,
      });
        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(文件);
      設定預覽影像(網址);
      setImageUrl(文件);
    }

  };
   const handleTextPreview = (e: 任意) => {
    e.preventDefault();
    setPreviewText(!previewText);
    console.log('預覽版你好!')
  };

  if(!article) return >
  if(文章?.內容)
  返回 (
    <section classname="w-full h-full min-h-screenrelative py-8">
      {預覽文字&&(
        <div classname="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">;
          ; setPreviewText(!previewText)}
          >>
        </div>
      )}

      <div classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-white drop- Shadow -md flex flex-col 間隙-2">
        <form classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop -shadow-md flex flex-col 間隙-2 ">
          {“”}
          <div classname="flex justify- Between items-center">;
            
              <bsbodytext></bsbodytext>>
              預覽
            按鈕>{" "}
            
                編輯阿蒂戈
              <aioutlineedit classname="w-5 h-5 group-hover:text-red-500"></aioutlineedit>>
            按鈕>
          </div>;
          <div classname="header-wrapper flex flex-col gap-2">;
            <div classname="image-box">;
              {previewImage.length === 0 && (
                <div classname="select-image">;
                  
                    <aioutlinefolderopen classname="w-7 h-7"></aioutlinefolderopen>>
                    拖放影像
                  標籤>
                  



<h2>
  
  
  結論
</h2>

<p>首先,我要感謝您花時間閱讀本教程,並且我還要祝賀您完成它。我希望它對您有幫助,並且逐步說明很容易遵循。 </p>

<p>其次,我想強調一下關於我們剛剛建立的內容的幾點。這是部落格系統的基礎,還有很多東西需要添加,例如顯示所有文章的公共頁面、用戶註冊頁面,甚至是自訂的 404 錯誤頁面。如果在教程期間您對這些頁面感到好奇並錯過了它們,請知道這是故意的。本教學為您提供了足夠的經驗來自行創建這些新頁面、添加許多其他頁面以及實現新功能。 </p>

<p>非常感謝,下次再見。哦/</p>


          </div>

            
        </div>
</div>
</form>
</div></section></htmlinputelement></htmlinputelement></string></string></boolean></string></object></iarticle>
登入後複製

以上是使用 Next.js 建立動態部落格儀表板的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
上一篇:如何使用 JavaScript 可靠地檢測 URL 哈希中的變更? 下一篇:掌握 HTML:從基礎到中級
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
最新問題
關於CSS心智圖的課件在哪? 課件
來自於 2024-04-16 10:10:18
0
0
1878
相關專題
更多>
熱門推薦
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板