<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Youtube &#8211; wordpress_blog</title>
	<atom:link href="/wordpress_blog/category/youtube/feed/" rel="self" type="application/rss+xml" />
	<link>/wordpress_blog</link>
	<description>This is a dynamic to static website.</description>
	<lastBuildDate>Wed, 26 Mar 2025 01:14:59 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.7.2</generator>

<image>
	<url>/wordpress_blog/wp-content/uploads/2022/03/logo.png</url>
	<title>Youtube &#8211; wordpress_blog</title>
	<link>/wordpress_blog</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Build &#038; Deploy Full Stack E-commerce Website &#124; Redux &#124; MERN Stack – 05</title>
		<link>/wordpress_blog/full-stack-rabbit-05/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Wed, 26 Mar 2025 01:14:57 +0000</pubDate>
				<category><![CDATA[Youtube]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=897</guid>

					<description><![CDATA[學習來自 YT:&#160;compiletab影片:&#038;nbsp [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>學習來自 YT:&nbsp;<a rel="noreferrer noopener" href="https://www.youtube.com/@compiletab" target="_blank">compiletab</a><br>影片:&nbsp;<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=hpgh2BTtac8" target="_blank">Build &amp; Deploy Full Stack E-commerce Website | Redux | MERN Stack Project</a><br>Github assets:&nbsp;<a rel="noreferrer noopener" href="https://github.com/kushald/rabbit-assets" target="_blank">連結</a><br>Thank you, teacher</p>



<h1 class="wp-block-heading">建立 &amp; 部署全端商業網站</h1>



<h2 class="wp-block-heading">影片時間戳</h2>



<p>00:00:00 – Introduction<br>00:01:56 – Demo<br>00:05:26 – Installation &amp; set up<br>05:50:25 – Admin UI<br>07:20:25 – Backend setup<br>07:32:40 – user routes<br>08:05:40 – Products routes<br>09:18:56 – cart routes<br>10:13:26 – checkout routes<br>10:57:55 – Admin routes<br>11:58:30 – Redux</p>



<h2 class="wp-block-heading">部署</h2>



<ul class="wp-block-list">
<li>使用 Vercel 部署這個專案</li>
</ul>



<h2 class="wp-block-heading">Github 部署流程</h2>



<ul class="wp-block-list">
<li>在後端專案資料夾建立 vercel.json 檔案</li>



<li>在前端專案資料夾建立 vercel.json 檔案</li>



<li>在 rabbit 專案資料夾建立 .gitignore 檔案</li>



<li>打開新的終端機，在 rabbit 專案根目錄下<br>git init – 初始化一個新的 Git 儲存庫</li>



<li>git add . – 加入到 Git 的暫存區</li>



<li>git commit -am “initial commit” – 將所有已修改的已跟蹤檔案自動加入暫存區並提交，並附上一條提交訊息</li>



<li>在 Github 建立新的儲存庫<br>Repository name: Rabbit<br>影片示範是使用 Public，我這裡選擇 Private 嘗試<br>Create Repository – 建立儲存庫</li>



<li>複製 Github 命令行的程式碼 – git branch -M master<br>在終端機 rabbit 根目錄下貼上、執行</li>



<li>複製 Github 命令行的程式碼 – git remote add origin https://github.com/GeeHsu/Rabbit.git<br>在終端機 rabbit 根目錄下貼上、執行</li>



<li>複製 Github 命令行的程式碼 – git push -u origin master<br>在終端機 rabbit 根目錄下貼上、執行</li>



<li>重新整理 Github 頁面可以看到程式碼已經順利上傳</li>
</ul>



<h2 class="wp-block-heading">Vercel 部署流程</h2>



<ul class="wp-block-list">
<li>打開 Vercel 儀錶板</li>



<li>新增專案</li>



<li>匯入 Git 儲存庫</li>



<li>新增 Github 帳號</li>



<li>指定儲存庫 > 安裝</li>



<li>Import – 匯入</li>



<li>Edit – 編輯<br>選擇 backend > 繼續</li>



<li>Environment Variables – 環境變數<br>打開後端資料夾 .env 檔案<br>MONGO_URI<br>JWT_SECRET<br>CLOUDINARY_CLOUD_NAME<br>CLOUDINARY_API_KEY<br>CLOUDINARY_API_SECRET</li>



<li>Deploy – 部署</li>



<li>點擊圖片確認後端部署是否成功</li>



<li>Continue to Dashboard – 繼續儀錶板</li>



<li>新增專案</li>



<li>Import – 匯入</li>



<li>Edit – 編輯<br>選擇 frontend > 繼續</li>



<li>Framework Preset – 框架預設<br>沒有自動選取的話，選擇 Vite</li>



<li>Environment Variables – 環境變數<br>打開前端資料夾 .env 檔案<br>VITE_BACKEND_URL=server URL – 伺服器網址 (自己的伺服器網址)<br>VITE_PAYPAL_CLIENT_ID</li>



<li>Deploy – 部署</li>



<li>示範影片是使用 Mac 出現錯誤，刪除 node_modules 資料夾、package-lock.json 檔案，然後重新安裝、上傳到 Github 儲存庫</li>



<li>編輯修正前端環境變數 VITE_BACKEND_URL 網址最後不用斜線</li>



<li>前端專案重新部署</li>



<li>測試功能是否正常運作</li>
</ul>



<pre class="wp-block-code"><code>// backend/vercel.json
{
  "version": 2,
  "name": "backend",
  "builds": &#91;
    {
      "src": "server.js",
      "use": "@vercel/node"
    }
  ],
  "routes": &#91;
    {
      "src": "/(.*)",
      "dest": "server.js"
    }
  ]
}
</code></pre>



<pre class="wp-block-code"><code>// frontend/vercel.json
{
  "version": 2,
  "builds": &#91;
    {
      "src": "package.json",
      "use": "@vercel/static-build",
      "config": { "distDir": "dist" }
    }
  ],
  "rewrites": &#91;{ "source": "/(.*)", "destination": "/" }]
}
</code></pre>



<pre class="wp-block-code"><code>// .gitignore
#Logs
logs
*.logs

# Node Modules
frontend/node_modules/
backend/node_modules/

# Environment variables
frontend/.env
backend/.env

# 忽略 .DS_Store 檔案 (Mac)
.DS_Store
# 忽略 Thumbs.db 檔案 (Windows)
Thumbs.db
</code></pre>



<pre class="wp-block-code"><code>// /rabbit
// 打開新的終端機
// 初始化一個新的 Git 儲存庫
git init
</code></pre>



<pre class="wp-block-code"><code>// /rabbit
// 加入到 Git 的暫存區
git add .
</code></pre>



<pre class="wp-block-code"><code>// /rabbit
// 將所有已修改的已跟蹤檔案自動加入暫存區並提交，並附上一條提交訊息
git commit -am "initial commit"
</code></pre>



<pre class="wp-block-code"><code>// /rabbit
// 在終端機 rabbit 根目錄下貼上、執行
git branch -M master</code></pre>



<pre class="wp-block-code"><code>// /rabbit
// 在終端機 rabbit 根目錄下貼上、執行
git remote add origin https://github.com/GeeHsu/Rabbit.git
</code></pre>



<pre class="wp-block-code"><code>// /rabbit
// 在終端機 rabbit 根目錄下貼上、執行
git push -u origin master</code></pre>



<h2 class="wp-block-heading">Personal Access Token (PAT)</h2>



<ul class="wp-block-list">
<li>步驟 1：登錄 GitHub 並進入設置頁面</li>



<li>步驟 2：創建 Personal Access Token (PAT)</li>



<li>步驟 3：選擇權限（Scopes）</li>



<li>步驟 4：選擇有效期限（Optional）</li>



<li>步驟 5：生成並複製 PAT</li>
</ul>



<h3 class="wp-block-heading">步驟 2：創建 Personal Access Token (PAT)</h3>



<ol class="wp-block-list">
<li>在 <strong>Settings</strong> 頁面左側選擇 <strong>Developer settings</strong>。</li>



<li>在 <strong>Developer settings</strong> 頁面，選擇 <strong>Personal access tokens</strong>。</li>



<li>點擊 <strong>Generate new token</strong> 按鈕，這會引導您創建新的 PAT。</li>
</ol>



<h3 class="wp-block-heading">步驟 3：選擇權限（Scopes）</h3>



<p>必要常見範圍 (Scopes):</p>



<ul class="wp-block-list">
<li>repo</li>



<li>workflow</li>



<li>read:org</li>



<li>user</li>
</ul>



<p>以上完成這個專案所有內容。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Build &#038; Deploy Full Stack E-commerce Website &#124; Redux &#124; MERN Stack – 04</title>
		<link>/wordpress_blog/full-stack-rabbit-04/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Wed, 26 Mar 2025 01:06:49 +0000</pubDate>
				<category><![CDATA[Youtube]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=892</guid>

					<description><![CDATA[學習來自 YT:&#160;compiletab影片:&#038;nbsp [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>學習來自 YT:&nbsp;<a rel="noreferrer noopener" href="https://www.youtube.com/@compiletab" target="_blank">compiletab</a><br>影片:&nbsp;<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=hpgh2BTtac8" target="_blank">Build &amp; Deploy Full Stack E-commerce Website | Redux | MERN Stack Project</a><br>Github assets:&nbsp;<a rel="noreferrer noopener" href="https://github.com/kushald/rabbit-assets" target="_blank">連結</a><br>Thank you, teacher</p>



<h1 class="wp-block-heading">建立 &amp; 部署全端商業網站</h1>



<h2 class="wp-block-heading">影片時間戳</h2>



<p>00:00:00 – Introduction<br>00:01:56 – Demo<br>00:05:26 – Installation &amp; set up<br>05:50:25 – Admin UI<br>07:20:25 – Backend setup<br>07:32:40 – user routes<br>08:05:40 – Products routes<br>09:18:56 – cart routes<br>10:13:26 – checkout routes<br>10:57:55 – Admin routes<br>11:58:30 – Redux</p>



<h2 class="wp-block-heading">Integrate frontend with backend (整合前端與後端)</h2>



<h2 class="wp-block-heading">New Arrivals (新品上架)</h2>



<pre class="wp-block-code"><code>// frontend/src/components/Products/NewArrivals.js
import React, { useEffect, useRef, useState } from "react";
import { FiChevronLeft, FiChevronRight } from "react-icons/fi";
import { Link } from "react-router-dom";
import axios from "axios";

const NewArrivals = () =&gt; {
  const scrollRef = useRef(null); // 用於引用滾動容器的 DOM 元素
  const &#91;isDragging, setIsDragging] = useState(false);
  const &#91;startX, setStartX] = useState(0);
  // const &#91;scrollLeft, setScrollLeft] = useState(false);
  // scrollLeft 是用來儲存滾動容器水平方向的偏移量，它應該是數字型態而非布林值。
  const &#91;scrollLeft, setScrollLeft] = useState(0);
  const &#91;canScrollLeft, setCanScrollLeft] = useState(false);
  const &#91;canScrollRight, setCanScrollRight] = useState(true);

  const &#91;newArrivals, setNewArrivals] = useState(&#91;]);

  useEffect(() =&gt; {
    const fetchNewArrivals = async () =&gt; {
      try {
        const response = await axios.get(
          `${import.meta.env.VITE_BACKEND_URL}/api/products/new-arrivals`
        );
        setNewArrivals(response.data);
      } catch (error) {
        console.error(error);
      }
    };

    fetchNewArrivals();
  }, &#91;]);

  const handleMouseDown = (e) =&gt; {
    setIsDragging(true); // 開始拖曳
    setStartX(e.pageX - scrollRef.current.offsetLeft); // 記錄滑鼠按下時的 X 座標，減去容器的偏移量
    setScrollLeft(scrollRef.current.scrollLeft); // 記錄滾動容器當前的 scrollLeft (滾動位置)
  };

  const handleMouseMove = (e) =&gt; {
    if (!isDragging) return; // 如果沒有正在拖曳，則不進行後續操作
    const x = e.pageX - scrollRef.current.offsetLeft; // 計算滑鼠相對於容器的 X 座標
    const walk = x - startX; // 計算滑鼠移動的距離
    scrollRef.current.scrollLeft = scrollLeft - walk; // 根據滑鼠移動的距離更新滾動位置
  };

  const handleMouseUpOrLeave = () =&gt; {
    setIsDragging(false); // 停止拖曳，將 isDraggin 設為 false
  };

  const scroll = (direction) =&gt; {
    const scrollAmount = direction === "left" ? -300 : 300; // 根據方向決定滾動的距離
    scrollRef.current.scrollBy({ left: scrollAmount, behaviour: "smooth" }); // 進行平滑滾動
  };

  // Update Scroll Buttons - 更新滾動的按鈕
  const updateScrollButtons = () =&gt; {
    const container = scrollRef.current; // 取得滾動容器的引用

    if (container) {
      const leftScroll = container.scrollLeft; // 取得當前滾動容器的滾動位置 (距離左邊的偏移量)
      const rightScrollable =
        container.scrollWidth &gt; leftScroll + container.clientWidth; // 檢查是否還有內容可以向右滾動

      setCanScrollLeft(leftScroll &gt; 0); // 如果滾動位置大於 0，則可以向左滾動
      setCanScrollRight(rightScrollable); // 如果總容器寬度大於當前滾動位置加上可視範圍，則可以向右滾動
    }

    // 用來調試，輸出滾動容器的一些狀態信息
    // console.log({
    //   scrollLeft: container.scrollLeft, // 當前滾動位置
    //   clientWidth: container.clientWidth, // 容器的可見寬度
    //   containerScrollWidth: container.scrollWidth, // 容器內容的總寬度
    //   offsetLeft: scrollRef.current.offsetLeft, // 滾動容器相對於頁面左邊的偏移量
    // });
  };

  useEffect(() =&gt; {
    const container = scrollRef.current; // 取得滾動容器的 DOM 元素
    if (container) {
      // 如果容器存在，則添加滾動事件監聽器
      container.addEventListener("scroll", updateScrollButtons);

      // 呼叫一次 updateScrollButtons 來初始化滾動按鈕狀態
      updateScrollButtons();

      // 返回清理函數，在組件卸載時移除滾動事件監聽器
      return () =&gt; container.removeEventListener("scroll", updateScrollButtons);
    }
  }, &#91;newArrivals]);

  return (
    &lt;section className="py-16 px-4 lg:px-0"&gt;
      &lt;div className="container mx-auto text-center mb-10 relative"&gt;
        &lt;h2 className="text-3xl font-bold mb-4"&gt;Explore New Arrivals&lt;/h2&gt;
        &lt;p className="text-lg text-gray-600 mb-8"&gt;
          Discover the latest styles straight off the runway, freshly added to
          keep your wardrobe on the cutting edge of fashion.
        &lt;/p&gt;

        {/* Scroll Buttons - 滾動按鈕 */}
        &lt;div className="absolute right-0 bottom-&#91;-30px] flex space-x-2"&gt;
          &lt;button
            onClick={() =&gt; scroll("left")}
            disabled={!canScrollLeft}
            className={`p-2 rounded border ${
              canScrollLeft
                ? "bg-white text-black"
                : "bg-gray-200 text-gray-400 cursor-not-allowed"
            }`}
          &gt;
            &lt;FiChevronLeft className="text-2xl" /&gt;
          &lt;/button&gt;
          &lt;button
            onClick={() =&gt; scroll("right")}
            className={`p-2 rounded border ${
              canScrollRight
                ? "bg-white text-black"
                : "bg-gray-200 text-gray-400 cursor-not-allowed"
            }`}
          &gt;
            &lt;FiChevronRight className="text-2xl" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      {/* Scrollable Content - 可滾動內容 */}
      &lt;div
        ref={scrollRef}
        className={`container mx-auto overflow-x-scroll flex space-x-6 relative ${
          isDragging ? "cursor-grabbing" : "cursor-grab"
        }`}
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUpOrLeave}
        onMouseLeave={handleMouseUpOrLeave}
      &gt;
        {newArrivals.map((product) =&gt; (
          &lt;div
            key={product._id}
            className="min-w-&#91;100%] sm:min-w-&#91;50%] lg:min-w-&#91;30%] relative"
          &gt;
            &lt;img
              src={product.images&#91;0]?.url}
              alt={product.images&#91;0]?.altText || product.name}
              className="w-full h-&#91;500px] object-cover rounded-lg"
              draggable="false"
            /&gt;
            &lt;div className="absolute bottom-0 left-0 right-0 bg-opacity-50 backdrop-blur-md text-white p-4 rounded-b-lg"&gt;
              &lt;Link to={`/product/${product._id}`} className="block"&gt;
                &lt;h4 className="font-medium"&gt;{product.name}&lt;/h4&gt;
                &lt;p className="mt-1"&gt;${product.price}&lt;/p&gt;
              &lt;/Link&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;
    &lt;/section&gt;
  );
};

export default NewArrivals;
</code></pre>



<h2 class="wp-block-heading">客製化產品資料圖片</h2>



<pre class="wp-block-code"><code>// backend/products.js
// 客製化產品資料圖片
// product.js:

const products = &#91;
  {
    name: "Classic Oxford Button-Down Shirt",
    description:
      "This classic Oxford shirt is tailored for a polished yet casual look. Crafted from high-quality cotton, it features a button-down collar and a comfortable, slightly relaxed fit. Perfect for both formal and casual occasions, it comes with long sleeves, a button placket, and a yoke at the back. The shirt is finished with a gently rounded hem and adjustable button cuffs.",
    price: 39.99,
    discountPrice: 34.99,
    countInStock: 20,
    sku: "OX-SH-001",
    category: "Top Wear",
    brand: "Urban Threads",
    sizes: &#91;"S", "M", "L", "XL", "XXL"],
    colors: &#91;"Red", "Blue", "Yellow"],
    collections: "Business Casual",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1531891437562-4301cf35b7e4?q=80&amp;w=1964&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Classic Oxford Button-Down Shirt Front View",
      },
      {
        url: "https://images.unsplash.com/photo-1531891570158-e71b35a485bc?q=80&amp;w=1964&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Classic Oxford Button-Down Shirt Back View",
      },
    ],
    rating: 4.5,
    numReviews: 12,
  },
  {
    name: "Slim-Fit Stretch Shirt",
    description:
      "A versatile slim-fit shirt perfect for business or evening events. Designed with a fitted silhouette, the added stretch provides maximum comfort throughout the day. Features a crisp turn-down collar, button placket, and adjustable cuffs.",
    price: 29.99,
    discountPrice: 24.99,
    countInStock: 35,
    sku: "SLIM-SH-002",
    category: "Top Wear",
    brand: "Modern Fit",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "Navy Blue", "Burgundy"],
    collections: "Formal Wear",
    material: "Cotton Blend",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1623880840102-7df0a9f3545b?q=80&amp;w=1964&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Slim-Fit Stretch Shirt Front View",
      },
      {
        url: "https://images.unsplash.com/photo-1623975561190-49d8eab816bb?q=80&amp;w=1964&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Slim-Fit Stretch Shirt Back View",
      },
    ],
    rating: 4.8,
    numReviews: 15,
  },
  {
    name: "Casual Denim Shirt",
    description:
      "This casual denim shirt is made from lightweight cotton denim. It features a regular fit, snap buttons, and a straight hem. With Western-inspired details, this shirt is perfect for layering or wearing solo.",
    price: 49.99,
    discountPrice: 44.99,
    countInStock: 15,
    sku: "CAS-DEN-003",
    category: "Top Wear",
    brand: "Street Style",
    sizes: &#91;"S", "M", "L", "XL", "XXL"],
    colors: &#91;"Light Blue", "Dark Wash"],
    collections: "Casual Wear",
    material: "Denim",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1635205383144-402b892efa23?q=80&amp;w=1965&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Casual Denim Shirt Front View",
      },
      {
        url: "https://images.unsplash.com/photo-1635205383325-aa3e6fb5ba55?q=80&amp;w=1965&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Casual Denim Shirt Back View",
      },
    ],
    rating: 4.6,
    numReviews: 8,
  },
  {
    name: "Printed Resort Shirt",
    description:
      "Designed for summer, this printed resort shirt is perfect for vacation or weekend getaways. It features a relaxed fit, short sleeves, and a camp collar. The all-over tropical print adds a playful vibe.",
    price: 29.99,
    discountPrice: 22.99,
    countInStock: 25,
    sku: "PRNT-RES-004",
    category: "Top Wear",
    brand: "Beach Breeze",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Tropical Print", "Navy Palms"],
    collections: "Vacation Wear",
    material: "Viscose",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1635205383325-aa3e6fb5ba55?q=80&amp;w=1965&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Printed Resort Shirt Front View",
      },
      {
        url: "https://images.unsplash.com/photo-1635205383450-e0fee6fe73c4?q=80&amp;w=1965&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Printed Resort Shirt Back View",
      },
    ],
    rating: 4.4,
    numReviews: 10,
  },
  {
    name: "Slim-Fit Easy-Iron Shirt",
    description:
      "A slim-fit, easy-iron shirt in woven cotton fabric with a fitted silhouette. Features a turn-down collar, classic button placket, and a yoke at the back. Long sleeves and adjustable button cuffs with a rounded hem.",
    price: 34.99,
    discountPrice: 29.99,
    countInStock: 30,
    sku: "SLIM-EIR-005",
    category: "Top Wear",
    brand: "Urban Chic",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Gray"],
    collections: "Business Wear",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1679101893304-045625840a94?q=80&amp;w=1964&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Slim-Fit Easy-Iron Shirt Front View",
      },
      {
        url: "https://images.unsplash.com/photo-1679101893301-6c87f1508e2e?q=80&amp;w=1964&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Slim-Fit Easy-Iron Shirt Front View",
      },
    ],
    rating: 5,
    numReviews: 14,
  },
  {
    name: "Polo T-Shirt with Ribbed Collar",
    description:
      "A wardrobe classic, this polo t-shirt features a ribbed collar and cuffs. Made from 100% cotton, it offers breathability and comfort throughout the day. Tailored in a slim fit with a button placket at the neckline.",
    price: 24.99,
    discountPrice: 19.99,
    countInStock: 50,
    sku: "POLO-TSH-006",
    category: "Top Wear",
    brand: "Polo Classics",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Navy", "Red"],
    collections: "Casual Wear",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1619533394727-57d522857f89?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Polo T-Shirt Front View",
      },
      {
        url: "https://images.unsplash.com/photo-1618886614638-80e3c103d31a?q=80&amp;w=1920&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Polo T-Shirt Back View",
      },
    ],
    rating: 4.3,
    numReviews: 22,
  },
  {
    name: "Oversized Graphic T-Shirt",
    description:
      "An oversized graphic t-shirt that combines comfort with street style. Featuring bold prints across the chest, this relaxed fit tee offers a modern vibe, perfect for pairing with jeans or joggers.",
    price: 19.99,
    discountPrice: 15.99,
    countInStock: 40,
    sku: "OVS-GRF-007",
    category: "Top Wear",
    brand: "Street Vibes",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "Gray"],
    collections: "Streetwear",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1492288991661-058aa541ff43?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Oversized Graphic T-Shirt Front View",
      },
    ],
    rating: 4.6,
    numReviews: 30,
  },
  {
    name: "Regular-Fit Henley Shirt",
    description:
      "A modern take on the classic Henley shirt, this regular-fit style features a buttoned placket and ribbed cuffs. Made from a soft cotton blend with a touch of elastane for stretch.",
    price: 22.99,
    discountPrice: 18.99,
    countInStock: 35,
    sku: "REG-HEN-008",
    category: "Top Wear",
    brand: "Heritage Wear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Heather Gray", "Olive", "Black"],
    collections: "Casual Wear",
    material: "Cotton Blend",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1519764622345-23439dd774f7?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Regular-Fit Henley Shirt Front View",
      },
    ],
    rating: 4.5,
    numReviews: 25,
  },
  {
    name: "Long-Sleeve Thermal Tee",
    description:
      "Stay warm with this long-sleeve thermal tee, made from soft cotton with a waffle-knit texture. Ideal for layering in cooler months, the slim-fit design ensures a snug yet comfortable fit.",
    price: 27.99,
    discountPrice: 22.99,
    countInStock: 20,
    sku: "LST-THR-009",
    category: "Top Wear",
    brand: "Winter Basics",
    sizes: &#91;"S", "M", "L", "XL", "XXL"],
    colors: &#91;"Charcoal", "Dark Green", "Navy"],
    collections: "Winter Essentials",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1552642986-ccb41e7059e7?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Long-Sleeve Thermal Tee Front View",
      },
    ],
    rating: 4.4,
    numReviews: 18,
  },
  {
    name: "V-Neck Classic T-Shirt",
    description:
      "A classic V-neck t-shirt for everyday wear. This regular-fit tee is made from breathable cotton and features a clean, simple design with a flattering V-neckline. Lightweight fabric and soft texture make it perfect for casual looks.",
    price: 14.99,
    discountPrice: 11.99,
    countInStock: 60,
    sku: "VNECK-CLS-010",
    category: "Top Wear",
    brand: "Everyday Comfort",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Black", "Navy"],
    collections: "Basics",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1505632958218-4f23394784a6?q=80&amp;w=1964&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "V-Neck Classic T-Shirt Front View",
      },
    ],
    rating: 4.7,
    numReviews: 28,
  },
  {
    name: "Slim Fit Joggers",
    description:
      "Slim-fit joggers with an elasticated drawstring waist. Features ribbed hems and side pockets. Ideal for casual outings or workouts.",
    price: 40,
    discountPrice: 35,
    countInStock: 20,
    sku: "BW-001",
    category: "Bottom Wear",
    brand: "ActiveWear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "Gray", "Navy"],
    collections: "Casual Collection",
    material: "Cotton Blend",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1599847022902-f64cc1ae97fd?q=80&amp;w=1964&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Slim Fit Joggers Front View",
      },
    ],
    rating: 4.5,
    numReviews: 12,
  },
  {
    name: "Cargo Joggers",
    description:
      "Relaxed-fit cargo joggers featuring multiple pockets for functionality. Drawstring waist and cuffed hems for a modern look.",
    price: 45,
    discountPrice: 40,
    countInStock: 15,
    sku: "BW-002",
    category: "Bottom Wear",
    brand: "UrbanStyle",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Olive", "Black"],
    collections: "Urban Collection",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1605192554106-d549b1b975cd?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Cargo Joggers Front View",
      },
    ],
    rating: 4.7,
    numReviews: 20,
  },
  {
    name: "Tapered Sweatpants",
    description:
      "Tapered sweatpants designed for comfort. Elastic waistband with adjustable drawstring, perfect for lounging or athletic activities.",
    price: 35,
    discountPrice: 30,
    countInStock: 25,
    sku: "BW-003",
    category: "Bottom Wear",
    brand: "ChillZone",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Gray", "Charcoal", "Blue"],
    collections: "Lounge Collection",
    material: "Fleece",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1636590416708-68a4867918f1?q=80&amp;w=1965&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Tapered Sweatpants Front View",
      },
    ],
    rating: 4.3,
    numReviews: 18,
  },
  {
    name: "Denim Jeans",
    description:
      "Classic slim-fit denim jeans with a slight stretch for comfort. Features a zip fly and five-pocket styling for a timeless look.",
    price: 60,
    discountPrice: 50,
    countInStock: 30,
    sku: "BW-004",
    category: "Bottom Wear",
    brand: "DenimCo",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Dark Blue", "Light Blue"],
    collections: "Denim Collection",
    material: "Denim",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1589591990984-68a20755020d?q=80&amp;w=1965&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Denim Jeans Front View",
      },
    ],
    rating: 4.6,
    numReviews: 22,
  },
  {
    name: "Chino Pants",
    description:
      "Slim-fit chino pants made from stretch cotton twill. Features a button closure and front and back pockets. Ideal for both casual and semi-formal wear.",
    price: 55,
    discountPrice: 48,
    countInStock: 40,
    sku: "BW-005",
    category: "Bottom Wear",
    brand: "CasualLook",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Beige", "Navy", "Black"],
    collections: "Smart Casual Collection",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1651448914541-db2ec9fedad1?q=80&amp;w=1969&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Chino Pants Front View",
      },
    ],
    rating: 4.8,
    numReviews: 15,
  },
  {
    name: "Track Pants",
    description:
      "Comfortable track pants with an elasticated waistband and tapered leg. Features side stripes for a sporty look. Ideal for athletic and casual wear.",
    price: 40,
    discountPrice: 35,
    countInStock: 20,
    sku: "BW-006",
    category: "Bottom Wear",
    brand: "SportX",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "Red", "Blue"],
    collections: "Activewear Collection",
    material: "Polyester",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1584940120743-8981ca35b012?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Track Pants Front View",
      },
    ],
    rating: 4.2,
    numReviews: 17,
  },
  {
    name: "Slim Fit Trousers",
    description:
      "Tailored slim-fit trousers with belt loops and a hook-and-eye closure. Suitable for formal occasions or smart-casual wear.",
    price: 65,
    discountPrice: 55,
    countInStock: 15,
    sku: "BW-007",
    category: "Bottom Wear",
    brand: "ExecutiveStyle",
    sizes: &#91;"M", "L", "XL"],
    colors: &#91;"Gray", "Black"],
    collections: "Office Wear",
    material: "Polyester",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1480429370139-e0132c086e2a?q=80&amp;w=1976&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Slim Fit Trousers Front View",
      },
    ],
    rating: 4.7,
    numReviews: 10,
  },
  {
    name: "Cargo Pants",
    description:
      "Loose-fit cargo pants with multiple utility pockets. Features adjustable ankle cuffs and a drawstring waist for versatility and comfort.",
    price: 50,
    discountPrice: 45,
    countInStock: 25,
    sku: "BW-008",
    category: "Bottom Wear",
    brand: "StreetWear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Olive", "Brown", "Black"],
    collections: "Street Style Collection",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1552337480-48918be048b9?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Cargo Pants Front View",
      },
    ],
    rating: 4.5,
    numReviews: 13,
  },
  {
    name: "Relaxed Fit Sweatpants",
    description:
      "Relaxed-fit sweatpants made from soft fleece fabric. Features an elastic waist and adjustable drawstring for a custom fit.",
    price: 35,
    discountPrice: 30,
    countInStock: 35,
    sku: "BW-009",
    category: "Bottom Wear",
    brand: "LoungeWear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Gray", "Black", "Navy"],
    collections: "Lounge Collection",
    material: "Fleece",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1581382575275-97901c2635b7?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Relaxed Fit Sweatpants Front View",
      },
    ],
    rating: 4.3,
    numReviews: 14,
  },
  {
    name: "Formal Dress Pants",
    description:
      "Classic formal dress pants with a slim fit. Made from lightweight, wrinkle-resistant fabric for a polished look at the office or formal events.",
    price: 70,
    discountPrice: 60,
    countInStock: 20,
    sku: "BW-010",
    category: "Bottom Wear",
    brand: "ElegantStyle",
    sizes: &#91;"M", "L", "XL"],
    colors: &#91;"Black", "Navy"],
    collections: "Formal Collection",
    material: "Polyester",
    gender: "Men",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1555097074-b16ec85d6b3e?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Formal Dress Pants Front View",
      },
    ],
    rating: 4.9,
    numReviews: 8,
  },
  {
    name: "High-Waist Skinny Jeans",
    description:
      "High-waist skinny jeans in stretch denim with a button and zip fly. Features a flattering fit that hugs your curves and enhances your silhouette.",
    price: 50,
    discountPrice: 45,
    countInStock: 30,
    sku: "BW-W-001",
    category: "Bottom Wear",
    brand: "DenimStyle",
    sizes: &#91;"XS", "S", "M", "L", "XL"],
    colors: &#91;"Dark Blue", "Black", "Light Blue"],
    collections: "Denim Collection",
    material: "Denim",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1594744803329-e58b31de8bf5?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "High-Waist Skinny Jeans",
      },
    ],
    rating: 4.8,
    numReviews: 20,
  },
  {
    name: "Wide-Leg Trousers",
    description:
      "Flowy, wide-leg trousers with a high waist and side pockets. Perfect for an elegant look that combines comfort and style.",
    price: 60,
    discountPrice: 55,
    countInStock: 25,
    sku: "BW-W-002",
    category: "Bottom Wear",
    brand: "ElegantWear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Beige", "Black", "White"],
    collections: "Formal Collection",
    material: "Polyester",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?q=80&amp;w=1727&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Wide-Leg Trousers Front View",
      },
    ],
    rating: 4.7,
    numReviews: 15,
  },
  {
    name: "Stretch Leggings",
    description:
      "Soft, stretch leggings in a high-rise style. Perfect for lounging, working out, or casual wear, with a smooth fit that flatters your body.",
    price: 25,
    discountPrice: 20,
    countInStock: 40,
    sku: "BW-W-003",
    category: "Bottom Wear",
    brand: "ComfyFit",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "Gray", "Navy"],
    collections: "Activewear Collection",
    material: "Cotton Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1609505848912-b7c3b8b4beda?q=80&amp;w=1965&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Stretch Leggings Front View",
      },
    ],
    rating: 4.5,
    numReviews: 30,
  },
  {
    name: "Pleated Midi Skirt",
    description:
      "Elegant pleated midi skirt with a high waistband and soft fabric that drapes beautifully. Ideal for both formal and casual occasions.",
    price: 55,
    discountPrice: 50,
    countInStock: 20,
    sku: "BW-W-004",
    category: "Bottom Wear",
    brand: "ChicStyle",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"Pink", "Navy", "Black"],
    collections: "Spring Collection",
    material: "Polyester",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1441123694162-e54a981ceba5?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Pleated Midi Skirt Front View",
      },
    ],
    rating: 4.6,
    numReviews: 18,
  },
  {
    name: "Flared Palazzo Pants",
    description:
      "High-waist palazzo pants with a loose, flowing fit. Comfortable and stylish, making them perfect for casual outings or beach days.",
    price: 45,
    discountPrice: 40,
    countInStock: 35,
    sku: "BW-W-005",
    category: "Bottom Wear",
    brand: "BreezyVibes",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Beige", "Light Blue"],
    collections: "Summer Collection",
    material: "Linen Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?q=80&amp;w=1976&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Flared Palazzo Pants Front View",
      },
    ],
    rating: 4.4,
    numReviews: 22,
  },
  {
    name: "High-Rise Joggers",
    description:
      "Comfortable high-rise joggers with an elastic waistband and drawstring for a perfect fit. Great for lounging or working out.",
    price: 40,
    discountPrice: 35,
    countInStock: 30,
    sku: "BW-W-006",
    category: "Bottom Wear",
    brand: "ActiveWear",
    sizes: &#91;"XS", "S", "M", "L"],
    colors: &#91;"Black", "Gray", "Pink"],
    collections: "Loungewear Collection",
    material: "Cotton Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1524504388940-b1c1722653e1?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "High-Rise Joggers Front View",
      },
    ],
    rating: 4.3,
    numReviews: 25,
  },
  {
    name: "Paperbag Waist Shorts",
    description:
      "Stylish paperbag waist shorts with a belted waist and wide legs. Perfect for summer outings and keeping cool in style.",
    price: 35,
    discountPrice: 30,
    countInStock: 20,
    sku: "BW-W-007",
    category: "Bottom Wear",
    brand: "SunnyStyle",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"White", "Khaki", "Blue"],
    collections: "Summer Collection",
    material: "Cotton",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1496440737103-cd596325d314?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Paperbag Waist Shorts Front View",
      },
    ],
    rating: 4.5,
    numReviews: 19,
  },
  {
    name: "Stretch Denim Shorts",
    description:
      "Comfortable stretch denim shorts with a high-waisted fit and raw hem. Perfect for pairing with your favorite tops during warmer months.",
    price: 40,
    discountPrice: 35,
    countInStock: 25,
    sku: "BW-W-008",
    category: "Bottom Wear",
    brand: "DenimStyle",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Blue", "Black", "White"],
    collections: "Denim Collection",
    material: "Denim",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1541823709867-1b206113eafd?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Stretch Denim Shorts Front View",
      },
    ],
    rating: 4.7,
    numReviews: 15,
  },
  {
    name: "Culottes",
    description:
      "Wide-leg culottes with a flattering high waist and cropped length. The perfect blend of comfort and style for any casual occasion.",
    price: 50,
    discountPrice: 45,
    countInStock: 30,
    sku: "BW-W-009",
    category: "Bottom Wear",
    brand: "ChicStyle",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "White", "Olive"],
    collections: "Casual Collection",
    material: "Polyester",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1526265218618-bdbe4fdb5360?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Culottes Front View",
      },
    ],
    rating: 4.6,
    numReviews: 23,
  },
  {
    name: "Classic Pleated Trousers",
    description:
      "Timeless pleated trousers with a tailored fit. A wardrobe essential for workwear or formal occasions.",
    price: 70,
    discountPrice: 65,
    countInStock: 25,
    sku: "BW-W-010",
    category: "Bottom Wear",
    brand: "ElegantWear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Navy", "Black", "Gray"],
    collections: "Formal Collection",
    material: "Wool Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1496217590455-aa63a8350eea?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Classic Pleated Trousers Front View",
      },
    ],
    rating: 4.8,
    numReviews: 20,
  },
  {
    name: "Knitted Cropped Top",
    description:
      "A stylish knitted cropped top with a flattering fitted silhouette. Perfect for pairing with high-waisted jeans or skirts for a casual look.",
    price: 40,
    discountPrice: 35,
    countInStock: 25,
    sku: "TW-W-001",
    category: "Top Wear",
    brand: "ChicKnit",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"Beige", "White"],
    collections: "Knits Collection",
    material: "Cotton Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1539655382699-69e1d1979ee0?q=80&amp;w=2126&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Knitted Cropped Top",
      },
    ],
    rating: 4.6,
    numReviews: 15,
  },
  {
    name: "Boho Floral Blouse",
    description:
      "Flowy boho blouse with floral patterns, featuring a relaxed fit and balloon sleeves. Ideal for casual summer days.",
    price: 50,
    discountPrice: 45,
    countInStock: 30,
    sku: "TW-W-002",
    category: "Top Wear",
    brand: "BohoVibes",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Pink"],
    collections: "Summer Collection",
    material: "Viscose",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1515734674582-29010bb37906?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Boho Floral Blouse",
      },
    ],
    rating: 4.7,
    numReviews: 20,
  },
  {
    name: "Casual T-Shirt",
    description:
      "A soft, breathable casual t-shirt with a classic fit. Features a round neckline and short sleeves, perfect for everyday wear.",
    price: 25,
    discountPrice: 20,
    countInStock: 50,
    sku: "TW-W-003",
    category: "Top Wear",
    brand: "ComfyTees",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "White", "Gray"],
    collections: "Essentials",
    material: "Cotton",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1647957867246-278e4d0f23fa?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Casual T-Shirt",
      },
    ],
    rating: 4.5,
    numReviews: 25,
  },
  {
    name: "Off-Shoulder Top",
    description:
      "An elegant off-shoulder top with ruffled sleeves and a flattering fit. Ideal for adding a touch of femininity to your outfit.",
    price: 45,
    discountPrice: 40,
    countInStock: 35,
    sku: "TW-W-004",
    category: "Top Wear",
    brand: "Elegance",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"Red", "White", "Blue"],
    collections: "Evening Collection",
    material: "Polyester",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1515372039744-b8f02a3ae446?q=80&amp;w=1976&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Off-Shoulder Top",
      },
    ],
    rating: 4.7,
    numReviews: 18,
  },
  {
    name: "Lace-Trimmed Cami Top",
    description:
      "A delicate cami top with lace trim and adjustable straps. The lightweight fabric makes it perfect for layering or wearing alone during warmer weather.",
    price: 35,
    discountPrice: 30,
    countInStock: 40,
    sku: "TW-W-005",
    category: "Top Wear",
    brand: "DelicateWear",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"Black", "White"],
    collections: "Lingerie-Inspired",
    material: "Silk Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1653919937297-4645e5e7806e?q=80&amp;w=1964&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Lace-Trimmed Cami Top",
      },
    ],
    rating: 4.8,
    numReviews: 22,
  },
  {
    name: "Graphic Print Tee",
    description:
      "A trendy graphic print tee with a relaxed fit. Pair it with jeans or skirts for a cool and casual look.",
    price: 30,
    discountPrice: 25,
    countInStock: 45,
    sku: "TW-W-006",
    category: "Top Wear",
    brand: "StreetStyle",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Black"],
    collections: "Urban Collection",
    material: "Cotton",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1734686885055-3e34120b0520?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Graphic Print Tee",
      },
    ],
    rating: 4.6,
    numReviews: 30,
  },
  {
    name: "Ribbed Long-Sleeve Top",
    description:
      "A cozy ribbed long-sleeve top that offers comfort and style. Perfect for layering during cooler months.",
    price: 55,
    discountPrice: 50,
    countInStock: 30,
    sku: "TW-W-007",
    category: "Top Wear",
    brand: "ComfortFit",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Gray", "Pink", "Brown"],
    collections: "Fall Collection",
    material: "Cotton Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1564485377539-4af72d1f6a2f?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Ribbed Long-Sleeve Top",
      },
    ],
    rating: 4.7,
    numReviews: 26,
  },
  {
    name: "Ruffle-Sleeve Blouse",
    description:
      "A lightweight ruffle-sleeve blouse with a flattering fit. Perfect for a feminine touch to any outfit.",
    price: 45,
    discountPrice: 40,
    countInStock: 20,
    sku: "TW-W-008",
    category: "Top Wear",
    brand: "FeminineWear",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"White", "Navy", "Lavender"],
    collections: "Summer Collection",
    material: "Viscose",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1629709200392-f3051760e529?q=80&amp;w=1964&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Ruffle-Sleeve Blouse",
      },
    ],
    rating: 4.5,
    numReviews: 19,
  },
  {
    name: "Classic Button-Up Shirt",
    description:
      "A versatile button-up shirt that can be dressed up or down. Made from soft fabric with a tailored fit, it's perfect for both casual and formal occasions.",
    price: 60,
    discountPrice: 55,
    countInStock: 25,
    sku: "TW-W-009",
    category: "Top Wear",
    brand: "ClassicStyle",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Light Blue", "Black"],
    collections: "Office Collection",
    material: "Cotton",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1582220225382-ba7e86e9ea5a?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "Classic Button-Up Shirt",
      },
    ],
    rating: 4.8,
    numReviews: 25,
  },
  {
    name: "V-Neck Wrap Top",
    description:
      "A chic v-neck wrap top with a tie waist. Its elegant style makes it perfect for both casual and semi-formal occasions.",
    price: 50,
    discountPrice: 45,
    countInStock: 30,
    sku: "TW-W-010",
    category: "Top Wear",
    brand: "ChicWrap",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"Red", "Black", "White"],
    collections: "Evening Collection",
    material: "Polyester",
    gender: "Women",
    images: &#91;
      {
        url: "https://images.unsplash.com/photo-1612904370193-72d578a78d67?q=80&amp;w=1974&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.0.3&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
        altText: "V-Neck Wrap Top",
      },
    ],
    rating: 4.7,
    numReviews: 22,
  },
];

module.exports = products;
</code></pre>



<h2 class="wp-block-heading">初始化資料庫資料</h2>



<pre class="wp-block-code"><code>// Terminal - 終端機
// backend
npm run seed
</code></pre>



<h2 class="wp-block-heading">Best Seller (暢銷商品)、Top Wear for Women (女裝上衣)</h2>



<pre class="wp-block-code"><code>// frontend/src/pages/Home.jsx
import React, { useEffect, useState } from "react";
import Hero from "../components/Layout/Hero";
import GenderCollectionSection from "../components/Products/GenderCollectionSection";
import NewArrivals from "../components/Products/NewArrivals";
import ProductDetails from "../components/Products/ProductDetails";
import ProductGrid from "../components/Products/ProductGrid";
import FeaturedCollection from "../components/Products/FeaturedCollection";
import FeaturesSection from "../components/Products/FeaturesSection";
import { useDispatch, useSelector } from "react-redux";
import { fetchProductsByFilters } from "../redux/slices/productsSlice";
import axios from "axios";

const Home = () =&gt; {
  const dispatch = useDispatch();
  const { products, loading, error } = useSelector((state) =&gt; state.products);
  const &#91;bestSellerProduct, setBestSellerProduct] = useState(null);

  useEffect(() =&gt; {
    // Fetch products for a specific collection - 獲取特定系列的產品
    dispatch(
      fetchProductsByFilters({
        gender: "Women",
        category: "Bottom Wear",
        limit: 8,
      })
    );
    // Fetch best seller product - 獲取暢銷商品
    const fetchBestSeller = async () =&gt; {
      try {
        const response = await axios.get(
          `${import.meta.env.VITE_BACKEND_URL}/api/products/best-seller`
        );
        setBestSellerProduct(response.data);
      } catch (error) {
        console.error(error);
      }
    };
    fetchBestSeller();
  }, &#91;dispatch]);

  return (
    &lt;div&gt;
      &lt;Hero /&gt;
      &lt;GenderCollectionSection /&gt;
      &lt;NewArrivals /&gt;

      {/* Best Seller - 暢銷商品  */}
      &lt;h2 className="text-3xl text-center font-bold mb-4"&gt;Best Seller&lt;/h2&gt;
      {bestSellerProduct ? (
        &lt;ProductDetails productId={bestSellerProduct._id} /&gt;
      ) : (
        &lt;p className="text-center"&gt;Loading best seller product ...&lt;/p&gt;
      )}

      &lt;div className="container mx-auto"&gt;
        &lt;h2 className="text-3xl text-center font-bold mb-4"&gt;
          Top Wears for Women
        &lt;/h2&gt;
        &lt;ProductGrid products={products} loading={loading} error={error} /&gt;
      &lt;/div&gt;

      &lt;FeaturedCollection /&gt;
      &lt;FeaturesSection /&gt;
    &lt;/div&gt;
  );
};

export default Home;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Products/ProductGrid.jsx
import React from "react";
import { Link } from "react-router-dom";

const ProductGrid = ({ products, loading, error }) =&gt; {
  if (loading) {
    return &lt;p&gt;Loading...&lt;/p&gt;;
  }

  if (error) {
    return &lt;p&gt;Error: {error}&lt;/p&gt;;
  }
  return (
    &lt;div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"&gt;
      {products.map((product, index) =&gt; (
        &lt;Link key={index} to={`/product/${product._id}`} className="block"&gt;
          &lt;div className="bg-white p-4 rounded-lg"&gt;
            &lt;div className="w-full h-96 mb-4"&gt;
              &lt;img
                src={product.images&#91;0].url}
                alt={product.images&#91;0].altText || product.name}
                className="w-full h-full object-cover rounded-lg"
              /&gt;
            &lt;/div&gt;
            &lt;h3 className="text-sm mb-2"&gt;{product.name}&lt;/h3&gt;
            &lt;p className="text-gray-500 font-medium text-sm tracking-tighter"&gt;
              $ {product.price}
            &lt;/p&gt;
          &lt;/div&gt;
        &lt;/Link&gt;
      ))}
    &lt;/div&gt;
  );
};

export default ProductGrid;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Products/ProductDetails.jsx
import React, { useEffect, useState } from "react";
import { toast } from "sonner";
import ProductGrid from "./ProductGrid";
import { useParams } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import {
  fetchProductDetails,
  fetchSimilarProducts,
} from "../../redux/slices/productsSlice";
import { addToCart } from "../../redux/slices/cartSlice";

const ProductDetails = ({ productId }) =&gt; {
  const { id } = useParams();
  const dispatch = useDispatch();
  const { selectedProduct, loading, error, similarProducts } = useSelector(
    (state) =&gt; state.products
  );
  const { user, guestId } = useSelector((state) =&gt; state.auth);
  const &#91;mainImage, setMainImage] = useState(""); // 用來存儲主圖像的狀態，初始值為空字符串
  const &#91;selectedSize, setSelectedSize] = useState(""); // 用來存儲選擇的尺碼的狀態，初始值為空字符串
  const &#91;selectedColor, setSelectedColor] = useState(""); // 用來存儲選擇的顏色的狀態，初始值為空字符串
  const &#91;quantity, setQuantity] = useState(1); // 用來存儲選擇的購買數量的狀態，初始值為 1
  const &#91;isButtonDisabled, setIsButtonDisabled] = useState(false); // 用來控制按鈕是否禁用的狀態，初始值為 false (按鈕可用)

  const productFetchId = productId || id;

  useEffect(() =&gt; {
    if (productFetchId) {
      dispatch(fetchProductDetails(productFetchId));
      dispatch(fetchSimilarProducts({ id: productFetchId }));
    }
  }, &#91;dispatch, productFetchId]);

  // 使用 useEffect 當 selectedProduct 變化時更新主圖片
  useEffect(() =&gt; {
    // 檢查 selectedProduct 是否有圖片，並且圖片陣列長度大於 0
    if (selectedProduct?.images?.length &gt; 0) {
      // 更新主圖片為第一張圖片的 URL
      setMainImage(selectedProduct.images&#91;0].url);
    }
  }, &#91;selectedProduct]); // 當 selectedProduct 變化時執行這個效果

  // 定義一個函數來處理購買數量的增減操作
  const handleQuantityChange = (action) =&gt; {
    // 如果 action 是 "plus"，則增加數量
    if (action === "plus") setQuantity((prev) =&gt; prev + 1);
    // 如果 action 是 "minus" 且數量大於 1，則減少數量
    if (action === "minus" &amp;&amp; quantity &gt; 1) setQuantity((prev) =&gt; prev - 1);
  };

  // 定義函數處理將商品加入購物車的操作
  const handleAddToCart = () =&gt; {
    // 檢查用戶是否選擇了尺碼和顏色
    if (!selectedSize || !selectedColor) {
      // 如果沒有選擇尺碼或顏色，顯示錯誤通知
      toast.error("Please select a size and color before adding to cart.", {
        duration: 1000, // 設置錯誤通知顯示 1 秒
      });
      return; // 退出函數，不執行後續操作
    }

    // 禁用按鈕，防止重複點擊
    setIsButtonDisabled(true);

    dispatch(
      addToCart({
        productId: productFetchId,
        quantity,
        size: selectedSize,
        color: selectedColor,
        guestId,
        userId: user?._id,
      })
    )
      .then(() =&gt; {
        toast.success("Product added to cart!", {
          duration: 1000,
        });
      })
      .finally(() =&gt; {
        setIsButtonDisabled(false);
      });
  };

  if (loading) {
    return &lt;p&gt;Loading...&lt;/p&gt;;
  }

  if (error) {
    return &lt;p&gt;Error: {error}&lt;/p&gt;;
  }

  return (
    &lt;div className="p-6"&gt;
      {selectedProduct &amp;&amp; (
        &lt;div className="max-w-6xl mx-auto bg-white p-8 rounded-lg"&gt;
          &lt;div className="flex flex-col md:flex-row"&gt;
            {/* Left Thumbnails - 左側縮圖 */}
            &lt;div className="hidden md:flex flex-col space-y-4 mr-6"&gt;
              {selectedProduct.images.map((image, index) =&gt; (
                &lt;img
                  key={index}
                  src={image.url}
                  alt={image.altText || `Thumbnail ${index}`}
                  className={`w-20 h-20 object-cover rounded-lg cursor-pointer border ${
                    mainImage === image.url ? "border-black" : "border-gray-300"
                  }`}
                  onClick={() =&gt; setMainImage(image.url)}
                /&gt;
              ))}
            &lt;/div&gt;
            {/* Main Image - 主要的圖片 */}
            &lt;div className="md:w-1/2"&gt;
              &lt;div className="mb-4"&gt;
                {/* 僅當 mainImage 不是空字符串時，才渲染圖片 */}
                {mainImage &amp;&amp; (
                  &lt;img
                    src={mainImage}
                    alt="Main Product"
                    className="w-full h-auto object-cover rounded-lg"
                  /&gt;
                )}
              &lt;/div&gt;
            &lt;/div&gt;
            {/* Mobile Thumbnail - 手機板型縮圖 */}
            &lt;div className="md:hidden flex overflow-x-scroll space-x-4 mb-4"&gt;
              {selectedProduct.images.map((image, index) =&gt; (
                &lt;img
                  key={index}
                  src={image.url}
                  alt={image.altText || `Thumbnail ${index}`}
                  className={`w-20 h-20 object-cover rounded-lg cursor-pointer border ${
                    mainImage === image.url ? "border-black" : "border-gray-300"
                  }`}
                  onClick={() =&gt; setMainImage(image.url)}
                /&gt;
              ))}
            &lt;/div&gt;

            {/* Right Side - 右側 */}
            &lt;div className="md:w-1/2 md:ml-10"&gt;
              &lt;h1 className="text-2xl md:text-3xl font-semibold mb-2"&gt;
                {selectedProduct.name}
              &lt;/h1&gt;

              &lt;p className="text-lg text-gray-600 mb-1 line-through"&gt;
                {selectedProduct.originalPrice &amp;&amp;
                  `${selectedProduct.originalPrice}`}
              &lt;/p&gt;
              &lt;p className="text-xl text-gray-500 mb-2"&gt;
                $ {selectedProduct.price}
              &lt;/p&gt;
              &lt;p className="text-gray-600 mb-4"&gt;
                {selectedProduct.description}
              &lt;/p&gt;

              &lt;div className="mb-4"&gt;
                &lt;p className="text-gray-700"&gt;Color:&lt;/p&gt;
                &lt;div className="flex gap-2 mt-2"&gt;
                  {selectedProduct.colors.map((color) =&gt; (
                    &lt;button
                      key={color}
                      onClick={() =&gt; setSelectedColor(color)}
                      className={`w-8 h-8 rounded-full border ${
                        selectedColor === color
                          ? "border-4 border-black"
                          : "border-gray-300"
                      }`}
                      style={{
                        backgroundColor: color.toLocaleLowerCase(),
                        filter: "brightness(0.5)",
                      }}
                    &gt;&lt;/button&gt;
                  ))}
                &lt;/div&gt;
              &lt;/div&gt;

              &lt;div className="mb-4"&gt;
                &lt;p className="text-gray-700"&gt;Size:&lt;/p&gt;
                &lt;div className="flex gap-2 mt-2"&gt;
                  {selectedProduct.sizes.map((size) =&gt; (
                    &lt;button
                      key={size}
                      onClick={() =&gt; setSelectedSize(size)}
                      className={`px-4 py-2 rounded border ${
                        selectedSize === size ? "bg-black text-white" : ""
                      }`}
                    &gt;
                      {size}
                    &lt;/button&gt;
                  ))}
                &lt;/div&gt;
              &lt;/div&gt;

              &lt;div className="mb-6"&gt;
                &lt;p className="text-gray-700"&gt;Quantity:&lt;/p&gt;
                &lt;div className="flex items-center space-x-4 mt-2"&gt;
                  &lt;button
                    onClick={() =&gt; handleQuantityChange("minus")}
                    className="px-2 py-1 bg-gray-200 rounded text-lg"
                  &gt;
                    -
                  &lt;/button&gt;
                  &lt;span className="text-lg"&gt;{quantity}&lt;/span&gt;
                  &lt;button
                    onClick={() =&gt; handleQuantityChange("plus")}
                    className="px-2 py-1 bg-gray-200 rounded text-lg"
                  &gt;
                    +
                  &lt;/button&gt;
                &lt;/div&gt;
              &lt;/div&gt;

              &lt;button
                onClick={handleAddToCart}
                disabled={isButtonDisabled}
                className={`bg-black text-white py-2 px-6 rounded w-full mb-4 ${
                  isButtonDisabled
                    ? "cursor-not-allowed opacity-50"
                    : "hover:bg-gray-900"
                }`}
              &gt;
                {isButtonDisabled ? "Adding..." : "ADD TO CART"}
              &lt;/button&gt;

              &lt;div className="mt-10 text-gray-700"&gt;
                &lt;h3 className="text-xl font-bold mb-4"&gt;Characteristics:&lt;/h3&gt;
                &lt;table className="w-full text-left text-sm text-gray-600"&gt;
                  &lt;tbody&gt;
                    &lt;tr&gt;
                      &lt;td className="py-1"&gt;Brand&lt;/td&gt;
                      &lt;td className="py-1"&gt;{selectedProduct.brand}&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;tr&gt;
                      &lt;td className="py-1"&gt;Material&lt;/td&gt;
                      &lt;td className="py-1"&gt;{selectedProduct.material}&lt;/td&gt;
                    &lt;/tr&gt;
                  &lt;/tbody&gt;
                &lt;/table&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div className="mt-20"&gt;
            &lt;h2 className="text-2xl text-center font-medium mb-4"&gt;
              You May Also Like
            &lt;/h2&gt;
            &lt;ProductGrid
              products={similarProducts}
              loading={loading}
              error={error}
            /&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  );
};

export default ProductDetails;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/productsSlice.js
// 修正錯誤 - similarProducts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// Async Thunk to Fetch Products by Collection and optional Filters - 使用非同步函數根據集合和可選過濾條件獲取產品
export const fetchProductsByFilters = createAsyncThunk(
  "products/fetchByFilters",
  async ({
    collection,
    size,
    color,
    gender,
    minPrice,
    maxPrice,
    sortBy,
    search,
    category,
    material,
    brand,
    limit,
  }) =&gt; {
    const query = new URLSearchParams();
    if (collection) query.append("collection", collection);
    if (size) query.append("size", size);
    if (color) query.append("color", color);
    if (gender) query.append("gender", gender);
    if (minPrice) query.append("minPrice", minPrice);
    if (maxPrice) query.append("maxPrice", maxPrice);
    if (sortBy) query.append("sortBy", sortBy);
    if (search) query.append("search", search);
    if (category) query.append("category", category);
    if (material) query.append("material", material);
    if (brand) query.append("brand", brand);
    if (limit) query.append("limit", limit);

    const response = await axios.get(
      `${import.meta.env.VITE_BACKEND_URL}/api/products?${query.toString()}`
    );
    return response.data;
  }
);

// Async thunk to fetch a single product by ID - 使用非同步函數根據 ID 獲取單個產品
export const fetchProductDetails = createAsyncThunk(
  "products/fetchProductDetails",
  async (id) =&gt; {
    const response = await axios.get(
      `${import.meta.env.VITE_BACKEND_URL}/api/products/${id}`
    );
    return response.data;
  }
);

// Async thunk to fetch update existing products - 使用非同步函數獲取並更新現有產品
export const updateProduct = createAsyncThunk(
  "products/updateProduct",
  async ({ id, productData }) =&gt; {
    const response = await axios.put(
      `${import.meta.env.VITE_BACKEND_URL}/api/products/${id}`,
      productData,
      {
        headers: {
          Authorization: `Bearer ${localStorage.getItem("userToken")}`,
        },
      }
    );
    return response.data;
  }
);

// Async thunk to fetch similar products - 使用非同步函數獲取相似產品
export const fetchSimilarProducts = createAsyncThunk(
  "products/fetchSimilarProducts",
  async ({ id }) =&gt; {
    const response = await axios.get(
      `${import.meta.env.VITE_BACKEND_URL}/api/products/similar/${id}`
    );
    return response.data;
  }
);

const productsSlice = createSlice({
  name: "products",
  initialState: {
    products: &#91;],
    selectedProduct: null, // Store the details of the single Product - 儲存單一產品的詳細資料
    similarProducts: &#91;],
    loading: false,
    error: null,
    filters: {
      category: "",
      size: "",
      color: "",
      gender: "",
      brand: "",
      minPrice: "",
      maxPrice: "",
      sortBy: "",
      search: "",
      material: "",
      collection: "",
    },
  },
  reducers: {
    setFilters: (state, action) =&gt; {
      state.filters = { ...state.filters, ...action.payload };
    },
    clearFilters: (state) =&gt; {
      state.filters = {
        category: "",
        size: "",
        color: "",
        gender: "",
        brand: "",
        minPrice: "",
        maxPrice: "",
        sortBy: "",
        search: "",
        material: "",
        collection: "",
      };
    },
  },
  extraReducers: (builder) =&gt; {
    builder
      // Handle fetching products with filter - 處理使用篩選條件獲取產品
      .addCase(fetchProductsByFilters.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchProductsByFilters.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.products = Array.isArray(action.payload) ? action.payload : &#91;];
      })
      .addCase(fetchProductsByFilters.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      })
      // Handle fetching single product details - 處理獲取單一產品詳細資料
      .addCase(fetchProductDetails.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchProductDetails.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.selectedProduct = action.payload;
      })
      .addCase(fetchProductDetails.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      })
      // Handle updating product - 處理更新產品
      .addCase(updateProduct.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(updateProduct.fulfilled, (state, action) =&gt; {
        state.loading = false;
        const updatedProduct = action.payload;
        const index = state.products.findIndex(
          (product) =&gt; product._id === updateProduct._id
        );
        if (index !== -1) {
          state.products&#91;index] = updateProduct;
        }
      })
      .addCase(updateProduct.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      })
      // Handle fetching similar products - 處理獲取相似產品
      .addCase(fetchSimilarProducts.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchSimilarProducts.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.similarProducts = action.payload;
      })
      .addCase(fetchSimilarProducts.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      });
  },
});

export const { setFilters, clearFilters } = productsSlice.actions;
export default productsSlice.reducer;
</code></pre>



<h2 class="wp-block-heading">測試加入購物車功能</h2>



<ul class="wp-block-list">
<li>安裝 Redux DevTools<br>react redux chrome extension</li>



<li>檢查 > Redux > cart/addToCart/fulfilled > DIFF<br>ADD TO CART – 加到購物車</li>



<li>測試個別產品</li>
</ul>



<h2 class="wp-block-heading">出現錯誤 (個人問題)</h2>



<ul class="wp-block-list">
<li>產品無法加到購物車</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/cartSlice.js
// 除錯 addCase 的 addToCart
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// Helper function to load cart from localStorage - 從本地儲存載入購物車的輔助函數
const loadCartFromStorage = () =&gt; {
  const storedCart = localStorage.getItem("cart");
  return storedCart ? JSON.parse(storedCart) : { products: &#91;] };
};

// Helper function to save cart to localStorage - 將購物車儲存到本地儲存的輔助函數
const saveCartToStorage = (cart) =&gt; {
  localStorage.setItem("cart", JSON.stringify(cart));
};

// Fetch cart for a user or guest - 為用戶或訪客獲取購物車
export const fetchCart = createAsyncThunk(
  "cart/fetchCart",
  async ({ userId, guestId }, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.get(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        {
          params: { userId, guestId },
        }
      );
      return response.data;
    } catch (error) {
      console.error(error);
      return rejectWithValue(error.response.data);
    }
  }
);

// Add an item to the cart for a user or guest - 為用戶或訪客將商品加入購物車
export const addToCart = createAsyncThunk(
  "cart/addToCart",
  async (
    { productId, quantity, size, color, guestId, userId },
    { rejectWithValue }
  ) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        {
          productId,
          quantity,
          size,
          color,
          guestId,
          userId,
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Update the quantity of an item in the cart - 更新購物車中商品的數量
export const updateCartItemQuantity = createAsyncThunk(
  "cart/updateCartItemQuantity",
  async (
    { productId, quantity, guestId, userId, size, color },
    { rejectWithValue }
  ) =&gt; {
    try {
      const response = await axios.put(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        {
          productId,
          quantity,
          guestId,
          userId,
          size,
          color,
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Remove an item from the cart - 從購物車中移除商品
export const removeFromCart = createAsyncThunk(
  "cart/removeFromCart",
  async ({ productId, guestId, userId, size, color }, { rejectWithValue }) =&gt; {
    try {
      const response = await axios({
        method: "DELETE",
        url: `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        data: {
          productId,
          guestId,
          userId,
          size,
          color,
        },
      });
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Merge guest cart into user cart
export const mergeCart = createAsyncThunk(
  "cart/mergeCart",
  async ({ guestId, user }, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart/merge`,
        { guestId, user },
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

const cartSlice = createSlice({
  name: "cart",
  initialState: {
    cart: loadCartFromStorage(),
    loading: false,
    error: null,
  },
  reducers: {
    clearCart: (state) =&gt; {
      state.cart = { products: &#91;] };
      localStorage.removeItem("cart");
    },
  },
  extraReducers: (builder) =&gt; {
    builder
      .addCase(fetchCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(fetchCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message || "Failed to fetch cart";
      })
      .addCase(addToCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(addToCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.cart = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(addToCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload?.message || "Failed to add to cart";
      })
      .addCase(updateCartItemQuantity.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(updateCartItemQuantity.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(updateCartItemQuantity.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error =
          action.payload?.message || "Failed to update item quantity";
      })
      .addCase(removeFromCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(removeFromCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(removeFromCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload?.message || "Failed to remove item";
      })
      .addCase(mergeCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(mergeCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(mergeCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload?.message || "Failed to merge cart";
      });
  },
});

export const { clearCart } = cartSlice.actions;
export default cartSlice.reducer;
</code></pre>



<h2 class="wp-block-heading">Collection Section (系列區域)</h2>



<ul class="wp-block-list">
<li>Populating the collection (填充集合)</li>



<li>修改選單連結<br>測試桌面版、手機版選單連結是否正常運作</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/pages/CollectionPage.jsx
import React, { useEffect, useRef, useState } from "react";
import { FaFilter } from "react-icons/fa";
import FilterSidebar from "../components/Products/FilterSidebar";
import SortOptions from "../components/Products/SortOptions";
import ProductGrid from "../components/Products/ProductGrid";
import { useParams, useSearchParams } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { fetchProductsByFilters } from "../redux/slices/productsSlice";

const CollectionPage = () =&gt; {
  const { collection } = useParams();
  const &#91;searchParams] = useSearchParams();
  const dispatch = useDispatch();
  const { products, loading, error } = useSelector((state) =&gt; state.products);
  const queryParams = Object.fromEntries(&#91;...searchParams]);

  const sidebarRef = useRef(null);
  const &#91;isSidebarOpen, setIsSidebarOpen] = useState(false);

  useEffect(() =&gt; {
    dispatch(fetchProductsByFilters({ collection, ...queryParams }));
  }, &#91;dispatch, collection, searchParams]);

  const toggleSidebar = () =&gt; {
    setIsSidebarOpen(!isSidebarOpen);
  };

  const handleClickOutside = (e) =&gt; {
    // Close sidebar if clicked outside - 如果點擊在外部則關閉側邊欄
    if (sidebarRef.current &amp;&amp; !sidebarRef.current.contains(e.target)) {
      setIsSidebarOpen(false);
    }
  };

  useEffect(() =&gt; {
    // Add event listner for clicks - 為點擊事件添加事件監聽器
    document.addEventListener("mousedown", handleClickOutside);
    // clean event listener - 清除事件監聽器
    return () =&gt; {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, &#91;]);

  return (
    &lt;div className="flex flex-col lg:flex-row"&gt;
      {/* Mobile Filter button - 手機篩選按鈕  */}
      &lt;button
        onClick={toggleSidebar}
        className="lg:hidden border p-2 flex justify-center items-center"
      &gt;
        &lt;FaFilter className="mr-2" /&gt; Filters
      &lt;/button&gt;

      {/* Filter Sidebar - 篩選側邊欄 */}
      &lt;div
        ref={sidebarRef}
        className={`${
          isSidebarOpen ? "translate-x-0" : "-translate-x-full"
        } fixed inset-y-0 z-50 left-0 w-64 bg-white overflow-y-auto transition-transform duration-300 lg:static lg:translate-x-0`}
      &gt;
        &lt;FilterSidebar /&gt;
      &lt;/div&gt;
      &lt;div className="flex-grow p-4"&gt;
        &lt;h2 className="text-2xl uppercase mb-4"&gt;All Collection&lt;/h2&gt;

        {/* Sort Options - 排序選項 */}
        &lt;SortOptions /&gt;

        {/* Product Grid - 產品網格  */}
        &lt;ProductGrid products={products} loading={loading} error={error} /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default CollectionPage;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Common/Navbar.jsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import {
  HiOutlineUser,
  HiOutlineShoppingBag,
  HiBars3BottomRight,
} from "react-icons/hi2";
import SearchBar from "./SearchBar";
import CartDrawer from "../Layout/CartDrawer";
import { IoMdClose } from "react-icons/io";

const Navbar = () =&gt; {
  const &#91;drawerOpen, setDrawerOpen] = useState(false);
  const &#91;navDrawerOpen, setNavDrawerOpen] = useState(false);

  const toggleNavDrawer = () =&gt; {
    setNavDrawerOpen(!navDrawerOpen);
  };

  const toggleCartDrawer = () =&gt; {
    setDrawerOpen(!drawerOpen);
  };

  return (
    &lt;&gt;
      &lt;nav className="container mx-auto flex items-center justify-between py-4 px-6"&gt;
        {/* Left - Logo -&gt; 左側 - 商標、標誌 */}
        &lt;div&gt;
          &lt;Link to="/" className="text-2xl font-medium"&gt;
            Rabbit
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Center - Navigation Links -&gt; 中間 - 導覽連結 */}
        &lt;div className="hidden md:flex space-x-6"&gt;
          &lt;Link
            to="/collections/all?gender=Men"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Men
          &lt;/Link&gt;
          &lt;Link
            to="/collections/all?gender=Women"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Women
          &lt;/Link&gt;
          &lt;Link
            to="/collections/all?category=Top Wear"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Top Wear
          &lt;/Link&gt;
          &lt;Link
            to="/collections/all?category=Bottom Wear"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Bottom Wear
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Right - Icons -&gt; 右側 - 圖示 */}
        &lt;div className="flex items-center space-x-4"&gt;
          &lt;Link
            to="/admin"
            className="block bg-black px-2 rounded text-sm text-white"
          &gt;
            Admin
          &lt;/Link&gt;
          &lt;Link to="/profile" className="hover:text-black"&gt;
            &lt;HiOutlineUser className="h-6 w-6 text-gray-700" /&gt;
          &lt;/Link&gt;
          &lt;button
            onClick={toggleCartDrawer}
            className="relative hover:text-black"
          &gt;
            &lt;HiOutlineShoppingBag className="h-6 w-6 text-gray-700" /&gt;
            &lt;span className="absolute -top-1 bg-rabbit-red text-white text-xs rounded-full px-2 py-0.5"&gt;
              4
            &lt;/span&gt;
          &lt;/button&gt;
          {/* Search - 搜尋 */}
          &lt;div className="overflow-hidden"&gt;
            &lt;SearchBar /&gt;
          &lt;/div&gt;

          &lt;button onClick={toggleNavDrawer} className="md:hidden"&gt;
            &lt;HiBars3BottomRight className="h-6 w-6 text-gray-700" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/nav&gt;
      &lt;CartDrawer drawerOpen={drawerOpen} toggleCartDrawer={toggleCartDrawer} /&gt;

      {/* Mobile Navigation - 手機版導覽 */}
      &lt;div
        className={`fixed top-0 left-0 w-3/4 sm:w-1/2 md:w-1/3 h-full bg-white shadow-lg transform transition-transform duration-300 z-50 ${
          navDrawerOpen ? "translate-x-0" : "-translate-x-full"
        }`}
      &gt;
        &lt;div className="flex justify-end p-4"&gt;
          &lt;button onClick={toggleNavDrawer}&gt;
            &lt;IoMdClose className="h-6 w-6 text-gray-600" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
        &lt;div className="p-4"&gt;
          &lt;h2 className="text-xl font-semibold mb-4"&gt;Menu&lt;/h2&gt;
          &lt;nav className="space-y-4"&gt;
            &lt;Link
              to="/collections/all?gender=Men"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Men
            &lt;/Link&gt;
            &lt;Link
              to="/collections/all?gender=Women"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Women
            &lt;/Link&gt;
            &lt;Link
              to="/collections/all?category=Top Wear"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Top Wear
            &lt;/Link&gt;
            &lt;Link
              to="/collections/all?category=Bottom Wear"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Bottom Wear
            &lt;/Link&gt;
          &lt;/nav&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default Navbar;
</code></pre>



<h2 class="wp-block-heading">Cart Functionality (購物車功能)</h2>



<pre class="wp-block-code"><code>// frontend/src/components/Layout/CartDrawer.jsx
import React, { useState } from "react";
import { IoMdClose } from "react-icons/io";
import CartContents from "../Cart/CartContents";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";

const CartDrawer = ({ drawerOpen, toggleCartDrawer }) =&gt; {
  const navigate = useNavigate();
  const { user, guestId } = useSelector((state) =&gt; state.auth);
  const { cart } = useSelector((state) =&gt; state.cart);
  const userId = user ? user._id : null;

  const handleCheckout = () =&gt; {
    toggleCartDrawer();
    if (!user) {
      navigate("/login?redirect=checkout");
    } else {
      navigate("/checkout");
    }
  };
  return (
    &lt;div
      className={`fixed top-0 right-0 w-3/4 sm:w-1/2 md:w-&#91;30rem] h-full bg-white shadow-lg transform transition-transform duration-300 flex flex-col z-50 ${
        drawerOpen ? "translate-x-0" : "translate-x-full"
      }`}
    &gt;
      {/* Close Button - 關閉的按鈕 */}
      &lt;div className="flex justify-end p-4"&gt;
        &lt;button onClick={toggleCartDrawer}&gt;
          &lt;IoMdClose className="h-6 w-6 text-gray-600" /&gt;
        &lt;/button&gt;
      &lt;/div&gt;
      {/* Cart contents with scrollable area - 具有可滾動區域的購物車內容 */}
      &lt;div className="flex-grow p-4 overflow-y-auto"&gt;
        &lt;h2 className="text-xl font-semibold mb-4"&gt;Your Cart&lt;/h2&gt;
        {/* Component for Cart Contents - 購物車內容組件 */}
        {cart &amp;&amp; cart?.products?.length &gt; 0 ? (
          &lt;CartContents cart={cart} userId={userId} guestId={guestId} /&gt;
        ) : (
          &lt;p&gt;Your cart is empty.&lt;/p&gt;
        )}
      &lt;/div&gt;

      {/* Checkout button fixed at the bottom - 結帳按鈕固定在底部 */}
      &lt;div className="p-4 bg-white sticky bottom-0"&gt;
        {cart &amp;&amp; cart?.products?.length &gt; 0 &amp;&amp; (
          &lt;&gt;
            &lt;button
              onClick={handleCheckout}
              className="w-full bg-black text-white py-3 rounded-lg font-semibold hover:bg-gray-800 transition"
            &gt;
              Checkout
            &lt;/button&gt;
            &lt;p className="text-sm tracking-tighter text-gray-500 mt-2 text-center"&gt;
              Shipping, taxes, and discount codes calculated at checkout.
            &lt;/p&gt;
          &lt;/&gt;
        )}
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default CartDrawer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Cart/CartContent.jsx
import React from "react";
import { RiDeleteBin3Line } from "react-icons/ri";
import { useDispatch } from "react-redux";
import {
  removeFromCart,
  updateCartItemQuantity,
} from "../../redux/slices/cartSlice";

const CartContents = ({ cart, userId, guestId }) =&gt; {
  const dispatch = useDispatch();

  // Handle adding or substracting to cart - 處理加入或減少購物車的商品
  const handleAddToCart = (productId, delta, quantity, size, color) =&gt; {
    const newQuantity = quantity + delta;
    if (newQuantity &gt;= 1) {
      dispatch(
        updateCartItemQuantity({
          productId,
          quantity: newQuantity,
          guestId,
          userId,
          size,
          color,
        })
      );
    }
  };

  const handleRemoveFromCart = (productId, size, color) =&gt; {
    dispatch(removeFromCart({ productId, guestId, userId, size, color }));
  };

  return (
    &lt;div&gt;
      {cart.products.map((product, index) =&gt; (
        &lt;div
          key={index}
          className="flex items-start justify-between py-4 border-b"
        &gt;
          &lt;div className="flex items-start"&gt;
            &lt;img
              src={product.image}
              alt={product.name}
              className="w-20 h-24 object-cover mr-4 rounded"
            /&gt;
            &lt;div&gt;
              &lt;h3&gt;{product.name}&lt;/h3&gt;
              &lt;p className="text-sm text-gray-500"&gt;
                size: {product.size} | color: {product.color}
              &lt;/p&gt;
              &lt;div className="flex items-center mt-2"&gt;
                &lt;button
                  onClick={() =&gt;
                    handleAddToCart(
                      product.productId,
                      -1,
                      product.quantity,
                      product.size,
                      product.color
                    )
                  }
                  className="border rounded px-2 py-1 text-xl font-medium"
                &gt;
                  -
                &lt;/button&gt;
                &lt;span className="mx-4"&gt;{product.quantity}&lt;/span&gt;
                &lt;button
                  onClick={() =&gt;
                    handleAddToCart(
                      product.productId,
                      1,
                      product.quantity,
                      product.size,
                      product.color
                    )
                  }
                  className="border rounded px-2 py-1 text-xl font-medium"
                &gt;
                  +
                &lt;/button&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div&gt;
            &lt;p&gt;$ {product.price.toLocaleString()}&lt;/p&gt;
            &lt;button
              onClick={() =&gt;
                handleRemoveFromCart(
                  product.productId,
                  product.size,
                  product.color
                )
              }
            &gt;
              &lt;RiDeleteBin3Line className="h-6 w-6 mt-2 text-red-600" /&gt;
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
};

export default CartContents;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Common/Navbar.jsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import {
  HiOutlineUser,
  HiOutlineShoppingBag,
  HiBars3BottomRight,
} from "react-icons/hi2";
import SearchBar from "./SearchBar";
import CartDrawer from "../Layout/CartDrawer";
import { IoMdClose } from "react-icons/io";
import { useSelector } from "react-redux";

const Navbar = () =&gt; {
  const &#91;drawerOpen, setDrawerOpen] = useState(false);
  const &#91;navDrawerOpen, setNavDrawerOpen] = useState(false);
  const { cart } = useSelector((state) =&gt; state.cart);

  const cartItemCount =
    cart?.products?.reduce((total, product) =&gt; total + product.quantity, 0) ||
    0;

  const toggleNavDrawer = () =&gt; {
    setNavDrawerOpen(!navDrawerOpen);
  };

  const toggleCartDrawer = () =&gt; {
    setDrawerOpen(!drawerOpen);
  };

  return (
    &lt;&gt;
      &lt;nav className="container mx-auto flex items-center justify-between py-4 px-6"&gt;
        {/* Left - Logo -&gt; 左側 - 商標、標誌 */}
        &lt;div&gt;
          &lt;Link to="/" className="text-2xl font-medium"&gt;
            Rabbit
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Center - Navigation Links -&gt; 中間 - 導覽連結 */}
        &lt;div className="hidden md:flex space-x-6"&gt;
          &lt;Link
            to="/collections/all?gender=Men"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Men
          &lt;/Link&gt;
          &lt;Link
            to="/collections/all?gender=Women"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Women
          &lt;/Link&gt;
          &lt;Link
            to="/collections/all?category=Top Wear"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Top Wear
          &lt;/Link&gt;
          &lt;Link
            to="/collections/all?category=Bottom Wear"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Bottom Wear
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Right - Icons -&gt; 右側 - 圖示 */}
        &lt;div className="flex items-center space-x-4"&gt;
          &lt;Link
            to="/admin"
            className="block bg-black px-2 rounded text-sm text-white"
          &gt;
            Admin
          &lt;/Link&gt;
          &lt;Link to="/profile" className="hover:text-black"&gt;
            &lt;HiOutlineUser className="h-6 w-6 text-gray-700" /&gt;
          &lt;/Link&gt;
          &lt;button
            onClick={toggleCartDrawer}
            className="relative hover:text-black"
          &gt;
            &lt;HiOutlineShoppingBag className="h-6 w-6 text-gray-700" /&gt;
            {cartItemCount &gt; 0 &amp;&amp; (
              &lt;span className="absolute -top-1 bg-rabbit-red text-white text-xs rounded-full px-2 py-0.5"&gt;
                {cartItemCount}
              &lt;/span&gt;
            )}
          &lt;/button&gt;
          {/* Search - 搜尋 */}
          &lt;div className="overflow-hidden"&gt;
            &lt;SearchBar /&gt;
          &lt;/div&gt;

          &lt;button onClick={toggleNavDrawer} className="md:hidden"&gt;
            &lt;HiBars3BottomRight className="h-6 w-6 text-gray-700" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/nav&gt;
      &lt;CartDrawer drawerOpen={drawerOpen} toggleCartDrawer={toggleCartDrawer} /&gt;

      {/* Mobile Navigation - 手機版導覽 */}
      &lt;div
        className={`fixed top-0 left-0 w-3/4 sm:w-1/2 md:w-1/3 h-full bg-white shadow-lg transform transition-transform duration-300 z-50 ${
          navDrawerOpen ? "translate-x-0" : "-translate-x-full"
        }`}
      &gt;
        &lt;div className="flex justify-end p-4"&gt;
          &lt;button onClick={toggleNavDrawer}&gt;
            &lt;IoMdClose className="h-6 w-6 text-gray-600" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
        &lt;div className="p-4"&gt;
          &lt;h2 className="text-xl font-semibold mb-4"&gt;Menu&lt;/h2&gt;
          &lt;nav className="space-y-4"&gt;
            &lt;Link
              to="/collections/all?gender=Men"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Men
            &lt;/Link&gt;
            &lt;Link
              to="/collections/all?gender=Women"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Women
            &lt;/Link&gt;
            &lt;Link
              to="/collections/all?category=Top Wear"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Top Wear
            &lt;/Link&gt;
            &lt;Link
              to="/collections/all?category=Bottom Wear"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Bottom Wear
            &lt;/Link&gt;
          &lt;/nav&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default Navbar;
</code></pre>



<h2 class="wp-block-heading">出現錯誤 (個人問題)</h2>



<ul class="wp-block-list">
<li>購物車抽屜數量無法增加、減少、刪除</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/cartSlices.js
// 除錯 addCase 的 fetchCart、updateCartItemQuantity、removeFromCart、mergeCart
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// Helper function to load cart from localStorage - 從本地儲存載入購物車的輔助函數
const loadCartFromStorage = () =&gt; {
  const storedCart = localStorage.getItem("cart");
  return storedCart ? JSON.parse(storedCart) : { products: &#91;] };
};

// Helper function to save cart to localStorage - 將購物車儲存到本地儲存的輔助函數
const saveCartToStorage = (cart) =&gt; {
  localStorage.setItem("cart", JSON.stringify(cart));
};

// Fetch cart for a user or guest - 為用戶或訪客獲取購物車
export const fetchCart = createAsyncThunk(
  "cart/fetchCart",
  async ({ userId, guestId }, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.get(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        {
          params: { userId, guestId },
        }
      );
      return response.data;
    } catch (error) {
      console.error(error);
      return rejectWithValue(error.response.data);
    }
  }
);

// Add an item to the cart for a user or guest - 為用戶或訪客將商品加入購物車
export const addToCart = createAsyncThunk(
  "cart/addToCart",
  async (
    { productId, quantity, size, color, guestId, userId },
    { rejectWithValue }
  ) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        {
          productId,
          quantity,
          size,
          color,
          guestId,
          userId,
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Update the quantity of an item in the cart - 更新購物車中商品的數量
export const updateCartItemQuantity = createAsyncThunk(
  "cart/updateCartItemQuantity",
  async (
    { productId, quantity, guestId, userId, size, color },
    { rejectWithValue }
  ) =&gt; {
    try {
      const response = await axios.put(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        {
          productId,
          quantity,
          guestId,
          userId,
          size,
          color,
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Remove an item from the cart - 從購物車中移除商品
export const removeFromCart = createAsyncThunk(
  "cart/removeFromCart",
  async ({ productId, guestId, userId, size, color }, { rejectWithValue }) =&gt; {
    try {
      const response = await axios({
        method: "DELETE",
        url: `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        data: {
          productId,
          guestId,
          userId,
          size,
          color,
        },
      });
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Merge guest cart into user cart
export const mergeCart = createAsyncThunk(
  "cart/mergeCart",
  async ({ guestId, user }, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart/merge`,
        { guestId, user },
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

const cartSlice = createSlice({
  name: "cart",
  initialState: {
    cart: loadCartFromStorage(),
    loading: false,
    error: null,
  },
  reducers: {
    clearCart: (state) =&gt; {
      state.cart = { products: &#91;] };
      localStorage.removeItem("cart");
    },
  },
  extraReducers: (builder) =&gt; {
    builder
      .addCase(fetchCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.cart = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(fetchCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message || "Failed to fetch cart";
      })
      .addCase(addToCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(addToCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.cart = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(addToCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload?.message || "Failed to add to cart";
      })
      .addCase(updateCartItemQuantity.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(updateCartItemQuantity.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.cart = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(updateCartItemQuantity.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error =
          action.payload?.message || "Failed to update item quantity";
      })
      .addCase(removeFromCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(removeFromCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.cart = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(removeFromCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload?.message || "Failed to remove item";
      })
      .addCase(mergeCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(mergeCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.cart = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(mergeCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload?.message || "Failed to merge cart";
      });
  },
});

export const { clearCart } = cartSlice.actions;
export default cartSlice.reducer;
</code></pre>



<h2 class="wp-block-heading">Search Bar (搜尋欄)</h2>



<pre class="wp-block-code"><code>// frontend/src/components/Common/SearchBar.jsx
import React, { useState } from "react";
import { HiMagnifyingGlass, HiMiniXMark } from "react-icons/hi2";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import {
  fetchProductsByFilters,
  setFilters,
} from "../../redux/slices/productsSlice";

const SearchBar = () =&gt; {
  const &#91;searchTerm, setSearchTerm] = useState("");
  const &#91;isOpen, setIsOpen] = useState(false);
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const handleSearchToggle = () =&gt; {
    setIsOpen(!isOpen);
  };

  const handleSearch = (e) =&gt; {
    e.preventDefault();
    // console.log("Search Term:", searchTerm);
    dispatch(setFilters({ search: searchTerm }));
    dispatch(fetchProductsByFilters({ search: searchTerm }));
    navigate(`/collections/all?search=${searchTerm}`);
    setIsOpen(false);
  };

  return (
    &lt;div
      className={`flex items-center justify-center w-full transition-all duration-300 ${
        isOpen ? "absolute top-0 left-0 w-full bg-white h-24 z-50" : "w-auto"
      }`}
    &gt;
      {isOpen ? (
        &lt;form
          onSubmit={handleSearch}
          className="relative flex items-center justify-center w-full"
        &gt;
          &lt;div className="relative w-1/2"&gt;
            &lt;input
              type="text"
              placeholder="Search"
              value={searchTerm}
              onChange={(e) =&gt; setSearchTerm(e.target.value)}
              className="bg-gray-100 px-4 py-2 pl-2 pr-12 rounded-lg focus:outline-none w-full placeholder:text-gray-700"
            /&gt;
            {/* search icon - 搜尋圖示 */}
            &lt;button
              type="submit"
              className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-800"
            &gt;
              &lt;HiMagnifyingGlass className="h-6 w-6" /&gt;
            &lt;/button&gt;
          &lt;/div&gt;
          {/* close button - 關閉的按鈕 */}
          &lt;button
            type="button"
            onClick={handleSearchToggle}
            className="absolute right-4 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-800"
          &gt;
            &lt;HiMiniXMark className="h-6 w-6" /&gt;
          &lt;/button&gt;
        &lt;/form&gt;
      ) : (
        &lt;button onClick={handleSearchToggle}&gt;
          &lt;HiMagnifyingGlass className="h-6 w-6" /&gt;
        &lt;/button&gt;
      )}
    &lt;/div&gt;
  );
};

export default SearchBar;
</code></pre>



<h2 class="wp-block-heading">Authentication Process (身份驗證過程)</h2>



<h2 class="wp-block-heading">製作登入帳號</h2>



<ul class="wp-block-list">
<li>帳號登入情況下 /login 會轉址到首頁</li>



<li>檢查 > Application<br>查看本地儲存是否有 userInfo、userToken</li>



<li>清除本地端儲存 /login 將不會轉址到首頁</li>



<li>除錯: 帳號登入不會跳轉<br>addCase 的 loginUser</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/pages/Login.jsx
import React, { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import login from "../assets/login.webp";
import { loginUser } from "../redux/slices/authSlice";
import { useDispatch, useSelector } from "react-redux";
import { mergeCart } from "../redux/slices/cartSlice";

const Login = () =&gt; {
  const &#91;email, setEmail] = useState("");
  const &#91;password, setPassword] = useState("");
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const location = useLocation();
  const { user, guestId } = useSelector((state) =&gt; state.auth);
  const { cart } = useSelector((state) =&gt; state.cart);

  // Get redirect parameter and check if it's checkout or something - 獲取重定向參數並檢查它是否為結帳或其他東西
  const redirect = new URLSearchParams(location.search).get("redirect") || "/";
  const isCheckoutRedirect = redirect.includes("checkout");

  useEffect(() =&gt; {
    if (user) {
      if (cart?.products.length &gt; 0 &amp;&amp; guestId) {
        dispatch(mergeCart({ guestId, user })).then(() =&gt; {
          navigate(isCheckoutRedirect ? "/checkout" : "/");
        });
      } else {
        navigate(isCheckoutRedirect ? "/checkout" : "/");
      }
    }
  }, &#91;user, guestId, cart, navigate, isCheckoutRedirect, dispatch]);

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    // console.log("User Login:", { email, password });
    dispatch(loginUser({ email, password }));
  };

  return (
    &lt;div className="flex"&gt;
      &lt;div className="w-full md:w-1/2 flex flex-col justify-center items-center p-8 md:p-12"&gt;
        &lt;form
          onSubmit={handleSubmit}
          className="w-full max-w-md bg-white p-8 rounded-lg border shadow-sm"
        &gt;
          &lt;div className="flex justify-center mb-6"&gt;
            &lt;h2 className="text-xl font-medium"&gt;Rabbit&lt;/h2&gt;
          &lt;/div&gt;
          &lt;h2 className="text-2xl font-bold text-center mb-6"&gt;Hey there! &lt;/h2&gt;
          &lt;p className="text-center mb-6"&gt;
            Enter your username and password to Login.
          &lt;/p&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Email&lt;/label&gt;
            &lt;input
              type="email"
              value={email}
              onChange={(e) =&gt; setEmail(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your email address"
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Password&lt;/label&gt;
            &lt;input
              type="password"
              value={password}
              onChange={(e) =&gt; setPassword(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your password"
            /&gt;
          &lt;/div&gt;
          &lt;button
            type="submit"
            className="w-full bg-black text-white p-2 rounded-lg font-semibold hover:bg-gray-800 transition"
          &gt;
            Sign In
          &lt;/button&gt;
          &lt;p className="mt-6 text-center text-sm"&gt;
            Don't have an account?{" "}
            &lt;Link
              to={`/register?redirect=${encodeURIComponent(redirect)}`}
              className="text-blue-500"
            &gt;
              Register
            &lt;/Link&gt;
          &lt;/p&gt;
        &lt;/form&gt;
      &lt;/div&gt;

      &lt;div className="hidden md:block w-1/2 bg-gray-800"&gt;
        &lt;div className="h-full flex flex-col justify-center items-center"&gt;
          &lt;img
            src={login}
            alt="Login to Account"
            className="h-&#91;750px] w-full object-cover"
          /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Login;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/authSlice.js
// 除錯: 帳號登入不會跳轉
// addCase 的 loginUser
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// Retrieve user info and token from localStorage if available - 如果可用，從本地存儲中獲取用戶資訊和令牌
const userFromStorage = localStorage.getItem("userInfo")
  ? JSON.parse(localStorage.getItem("userInfo"))
  : null;

// Check for an existing guest ID in the localStorage or generate a new one - 檢查本地存儲中是否已有現有的訪客 ID，若沒有則生成一個新的
const initialGuestId =
  localStorage.getItem("guestId") || `guest_${new Date().getTime()}`;
localStorage.setItem("guestId", initialGuestId);

// Initial state - 初始狀態
const initialState = {
  user: userFromStorage,
  guestId: initialGuestId,
  loading: false,
  error: null,
};

// Async Thunk for User Login - 用於用戶登入的非同步延遲函數
export const loginUser = createAsyncThunk(
  "auth/loginUser",
  async (userData, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/users/login`,
        userData
      );
      localStorage.setItem("userInfo", JSON.stringify(response.data.user));
      localStorage.setItem("userToken", response.data.token);

      return response.data.user; // Return the user object from the response - 從回應中返回用戶對象
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Async Thunk for User Registration - 用於用戶註冊的非同步延遲函數
export const registerUser = createAsyncThunk(
  "auth/registerUser",
  async (userData, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/users/register`,
        userData
      );
      localStorage.setItem("userInfo", JSON.stringify(response.data.user));
      localStorage.setItem("userToken", response.data.token);

      return response.data.user; // Return the user object from the response - 從回應中返回用戶對象
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Slice - 切片
const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {
    logout: (state) =&gt; {
      state.user = null;
      state.guestId = `guest_${new Date().getTime()}`; // Reset guest ID on logout - 在登出時重置訪客 ID
      localStorage.removeItem("userInfo");
      localStorage.removeItem("userToken");
      localStorage.setItem("guestId", state.guestId); // Set new guest ID in localStorage - 在本地存儲中設置新的訪客 ID
    },
    generateNewGuestId: (state) =&gt; {
      state.guestId = `guest_${new Date().getTime()}`;
      localStorage.setItem("guestId", state.guestId);
    },
  },
  extraReducers: (builder) =&gt; {
    builder
      .addCase(loginUser.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(loginUser.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.user = action.payload;
      })
      .addCase(loginUser.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload.message;
      })
      .addCase(registerUser.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(registerUser.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
      })
      .addCase(registerUser.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload.message;
      });
  },
});

export const { logout, generateNewGuestId } = authSlice.actions;
export default authSlice.reducer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Profile.jsx
import React, { useEffect } from "react";
import MyOrdersPage from "./MyOrdersPage";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { logout } from "../redux/slices/authSlice";
import { clearCart } from "../redux/slices/cartSlice";

const Profile = () =&gt; {
  const { user } = useSelector((state) =&gt; state.auth);
  const navigate = useNavigate();
  const dispatch = useDispatch();

  useEffect(() =&gt; {
    if (!user) {
      navigate("/login");
    }
  }, &#91;user, navigate]);

  const handleLogout = () =&gt; {
    dispatch(logout());
    dispatch(clearCart());
    navigate("/login");
  };

  return (
    &lt;div className="min-h-screen flex flex-col"&gt;
      &lt;div className="flex-grow container mx-auto p-4 md:p-6"&gt;
        &lt;div className="flex flex-col md:flex-row md:space-x-6 space-y-6 md:space-y-0"&gt;
          {/* Left Section - 左側區域 */}
          &lt;div className="w-full md:w-1/3 lg:w-1/4 shadow-md rounded-lg p-6"&gt;
            &lt;h1 className="text-2xl md:text-3xl font-bold mb-4"&gt;
              {user?.name}
            &lt;/h1&gt;
            &lt;p className="text-lg text-gray-600 mb-4"&gt;{user?.email}&lt;/p&gt;
            &lt;button
              onClick={handleLogout}
              className="w-full bg-red-500 text-white py-2 px-4 rounded hover:bg-red-600"
            &gt;
              Logout
            &lt;/button&gt;
          &lt;/div&gt;
          {/* Right Section: Orders table - 右側區域: 訂單表格 */}
          &lt;div className="w-full md:w-2/3 lg:w-3/4"&gt;
            &lt;MyOrdersPage /&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Profile;
</code></pre>



<h2 class="wp-block-heading">製作註冊帳號</h2>



<ul class="wp-block-list">
<li>除錯: 註冊帳號不會跳轉</li>



<li>測試合併功能<br>未登入時產品加入購物車結帳、登入是否會合併<br>未登入時產品加入購物車結帳、註冊是否會合併</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/pages/Register.jsx
import React, { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import register from "../assets/register.webp";
import { registerUser } from "../redux/slices/authSlice";
import { useDispatch, useSelector } from "react-redux";
import { mergeCart } from "../redux/slices/cartSlice";

const Register = () =&gt; {
  const &#91;name, setName] = useState("");
  const &#91;email, setEmail] = useState("");
  const &#91;password, setPassword] = useState("");
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const location = useLocation();
  const { user, guestId } = useSelector((state) =&gt; state.auth);
  const { cart } = useSelector((state) =&gt; state.cart);

  // Get redirect parameter and check if it's checkout or something - 獲取重定向參數並檢查它是否為結帳或其他東西
  const redirect = new URLSearchParams(location.search).get("redirect") || "/";
  const isCheckoutRedirect = redirect.includes("checkout");

  useEffect(() =&gt; {
    if (user) {
      if (cart?.products.length &gt; 0 &amp;&amp; guestId) {
        dispatch(mergeCart({ guestId, user })).then(() =&gt; {
          navigate(isCheckoutRedirect ? "/checkout" : "/");
        });
      } else {
        navigate(isCheckoutRedirect ? "/checkout" : "/");
      }
    }
  }, &#91;user, guestId, cart, navigate, isCheckoutRedirect, dispatch]);

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    // console.log("User Registered:", { name, email, password });
    dispatch(registerUser({ name, email, password }));
  };

  return (
    &lt;div className="flex"&gt;
      &lt;div className="w-full md:w-1/2 flex flex-col justify-center items-center p-8 md:p-12"&gt;
        &lt;form
          onSubmit={handleSubmit}
          className="w-full max-w-md bg-white p-8 rounded-lg border shadow-sm"
        &gt;
          &lt;div className="flex justify-center mb-6"&gt;
            &lt;h2 className="text-xl font-medium"&gt;Rabbit&lt;/h2&gt;
          &lt;/div&gt;
          &lt;h2 className="text-2xl font-bold text-center mb-6"&gt;Hey there! &lt;/h2&gt;
          &lt;p className="text-center mb-6"&gt;
            Enter your username and password to Login.
          &lt;/p&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Name&lt;/label&gt;
            &lt;input
              type="text"
              value={name}
              onChange={(e) =&gt; setName(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your name"
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Email&lt;/label&gt;
            &lt;input
              type="email"
              value={email}
              onChange={(e) =&gt; setEmail(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your email address"
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Password&lt;/label&gt;
            &lt;input
              type="password"
              value={password}
              onChange={(e) =&gt; setPassword(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your password"
            /&gt;
          &lt;/div&gt;
          &lt;button
            type="submit"
            className="w-full bg-black text-white p-2 rounded-lg font-semibold hover:bg-gray-800 transition"
          &gt;
            Sign Up
          &lt;/button&gt;
          &lt;p className="mt-6 text-center text-sm"&gt;
            Don't have an account?{" "}
            &lt;Link
              to={`/login?redirect=${encodeURIComponent(redirect)}`}
              className="text-blue-500"
            &gt;
              Login
            &lt;/Link&gt;
          &lt;/p&gt;
        &lt;/form&gt;
      &lt;/div&gt;

      &lt;div className="hidden md:block w-1/2 bg-gray-800"&gt;
        &lt;div className="h-full flex flex-col justify-center items-center"&gt;
          &lt;img
            src={register}
            alt="Login to Account"
            className="h-&#91;750px] w-full object-cover"
          /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Register;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/authSlice.js
// 除錯: 註冊帳號不會跳轉
// addCase 的 registerUser
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// Retrieve user info and token from localStorage if available - 如果可用，從本地存儲中獲取用戶資訊和令牌
const userFromStorage = localStorage.getItem("userInfo")
  ? JSON.parse(localStorage.getItem("userInfo"))
  : null;

// Check for an existing guest ID in the localStorage or generate a new one - 檢查本地存儲中是否已有現有的訪客 ID，若沒有則生成一個新的
const initialGuestId =
  localStorage.getItem("guestId") || `guest_${new Date().getTime()}`;
localStorage.setItem("guestId", initialGuestId);

// Initial state - 初始狀態
const initialState = {
  user: userFromStorage,
  guestId: initialGuestId,
  loading: false,
  error: null,
};

// Async Thunk for User Login - 用於用戶登入的非同步延遲函數
export const loginUser = createAsyncThunk(
  "auth/loginUser",
  async (userData, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/users/login`,
        userData
      );
      localStorage.setItem("userInfo", JSON.stringify(response.data.user));
      localStorage.setItem("userToken", response.data.token);

      return response.data.user; // Return the user object from the response - 從回應中返回用戶對象
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Async Thunk for User Registration - 用於用戶註冊的非同步延遲函數
export const registerUser = createAsyncThunk(
  "auth/registerUser",
  async (userData, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/users/register`,
        userData
      );
      localStorage.setItem("userInfo", JSON.stringify(response.data.user));
      localStorage.setItem("userToken", response.data.token);

      return response.data.user; // Return the user object from the response - 從回應中返回用戶對象
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Slice - 切片
const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {
    logout: (state) =&gt; {
      state.user = null;
      state.guestId = `guest_${new Date().getTime()}`; // Reset guest ID on logout - 在登出時重置訪客 ID
      localStorage.removeItem("userInfo");
      localStorage.removeItem("userToken");
      localStorage.setItem("guestId", state.guestId); // Set new guest ID in localStorage - 在本地存儲中設置新的訪客 ID
    },
    generateNewGuestId: (state) =&gt; {
      state.guestId = `guest_${new Date().getTime()}`;
      localStorage.setItem("guestId", state.guestId);
    },
  },
  extraReducers: (builder) =&gt; {
    builder
      .addCase(loginUser.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(loginUser.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.user = action.payload;
      })
      .addCase(loginUser.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload.message;
      })
      .addCase(registerUser.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(registerUser.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.user = action.payload;
      })
      .addCase(registerUser.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload.message;
      });
  },
});

export const { logout, generateNewGuestId } = authSlice.actions;
export default authSlice.reducer;
</code></pre>



<h2 class="wp-block-heading">製作結帳頁面</h2>



<ul class="wp-block-list">
<li>填寫表單測試繼續結帳是否能正常運作</li>



<li>檢查 > Network<br>查看請求是否有正確運行</li>



<li>使用 PayPal Sandbox 個人帳號測試能否正常結帳</li>



<li>使用 Paypal payment method</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/components/Cart/Checkout.jsx
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import PayPalButton from "./PayPalButton";
import { useDispatch, useSelector } from "react-redux";
import { createCheckout } from "../../redux/slices/checkoutSlice";
import axios from "axios";

const Checkout = () =&gt; {
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const { cart, loading, error } = useSelector((state) =&gt; state.cart);
  const { user } = useSelector((state) =&gt; state.auth);

  const &#91;checkoutId, setCheckoutId] = useState(null);
  const &#91;shippingAddress, setShippingAddress] = useState({
    firstName: "",
    lastName: "",
    address: "",
    city: "",
    postalCode: "",
    country: "",
    phone: "",
  });

  // Ensure cart is loaded before proceeding - 確保購物車已加載完成再繼續
  useEffect(() =&gt; {
    if (!cart || !cart.products || cart.products.length === 0) {
      navigate("/");
    }
  }, &#91;cart, navigate]);

  const handleCreateCheckout = async (e) =&gt; {
    e.preventDefault();
    if (cart &amp;&amp; cart.products.length &gt; 0) {
      const res = await dispatch(
        createCheckout({
          checkoutItems: cart.products,
          shippingAddress,
          paymentMethod: "Paypal",
          totalPrice: cart.totalPrice,
        })
      );
      if (res.payload &amp;&amp; res.payload._id) {
        setCheckoutId(res.payload._id); // Set checkout ID if checkout was successful - 如果結帳成功，設置結帳 ID
      }
    }
  };

  const handlePaymentSuccess = async (details) =&gt; {
    // console.log("Payment Successful", details);
    try {
      const response = await axios.put(
        `${import.meta.env.VITE_BACKEND_URL}/api/checkout/${checkoutId}/pay`,
        { paymentStatus: "paid", paymentDetails: details },
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );

      await handleFinalizeCheckout(checkoutId); // Finalize checkout if payment is successful - 如果付款成功，完成結帳
    } catch (error) {
      console.error(error);
    }
  };

  const handleFinalizeCheckout = async (checkoutId) =&gt; {
    try {
      const response = await axios.post(
        `${
          import.meta.env.VITE_BACKEND_URL
        }/api/checkout/${checkoutId}/finalize`,
        {},
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      navigate("/order-confirmation");
    } catch (error) {
      console.error(error);
    }
  };

  if (loading) return &lt;p&gt;Loading cart ...&lt;/p&gt;;
  if (error) return &lt;p&gt;Error: {error}&lt;/p&gt;;
  if (!cart || !cart.products || cart.products.length === 0) {
    return &lt;p&gt;Your cart is empty&lt;/p&gt;;
  }

  return (
    &lt;div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-7xl mx-auto py-10 px-6 tracking-tighter"&gt;
      {/* Left Section - 左側區域 */}
      &lt;div className="bg-white rounded-lg p-6"&gt;
        &lt;h2 className="text-2xl uppercase mb-6"&gt;Checkout&lt;/h2&gt;
        &lt;form onSubmit={handleCreateCheckout}&gt;
          &lt;h3 className="text-lg mb-4"&gt;Contact Details&lt;/h3&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Email&lt;/label&gt;
            &lt;input
              type="email"
              value={user ? user.email : ""}
              className="w-full p-2 border rounded"
              disabled
            /&gt;
          &lt;/div&gt;
          &lt;h3 className="text-lg mb-4"&gt;Delivery&lt;/h3&gt;
          &lt;div className="mb-4 grid grid-cols-2 gap-4"&gt;
            &lt;div&gt;
              &lt;label className="block text-gray-700"&gt;First Name&lt;/label&gt;
              &lt;input
                type="text"
                value={shippingAddress.firstName}
                onChange={(e) =&gt;
                  setShippingAddress({
                    ...shippingAddress,
                    firstName: e.target.value,
                  })
                }
                className="w-full p-2 border rounded"
                required
              /&gt;
            &lt;/div&gt;
            &lt;div&gt;
              &lt;label className="block text-gray-700"&gt;Last Name&lt;/label&gt;
              &lt;input
                type="text"
                value={shippingAddress.lastName}
                onChange={(e) =&gt;
                  setShippingAddress({
                    ...shippingAddress,
                    lastName: e.target.value,
                  })
                }
                className="w-full p-2 border rounded"
                required
              /&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Address&lt;/label&gt;
            &lt;input
              type="text"
              value={shippingAddress.address}
              onChange={(e) =&gt;
                setShippingAddress({
                  ...shippingAddress,
                  address: e.target.value,
                })
              }
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4 grid grid-cols-2 gap-4"&gt;
            &lt;div&gt;
              &lt;label className="block text-gray-700"&gt;City&lt;/label&gt;
              &lt;input
                type="text"
                value={shippingAddress.city}
                onChange={(e) =&gt;
                  setShippingAddress({
                    ...shippingAddress,
                    city: e.target.value,
                  })
                }
                className="w-full p-2 border rounded"
                required
              /&gt;
            &lt;/div&gt;
            &lt;div&gt;
              &lt;label className="block text-gray-700"&gt;Postal Code&lt;/label&gt;
              &lt;input
                type="text"
                value={shippingAddress.postalCode}
                onChange={(e) =&gt;
                  setShippingAddress({
                    ...shippingAddress,
                    postalCode: e.target.value,
                  })
                }
                className="w-full p-2 border rounded"
                required
              /&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Country&lt;/label&gt;
            &lt;input
              type="text"
              value={shippingAddress.country}
              onChange={(e) =&gt;
                setShippingAddress({
                  ...shippingAddress,
                  country: e.target.value,
                })
              }
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Phone&lt;/label&gt;
            &lt;input
              type="tel"
              value={shippingAddress.phone}
              onChange={(e) =&gt;
                setShippingAddress({
                  ...shippingAddress,
                  phone: e.target.value,
                })
              }
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mt-6"&gt;
            {!checkoutId ? (
              &lt;button
                type="submit"
                className="w-full bg-black text-white py-3 rounded"
              &gt;
                Continue to Payment
              &lt;/button&gt;
            ) : (
              &lt;div&gt;
                &lt;h3 className="text-lg mb-4"&gt;Pay with Paypal&lt;/h3&gt;
                {/* Paypal Component */}
                &lt;PayPalButton
                  amount={cart.totalPrice}
                  onSuccess={handlePaymentSuccess}
                  onError={(err) =&gt; alert("Payment failed. Try again.")}
                /&gt;
              &lt;/div&gt;
            )}
          &lt;/div&gt;
        &lt;/form&gt;
      &lt;/div&gt;
      {/* Right Section - 右側區域 */}
      &lt;div className="bg-gray-50 p-6 rounded-lg"&gt;
        &lt;h3 className="text-lg mb-4"&gt;Order Summary&lt;/h3&gt;
        &lt;div className="border-t py-4 mb-4"&gt;
          {cart.products.map((product, index) =&gt; (
            &lt;div
              key={index}
              className="flex items-start justify-between py-2 border-b"
            &gt;
              &lt;div className="flex items-start"&gt;
                &lt;img
                  src={product.image}
                  alt={product.name}
                  className="w-20 h-24 object-cover mr-4"
                /&gt;
                &lt;div&gt;
                  &lt;h3 className="text-md"&gt;{product.name}&lt;/h3&gt;
                  &lt;p className="text-gray-500"&gt;Size: {product.size}&lt;/p&gt;
                  &lt;p className="text-gray-500"&gt;Color: {product.color}&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;p className="text-xl"&gt;${product.price?.toLocaleString()}&lt;/p&gt;
            &lt;/div&gt;
          ))}
        &lt;/div&gt;
        &lt;div className="flex justify-between items-center text-lg mb-4"&gt;
          &lt;p&gt;Subtotal&lt;/p&gt;
          &lt;p&gt;${cart.totalPrice?.toLocaleString()}&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="flex justify-between items-center text-lg"&gt;
          &lt;p&gt;Shipping&lt;/p&gt;
          &lt;p&gt;Free&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="flex justify-between items-center text-lg mt-4 border-t pt-4"&gt;
          &lt;p&gt;Total&lt;/p&gt;
          &lt;p&gt;${cart.totalPrice?.toLocaleString()}&lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Checkout;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Cart/PayPalButton.jsx
import React from "react";
import { PayPalButtons, PayPalScriptProvider } from "@paypal/react-paypal-js";

const PayPalButton = ({ amount, onSuccess, onError }) =&gt; {
  return (
    &lt;PayPalScriptProvider
      options={{
        "client-id": import.meta.env.VITE_PAYPAL_CLIENT_ID,
      }}
    &gt;
      &lt;PayPalButtons
        style={{ layout: "vertical" }}
        createOrder={(data, actions) =&gt; {
          return actions.order.create({
            purchase_units: &#91;
              { amount: { value: parseFloat(amount).toFixed(2) } },
            ],
          });
        }}
        onApprove={(data, actions) =&gt; {
          return actions.order.capture().then(onSuccess);
        }}
        onError={onError}
      /&gt;
    &lt;/PayPalScriptProvider&gt;
  );
};

export default PayPalButton;
</code></pre>



<h2 class="wp-block-heading">製作訂單確認頁面</h2>



<pre class="wp-block-code"><code>// frontend/src/pages/OrderConfirmationPage.jsx
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { clearCart } from "../redux/slices/cartSlice";

const OrderConfirmationPage = () =&gt; {
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const { checkout } = useSelector((state) =&gt; state.checkout);

  // Clear the cart when the order is confirmed - 訂單確認後清空購物車
  useEffect(() =&gt; {
    if (checkout &amp;&amp; checkout._id) {
      dispatch(clearCart());
      localStorage.removeItem("cart");
    } else {
      navigate("/my-order");
    }
  }, &#91;checkout, dispatch, navigate]);

  const calculateEstimatedDelivery = (createdAt) =&gt; {
    const orderDate = new Date(createdAt);
    orderDate.setDate(orderDate.getDate() + 10); // Add 10 days to the order date - 訂單日期增加10天
    return orderDate.toLocaleDateString();
  };

  return (
    &lt;div className="max-w-4xl mx-auto p-6 bg-white"&gt;
      &lt;h1 className="text-4xl font-bold text-center text-emerald-700 mb-8"&gt;
        Thank You for Your Order!
      &lt;/h1&gt;

      {checkout &amp;&amp; (
        &lt;div className="p-6 rounded-lg border"&gt;
          &lt;div className="flex justify-between mb-20"&gt;
            {/* Order Id and Date - 訂單編號和日期 */}
            &lt;div&gt;
              &lt;h2 className="text-xl font-semibold"&gt;
                Order ID: {checkout._id}
              &lt;/h2&gt;
              &lt;p className="text-gray-500"&gt;
                Order date: {new Date(checkout.createdAt).toLocaleDateString()}
              &lt;/p&gt;
            &lt;/div&gt;
            {/* Estimated Delivery- 預計送達 */}
            &lt;div&gt;
              &lt;p className="text-emerald-700 text-sm"&gt;
                Estimated Delivery:{" "}
                {calculateEstimatedDelivery(checkout.createdAt)}
              &lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* Ordered Items - 訂單項目 */}
          &lt;div className="mb-20"&gt;
            {checkout.checkoutItems.map((item) =&gt; (
              &lt;div key={item.productId} className="flex items-center mb-4"&gt;
                &lt;img
                  src={item.image}
                  alt={item.name}
                  className="w-16 h-16 object-cover rounded-md mr-4"
                /&gt;
                &lt;div&gt;
                  &lt;h4 className="text-md font-semibold"&gt;{item.name}&lt;/h4&gt;
                  &lt;p className="text-sm text-gray-500"&gt;
                    {item.color} | {item.size}
                  &lt;/p&gt;
                &lt;/div&gt;
                &lt;div className="ml-auto text-right"&gt;
                  &lt;p className="text-md"&gt;${item.price}&lt;/p&gt;
                  &lt;p className="text-sm text-gray-500"&gt;Qty: {item.quantity}&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            ))}
          &lt;/div&gt;
          {/* Payment and Delivery Info - 付款與送貨資訊 */}
          &lt;div className="grid grid-cols-2 gap-8"&gt;
            {/* Payment Info - 付款資訊 */}
            &lt;div&gt;
              &lt;h4 className="text-lg font-semibold mb-2"&gt;Payment&lt;/h4&gt;
              &lt;p className="text-gray-600"&gt;PayPal&lt;/p&gt;
            &lt;/div&gt;

            {/* Delivery Info - 送貨資訊 */}
            &lt;div&gt;
              &lt;h4 className="text-lg font-semibold mb-2"&gt;Delivery&lt;/h4&gt;
              &lt;p className="text-gray-600"&gt;
                {checkout.shippingAddress.address}
              &lt;/p&gt;
              &lt;p className="text-gray-600"&gt;
                {checkout.shippingAddress.city},{" "}
                {checkout.shippingAddress.country}
              &lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  );
};

export default OrderConfirmationPage;
</code></pre>



<h2 class="wp-block-heading">製作我的訂單 (My Orders Component)</h2>



<pre class="wp-block-code"><code>// frontend/src/pages/MyOrdersPage.jsx
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { fetchUserOrders } from "../redux/slices/orderSlice";

const MyOrdersPage = () =&gt; {
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const { orders, loading, error } = useSelector((state) =&gt; state.orders);

  useEffect(() =&gt; {
    dispatch(fetchUserOrders());
  }, &#91;dispatch]);

  const handleRowClick = (orderId) =&gt; {
    navigate(`/order/${orderId}`);
  };

  if (loading) return &lt;p&gt;Loading ...&lt;/p&gt;;
  if (error) return &lt;p&gt;Error: {error}&lt;/p&gt;;

  return (
    &lt;div className="max-w-7xl mx-auto p-4 sm:p-6"&gt;
      &lt;h2 className="text-xl sm:text-2xl font-bold mb-6"&gt;My Orders&lt;/h2&gt;
      &lt;div className="relative shadow-md sm:rounded-lg overflow-hidden"&gt;
        &lt;table className="min-w-full text-left text-gray-500"&gt;
          &lt;thead className="bg-gray-100 text-xs uppercase text-gray-700"&gt;
            &lt;tr&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Image&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Order ID&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Created&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Shipping Address&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Items&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Price&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Status&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            {orders.length &gt; 0 ? (
              orders.map((order) =&gt; (
                &lt;tr
                  key={order._id}
                  onClick={() =&gt; handleRowClick(order._id)}
                  className="border-b hover:border-gray-50 cursor-pointer"
                &gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    &lt;img
                      src={order.orderItems&#91;0].image}
                      alt={order.orderItems&#91;0].name}
                      className="w-10 h-10 sm:w-12 sm:h-12 object-cover rounded-lg"
                    /&gt;
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4 font-medium text-gray-900 whitespace-nowrap"&gt;
                    #{order._id}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    {new Date(order.createdAt).toLocaleDateString()}{" "}
                    {new Date(order.createdAt).toLocaleTimeString()}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    {order.shippingAddress
                      ? `${order.shippingAddress.city}, ${order.shippingAddress.country}`
                      : "N/A"}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    {order.orderItems.length}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    ${order.totalPrice}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    &lt;span
                      className={`${
                        order.isPaid
                          ? "bg-green-100 text-green-700"
                          : "bg-red-100 text-red-700"
                      } px-2 py-1 rounded-full text-xs sm:text-sm font-medium`}
                    &gt;
                      {order.isPaid ? "Paid" : "Pending"}
                    &lt;/span&gt;
                  &lt;/td&gt;
                &lt;/tr&gt;
              ))
            ) : (
              &lt;tr&gt;
                &lt;td colSpan={7} className="py-4 px-4 text-center text-gray-500"&gt;
                  You have no orders
                &lt;/td&gt;
              &lt;/tr&gt;
            )}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default MyOrdersPage;
</code></pre>



<h2 class="wp-block-heading">製作訂單詳情頁面</h2>



<pre class="wp-block-code"><code>// frontend/src/pages/OrderDetailsPage.jsx
import React, { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { fetchOrderDetails } from "../redux/slices/orderSlice";

const OrderDetailsPage = () =&gt; {
  const { id } = useParams();
  const dispatch = useDispatch();
  const { orderDetails, loading, error } = useSelector((state) =&gt; state.orders);

  useEffect(() =&gt; {
    dispatch(fetchOrderDetails(id));
  }, &#91;dispatch, id]);

  if (loading) return &lt;p&gt;Loading...&lt;/p&gt;;
  if (error) return &lt;p&gt;Error: {error}&lt;/p&gt;;

  return (
    &lt;div className="max-w-7xl mx-auto p-4 sm:p-6"&gt;
      &lt;h2 className="text-2xl md:text-3xl font-bold mb-6"&gt;Order Details&lt;/h2&gt;
      {!orderDetails ? (
        &lt;p&gt;No Order details found&lt;/p&gt;
      ) : (
        &lt;div className="p-4 sm:p-6 rounded-lg border"&gt;
          {/* Order Info - 訂單資訊 */}
          &lt;div className="flex flex-col sm:flex-row justify-between mb-8"&gt;
            &lt;div&gt;
              &lt;h3 className="text-lg md:text-xl font-semibold"&gt;
                Order ID: #{orderDetails._id}
              &lt;/h3&gt;
              &lt;p className="text-gray-600"&gt;
                {new Date(orderDetails.createdAt).toLocaleDateString()}
              &lt;/p&gt;
            &lt;/div&gt;
            &lt;div className="flex flex-col items-start sm:items-end mt-4 sm:mt-0"&gt;
              &lt;span
                className={`${
                  orderDetails.isPaid
                    ? "bg-green-100 text-green-700"
                    : "bg-red-100 text-red-700"
                } px-3 py-1 rounded-full text-sm font-medium mb-2`}
              &gt;
                {orderDetails.isPaid ? "Approved" : "Pending"}
              &lt;/span&gt;
              &lt;span
                className={`${
                  orderDetails.isDelivered
                    ? "bg-green-100 text-green-700"
                    : "bg-yellow-100 text-yellow-700"
                } px-3 py-1 rounded-full text-sm font-medium mb-2`}
              &gt;
                {orderDetails.isDelivered ? "Delivered" : "Pending"}
              &lt;/span&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* Customer, Payment, Shipping Info - 顧客, 付款, 送貨資訊 */}
          &lt;div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 mb-8"&gt;
            &lt;div&gt;
              &lt;h4 className="text-lg font-semibold mb-2"&gt;Payment Info&lt;/h4&gt;
              &lt;p&gt;Payment Method: {orderDetails.paymentMethod}&lt;/p&gt;
              &lt;p&gt;Status: {orderDetails.isPaid ? "Paid" : "Unpaid"}&lt;/p&gt;
            &lt;/div&gt;
            &lt;div&gt;
              &lt;h4 className="text-lg font-semibold mb-2"&gt;Shipping Info&lt;/h4&gt;
              &lt;p&gt;Shipping Method: {orderDetails.shippingMethod}&lt;/p&gt;
              &lt;p&gt;
                Address:{" "}
                {`${orderDetails.shippingAddress.city}, ${orderDetails.shippingAddress.country}`}
              &lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* Product list - 產品清單 */}
          &lt;div className="overflow-x-auto"&gt;
            &lt;h4 className="text-lg font-semibold mb-4"&gt;Products&lt;/h4&gt;
            &lt;table className="min-w-full text-gray-600 mb-4"&gt;
              &lt;thead className="bg-gray-100"&gt;
                &lt;tr&gt;
                  &lt;th className="py-2 px-4"&gt;Name&lt;/th&gt;
                  &lt;th className="py-2 px-4"&gt;Unit Price&lt;/th&gt;
                  &lt;th className="py-2 px-4"&gt;Quantity&lt;/th&gt;
                  &lt;th className="py-2 px-4"&gt;Total&lt;/th&gt;
                &lt;/tr&gt;
              &lt;/thead&gt;
              &lt;tbody&gt;
                {orderDetails.orderItems.map((item) =&gt; (
                  &lt;tr key={item.productId} className="border-b"&gt;
                    &lt;td className="py-2 px-4 flex items-center"&gt;
                      &lt;img
                        src={item.image}
                        alt={item.name}
                        className="w-12 h-12 object-cover rounded-lg mr-4"
                      /&gt;
                      &lt;Link
                        to={`/product/${item.productId}`}
                        className="text-blue-500 hover:underline"
                      &gt;
                        {item.name}
                      &lt;/Link&gt;
                    &lt;/td&gt;
                    &lt;td className="py-2 px-4"&gt;${item.price}&lt;/td&gt;
                    &lt;td className="py-2 px-4"&gt;{item.quantity}&lt;/td&gt;
                    &lt;td className="py-2 px-4"&gt;${item.price * item.quantity}&lt;/td&gt;
                  &lt;/tr&gt;
                ))}
              &lt;/tbody&gt;
            &lt;/table&gt;
          &lt;/div&gt;

          {/* Back to Orders Link - 回到訂單頁面連結 */}
          &lt;Link to="/my-orders" className="text-blue-500 hover:underline"&gt;
            Back to My Orders
          &lt;/Link&gt;
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  );
};

export default OrderDetailsPage;
</code></pre>



<ul class="wp-block-list">
<li></li>
</ul>



<h2 class="wp-block-heading">除錯: 結帳完成頁面顏色、大小顯示錯誤</h2>



<pre class="wp-block-code"><code>// backend/models/Checkout.js
// 除錯: 結帳完成頁面產品顏色、大小顯示錯誤
const mongoose = require("mongoose");

const checkoutItemSchema = new mongoose.Schema(
  {
    productId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Product",
      required: true,
    },
    name: {
      type: String,
      required: true,
    },
    image: {
      type: String,
      requied: true,
    },
    price: {
      type: Number,
      requied: true,
    },
    quantity: {
      type: Number,
      required: true,
    },
    size: String,
    color: String,
  },
  { _id: false }
);

const checkoutSchema = new mongoose.Schema(
  {
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
      requied: true,
    },
    checkoutItems: &#91;checkoutItemSchema],
    shippingAddress: {
      address: { type: String, required: true },
      city: { type: String, required: true },
      postalCode: { type: String, required: true },
      country: { type: String, required: true },
    },
    paymentMethod: {
      type: String,
      required: true,
    },
    totalPrice: {
      type: Number,
      required: true,
    },
    isPaid: {
      type: Boolean,
      default: false,
    },
    paidAt: {
      type: Date,
    },
    paymentStatus: {
      type: String,
      default: "pending",
    },
    paymentDetails: {
      type: mongoose.Schema.Types.Mixed, // store payment-related details(transaction ID, paypal response) - 存儲與支付相關的詳細資訊(如交易 ID 和 PayPal 回應)
    },
    isFinalized: {
      type: Boolean,
      default: false,
    },
    finalizedAt: {
      type: Date,
    },
  },
  { timestamps: true }
);

module.exports = mongoose.model("Checkout", checkoutSchema);
</code></pre>



<h2 class="wp-block-heading">製作使用者角色功能 (管理員、顧客)</h2>



<ul class="wp-block-list">
<li>管理員帳號登入: admin 按鈕、可進入到儀表板頁面<br>顧客帳號登入: 沒有 admin 按鈕、不可進入到儀表板頁面</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/components/Common/ProtectedRoute.jsx
import React from "react";
import { useSelector } from "react-redux";
import { Navigate } from "react-router-dom";

const ProtectedRoute = ({ children, role }) =&gt; {
  const { user } = useSelector((state) =&gt; state.auth);

  if (!user || (role &amp;&amp; user.role !== role)) {
    return &lt;Navigate to="/login" replace /&gt;;
  }

  return children;
};

export default ProtectedRoute;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UserLayout from "./components/Layout/UserLayout";
import Home from "./pages/Home";
import { Toaster } from "sonner";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Profile from "./pages/Profile";
import CollectionPage from "./pages/CollectionPage";
import ProductDetails from "./components/Products/ProductDetails";
import Checkout from "./components/Cart/Checkout";
import OrderConfirmationPage from "./pages/OrderConfirmationPage";
import OrderDetailsPage from "./pages/OrderDetailsPage";
import MyOrdersPage from "./pages/MyOrdersPage";
import AdminLayout from "./components/Admin/AdminLayout";
import AdminHomePage from "./pages/AdminHomePage";
import UserManagement from "./components/Admin/UserManagement";
import ProductManagement from "./components/Admin/ProductManagement";
import EditProductPage from "./components/Admin/EditProductPage";
import OrderManagement from "./components/Admin/OrderManagement";

import { Provider } from "react-redux";
import store from "./redux/store";
import ProtectedRoute from "./components/Common/ProtectedRoute";

const App = () =&gt; {
  return (
    &lt;Provider store={store}&gt;
      &lt;BrowserRouter&gt;
        &lt;Toaster position="top-right" /&gt;
        &lt;Routes&gt;
          &lt;Route path="/" element={&lt;UserLayout /&gt;}&gt;
            {/* User Layout - 使用者佈局 */}
            &lt;Route index element={&lt;Home /&gt;} /&gt;
            &lt;Route path="login" element={&lt;Login /&gt;} /&gt;
            &lt;Route path="register" element={&lt;Register /&gt;} /&gt;
            &lt;Route path="profile" element={&lt;Profile /&gt;} /&gt;
            &lt;Route
              path="collections/:collection"
              element={&lt;CollectionPage /&gt;}
            /&gt;
            &lt;Route path="product/:id" element={&lt;ProductDetails /&gt;} /&gt;
            &lt;Route path="checkout" element={&lt;Checkout /&gt;} /&gt;
            &lt;Route
              path="order-confirmation"
              element={&lt;OrderConfirmationPage /&gt;}
            /&gt;
            &lt;Route path="order/:id" element={&lt;OrderDetailsPage /&gt;} /&gt;
            &lt;Route path="my-orders" element={&lt;MyOrdersPage /&gt;} /&gt;
          &lt;/Route&gt;
          &lt;Route
            path="/admin"
            element={
              &lt;ProtectedRoute role="admin"&gt;
                &lt;AdminLayout /&gt;
              &lt;/ProtectedRoute&gt;
            }
          &gt;
            {/* Admin Layout - 管理員佈局 */}
            &lt;Route index element={&lt;AdminHomePage /&gt;} /&gt;
            &lt;Route path="users" element={&lt;UserManagement /&gt;} /&gt;
            &lt;Route path="products" element={&lt;ProductManagement /&gt;} /&gt;
            &lt;Route path="products/:id/edit" element={&lt;EditProductPage /&gt;} /&gt;
            &lt;Route path="orders" element={&lt;OrderManagement /&gt;} /&gt;
          &lt;/Route&gt;
        &lt;/Routes&gt;
      &lt;/BrowserRouter&gt;
    &lt;/Provider&gt;
  );
};

export default App;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Common/Navbar.jsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import {
  HiOutlineUser,
  HiOutlineShoppingBag,
  HiBars3BottomRight,
} from "react-icons/hi2";
import SearchBar from "./SearchBar";
import CartDrawer from "../Layout/CartDrawer";
import { IoMdClose } from "react-icons/io";
import { useSelector } from "react-redux";

const Navbar = () =&gt; {
  const &#91;drawerOpen, setDrawerOpen] = useState(false);
  const &#91;navDrawerOpen, setNavDrawerOpen] = useState(false);
  const { cart } = useSelector((state) =&gt; state.cart);
  const { user } = useSelector((state) =&gt; state.auth);

  const cartItemCount =
    cart?.products?.reduce((total, product) =&gt; total + product.quantity, 0) ||
    0;

  const toggleNavDrawer = () =&gt; {
    setNavDrawerOpen(!navDrawerOpen);
  };

  const toggleCartDrawer = () =&gt; {
    setDrawerOpen(!drawerOpen);
  };

  return (
    &lt;&gt;
      &lt;nav className="container mx-auto flex items-center justify-between py-4 px-6"&gt;
        {/* Left - Logo -&gt; 左側 - 商標、標誌 */}
        &lt;div&gt;
          &lt;Link to="/" className="text-2xl font-medium"&gt;
            Rabbit
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Center - Navigation Links -&gt; 中間 - 導覽連結 */}
        &lt;div className="hidden md:flex space-x-6"&gt;
          &lt;Link
            to="/collections/all?gender=Men"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Men
          &lt;/Link&gt;
          &lt;Link
            to="/collections/all?gender=Women"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Women
          &lt;/Link&gt;
          &lt;Link
            to="/collections/all?category=Top Wear"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Top Wear
          &lt;/Link&gt;
          &lt;Link
            to="/collections/all?category=Bottom Wear"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Bottom Wear
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Right - Icons -&gt; 右側 - 圖示 */}
        &lt;div className="flex items-center space-x-4"&gt;
          {user &amp;&amp; user.role === "admin" &amp;&amp; (
            &lt;Link
              to="/admin"
              className="block bg-black px-2 rounded text-sm text-white"
            &gt;
              Admin
            &lt;/Link&gt;
          )}
          &lt;Link to="/profile" className="hover:text-black"&gt;
            &lt;HiOutlineUser className="h-6 w-6 text-gray-700" /&gt;
          &lt;/Link&gt;
          &lt;button
            onClick={toggleCartDrawer}
            className="relative hover:text-black"
          &gt;
            &lt;HiOutlineShoppingBag className="h-6 w-6 text-gray-700" /&gt;
            {cartItemCount &gt; 0 &amp;&amp; (
              &lt;span className="absolute -top-1 bg-rabbit-red text-white text-xs rounded-full px-2 py-0.5"&gt;
                {cartItemCount}
              &lt;/span&gt;
            )}
          &lt;/button&gt;
          {/* Search - 搜尋 */}
          &lt;div className="overflow-hidden"&gt;
            &lt;SearchBar /&gt;
          &lt;/div&gt;

          &lt;button onClick={toggleNavDrawer} className="md:hidden"&gt;
            &lt;HiBars3BottomRight className="h-6 w-6 text-gray-700" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/nav&gt;
      &lt;CartDrawer drawerOpen={drawerOpen} toggleCartDrawer={toggleCartDrawer} /&gt;

      {/* Mobile Navigation - 手機版導覽 */}
      &lt;div
        className={`fixed top-0 left-0 w-3/4 sm:w-1/2 md:w-1/3 h-full bg-white shadow-lg transform transition-transform duration-300 z-50 ${
          navDrawerOpen ? "translate-x-0" : "-translate-x-full"
        }`}
      &gt;
        &lt;div className="flex justify-end p-4"&gt;
          &lt;button onClick={toggleNavDrawer}&gt;
            &lt;IoMdClose className="h-6 w-6 text-gray-600" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
        &lt;div className="p-4"&gt;
          &lt;h2 className="text-xl font-semibold mb-4"&gt;Menu&lt;/h2&gt;
          &lt;nav className="space-y-4"&gt;
            &lt;Link
              to="/collections/all?gender=Men"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Men
            &lt;/Link&gt;
            &lt;Link
              to="/collections/all?gender=Women"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Women
            &lt;/Link&gt;
            &lt;Link
              to="/collections/all?category=Top Wear"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Top Wear
            &lt;/Link&gt;
            &lt;Link
              to="/collections/all?category=Bottom Wear"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Bottom Wear
            &lt;/Link&gt;
          &lt;/nav&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default Navbar;
</code></pre>



<ul class="wp-block-list">
<li></li>
</ul>



<h2 class="wp-block-heading">製作管理員頁面</h2>



<ul class="wp-block-list">
<li>修正 Manage Orders、Manage Products 連結</li>



<li>價格金額加上 toFixed(2)</li>



<li>製作管理員儀錶板登出功能</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/pages/AdminHomePage.jsx
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { fetchAdminProducts } from "../redux/slices/adminProductSlice";
import { fetchAllOrders } from "../redux/slices/adminOrderSlice";

const AdminHomePage = () =&gt; {
  const dispatch = useDispatch();
  const {
    products,
    loading: productsLoading,
    error: productsError,
  } = useSelector((state) =&gt; state.adminProducts);
  const {
    orders,
    totalOrders,
    totalSales,
    loading: ordersLoading,
    error: ordersError,
  } = useSelector((state) =&gt; state.adminOrders);

  useEffect(() =&gt; {
    dispatch(fetchAdminProducts());
    dispatch(fetchAllOrders());
  }, &#91;dispatch]);

  return (
    &lt;div className="max-w-7xl mx-auto p-6"&gt;
      &lt;h1 className="text-3xl font-bold mb-6"&gt;Admin Dashboard&lt;/h1&gt;
      {productsLoading || ordersLoading ? (
        &lt;p&gt;Loading ...&lt;/p&gt;
      ) : productsError ? (
        &lt;p className="text-red-500"&gt;Error fetching products: {productsError}&lt;/p&gt;
      ) : ordersError ? (
        &lt;p className="text-red-500"&gt;Error fetching orders: {ordersError}&lt;/p&gt;
      ) : (
        &lt;div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"&gt;
          &lt;div className="p-4 shadow-md rounded-lg"&gt;
            &lt;h2 className="text-xl font-semibold"&gt;Revenue&lt;/h2&gt;
            &lt;p className="text-2xl"&gt;${totalSales.toFixed(2)}&lt;/p&gt;
          &lt;/div&gt;
          &lt;div className="p-4 shadow-md rounded-lg"&gt;
            &lt;h2 className="text-xl font-semibold"&gt;Total Order&lt;/h2&gt;
            &lt;p className="text-2xl"&gt;{totalOrders}&lt;/p&gt;
            &lt;Link to="/admin/orders" className="text-blue-500 hover:underline"&gt;
              Manage Orders
            &lt;/Link&gt;
          &lt;/div&gt;
          &lt;div className="p-4 shadow-md rounded-lg"&gt;
            &lt;h2 className="text-xl font-semibold"&gt;Total Products&lt;/h2&gt;
            &lt;p className="text-2xl"&gt;{products.length}&lt;/p&gt;
            &lt;Link
              to="/admin/products"
              className="text-blue-500 hover:underline"
            &gt;
              Manage Products
            &lt;/Link&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      )}
      &lt;div className="mt-6"&gt;
        &lt;h2 className="text-2xl font-bold mb-4"&gt;Recent Orders&lt;/h2&gt;
        &lt;div className="overflow-x-auto"&gt;
          &lt;table className="min-w-full text-left text-gray-500"&gt;
            &lt;thead className="bg-gray-100 text-xs uppercase text-gray-700"&gt;
              &lt;tr&gt;
                &lt;th className="py-3 px-4"&gt;Order ID&lt;/th&gt;
                &lt;th className="py-3 px-4"&gt;User&lt;/th&gt;
                &lt;th className="py-3 px-4"&gt;Total Price&lt;/th&gt;
                &lt;th className="py-3 px-4"&gt;Status&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
              {orders.length &gt; 0 ? (
                orders.map((order) =&gt; (
                  &lt;tr
                    key={order._id}
                    className="border-b hover:bg-gray-50 cursor-pointer"
                  &gt;
                    &lt;td className="p-4"&gt;{order._id}&lt;/td&gt;
                    &lt;td className="p-4"&gt;{order.user.name}&lt;/td&gt;
                    &lt;td className="p-4"&gt;{order.totalPrice.toFixed(2)}&lt;/td&gt;
                    &lt;td className="p-4"&gt;{order.status}&lt;/td&gt;
                  &lt;/tr&gt;
                ))
              ) : (
                &lt;tr&gt;
                  &lt;td colSpan={4} className="p-4 text-center text-gray-500"&gt;
                    No recent orders found.
                  &lt;/td&gt;
                &lt;/tr&gt;
              )}
            &lt;/tbody&gt;
          &lt;/table&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default AdminHomePage;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Admin/AdminSidebar.jsx
import React from "react";
import {
  FaBoxOpen,
  FaClipboardList,
  FaSignOutAlt,
  FaStore,
  FaUser,
} from "react-icons/fa";
import { useDispatch } from "react-redux";
import { Link, NavLink, useNavigate } from "react-router-dom";
import { logout } from "../../redux/slices/authSlice";
import { clearCart } from "../../redux/slices/cartSlice";

const AdminSidebar = () =&gt; {
  const navigate = useNavigate();
  const dispatch = useDispatch();

  const handleLogout = () =&gt; {
    dispatch(logout());
    dispatch(clearCart());
    navigate("/");
  };

  return (
    &lt;div className="p-6"&gt;
      &lt;div className="mb-6"&gt;
        &lt;Link to="/admin" className="text-2xl font-medium"&gt;
          Rabbit
        &lt;/Link&gt;
      &lt;/div&gt;
      &lt;h2 className="text-xl font-medium mb-6 text-center"&gt;Admin Dashboard&lt;/h2&gt;

      &lt;nav className="flex flex-col space-y-2"&gt;
        &lt;NavLink
          to="/admin/users"
          className={({ isActive }) =&gt;
            isActive
              ? "bg-gray-700 text-white py-3 px-4 rounded flex items-center space-x-2"
              : "text-gray-300 hover:bg-gray-700 hover:text-white py-3 px-4 rounded flex items-center space-x-2"
          }
        &gt;
          &lt;FaUser /&gt;
          &lt;span&gt;Users&lt;/span&gt;
        &lt;/NavLink&gt;
        &lt;NavLink
          to="/admin/products"
          className={({ isActive }) =&gt;
            isActive
              ? "bg-gray-700 text-white py-3 px-4 rounded flex items-center space-x-2"
              : "text-gray-300 hover:bg-gray-700 hover:text-white py-3 px-4 rounded flex items-center space-x-2"
          }
        &gt;
          &lt;FaBoxOpen /&gt;
          &lt;span&gt;Products&lt;/span&gt;
        &lt;/NavLink&gt;
        &lt;NavLink
          to="/admin/orders"
          className={({ isActive }) =&gt;
            isActive
              ? "bg-gray-700 text-white py-3 px-4 rounded flex items-center space-x-2"
              : "text-gray-300 hover:bg-gray-700 hover:text-white py-3 px-4 rounded flex items-center space-x-2"
          }
        &gt;
          &lt;FaClipboardList /&gt;
          &lt;span&gt;Orders&lt;/span&gt;
        &lt;/NavLink&gt;
        &lt;NavLink
          to="/"
          className={({ isActive }) =&gt;
            isActive
              ? "bg-gray-700 text-white py-3 px-4 rounded flex items-center space-x-2"
              : "text-gray-300 hover:bg-gray-700 hover:text-white py-3 px-4 rounded flex items-center space-x-2"
          }
        &gt;
          &lt;FaStore /&gt;
          &lt;span&gt;Shop&lt;/span&gt;
        &lt;/NavLink&gt;
      &lt;/nav&gt;
      &lt;div className="mt-6"&gt;
        &lt;button
          onClick={handleLogout}
          className="w-full bg-red-500 hover:bg-red-600 text-white py-2 px-4 rounded flex items-center justify-center space-x-2"
        &gt;
          &lt;FaSignOutAlt /&gt;
          &lt;span&gt;Logout&lt;/span&gt;
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default AdminSidebar;
</code></pre>



<h2 class="wp-block-heading">製作使用者管理功能 (Admin)</h2>



<ul class="wp-block-list">
<li>除錯: fetchUsers 沒有加上 return</li>



<li>除錯: 無法更改 Role</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/components/Admin/UserManagement.jsx
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import {
  addUser,
  deleteUser,
  fetchUsers,
  updateUser,
} from "../../redux/slices/adminSlice";

const UserManagement = () =&gt; {
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const { user } = useSelector((state) =&gt; state.auth);
  const { users, loading, error } = useSelector((state) =&gt; state.admin);

  useEffect(() =&gt; {
    if (user &amp;&amp; user.role !== "admin") {
      navigate("/");
    }
  }, &#91;user, navigate]);

  useEffect(() =&gt; {
    if (user &amp;&amp; user.role === "admin") {
      dispatch(fetchUsers());
    }
  }, &#91;dispatch, user]);

  const &#91;formData, setFormData] = useState({
    name: "",
    email: "",
    password: "",
    role: "customer", // Default role - 預設角色
  });

  const handleChange = (e) =&gt; {
    setFormData({
      ...formData,
      &#91;e.target.name]: e.target.value,
    });
  };

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    // console.log(formData);
    dispatch(addUser(formData));

    // Reset the form after Submission - 提交後重置表單
    setFormData({
      name: "",
      email: "",
      password: "",
      role: "customer",
    });
  };

  const handleRoleChange = (userId, newRole) =&gt; {
    // console.log({ id: userId, role: newRole });
    dispatch(updateUser({ id: userId, role: newRole }));
  };

  const handleDeleteUser = (userId) =&gt; {
    if (window.confirm("Are you sure you want to delete this user?")) {
      // console.log("deleting user with ID", userId);
      dispatch(deleteUser(userId));
    }
  };

  return (
    &lt;div className="max-w-7xl mx-auto p-6"&gt;
      &lt;h2 className="text-2xl font-bold mb-6"&gt;User Management&lt;/h2&gt;
      {loading &amp;&amp; &lt;p&gt;Loading...&lt;/p&gt;}
      {error &amp;&amp; &lt;p&gt;Error: {error}&lt;/p&gt;}
      {/* Add New User Form - 新增使用者表單 */}
      &lt;div className="p-6 rounded-lg mb-6"&gt;
        &lt;h3 className="text-lg font-bold mb-4"&gt;Add New User&lt;/h3&gt;
        &lt;form onSubmit={handleSubmit}&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Name&lt;/label&gt;
            &lt;input
              type="text"
              name="name"
              value={formData.name}
              onChange={handleChange}
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Email&lt;/label&gt;
            &lt;input
              type="email"
              name="email"
              value={formData.email}
              onChange={handleChange}
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Password&lt;/label&gt;
            &lt;input
              type="password"
              name="password"
              value={formData.password}
              onChange={handleChange}
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Role&lt;/label&gt;
            &lt;select
              name="role"
              value={formData.role}
              onChange={handleChange}
              className="w-full p-2 border rounded"
            &gt;
              &lt;option value="customer"&gt;Customer&lt;/option&gt;
              &lt;option value="admin"&gt;Admin&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;
          &lt;button
            type="submit"
            className="bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600"
          &gt;
            Add User
          &lt;/button&gt;
        &lt;/form&gt;
      &lt;/div&gt;

      {/* User List Management - 用戶列表管理 */}
      &lt;div className="overflow-x-auto shadow-md sm:rounded-lg"&gt;
        &lt;table className="min-w-full text-left text-gray-500"&gt;
          &lt;thead className="bg-gray-100 text-xs uppercase text-gray-700"&gt;
            &lt;tr&gt;
              &lt;th className="py-3 px-4"&gt;Name&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Email&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Role&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Actions&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            {users.map((user) =&gt; (
              &lt;tr key={user._id} className="border-b hover:bg-gray-50"&gt;
                &lt;td className="p-4 font-medium text-gray-900 whitespace-nowrap"&gt;
                  {user.name}
                &lt;/td&gt;
                &lt;td className="p-4"&gt;{user.email}&lt;/td&gt;
                &lt;td className="p-4"&gt;
                  &lt;select
                    value={user.role}
                    onChange={(e) =&gt; handleRoleChange(user._id, e.target.value)}
                    className="p-2 border rounded"
                  &gt;
                    &lt;option value="customer"&gt;Customer&lt;/option&gt;
                    &lt;option value="admin"&gt;Admin&lt;/option&gt;
                  &lt;/select&gt;
                &lt;/td&gt;
                &lt;td className="p-4"&gt;
                  &lt;button
                    onClick={() =&gt; handleDeleteUser(user._id)}
                    className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
                  &gt;
                    Delete
                  &lt;/button&gt;
                &lt;/td&gt;
              &lt;/tr&gt;
            ))}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default UserManagement;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/adminSlice.js
// 除錯1: fetchUsers 沒有加上 return
// 除錯2: 無法更改 Role
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// fetch all users (admin only) - 獲取所有用戶 (僅限管理員)
export const fetchUsers = createAsyncThunk("admin/fetchUsers", async () =&gt; {
  const response = await axios.get(
    `${import.meta.env.VITE_BACKEND_URL}/api/admin/users`,
    {
      headers: { Authorization: `Bearer ${localStorage.getItem("userToken")}` },
    }
  );
  return response.data;
});

// Add the create user action - 新增創建用戶的動作
export const addUser = createAsyncThunk(
  "admin/addUser",
  async (userData, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/admin/users`,
        userData,
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Update user info - 更新用戶資訊
export const updateUser = createAsyncThunk(
  "admin/updateUser",
  async ({ id, name, email, role }) =&gt; {
    const response = await axios.put(
      `${import.meta.env.VITE_BACKEND_URL}/api/admin/users/${id}`,
      { name, email, role },
      {
        headers: {
          Authorization: `Bearer ${localStorage.getItem("userToken")}`,
        },
      }
    );
    return response.data.user;
  }
);

// Delete a user - 刪除用戶
export const deleteUser = createAsyncThunk("admin/deleteUser", async (id) =&gt; {
  await axios.delete(
    `${import.meta.env.VITE_BACKEND_URL}/api/admin/users/${id}`,
    {
      headers: {
        Authorization: `Bearer ${localStorage.getItem("userToken")}`,
      },
    }
  );
  return id;
});

const adminSlice = createSlice({
  name: "admin",
  initialState: {
    users: &#91;],
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) =&gt; {
    builder
      .addCase(fetchUsers.pending, (state) =&gt; {
        state.loading = true;
      })
      .addCase(fetchUsers.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.users = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      })
      .addCase(updateUser.fulfilled, (state, action) =&gt; {
        const updatedUser = action.payload;
        // console.log(action.payload); // 顯示開發過程中的訊息

        const userIndex = state.users.findIndex(
          (user) =&gt; user._id === updatedUser._id
        );
        if (userIndex !== -1) {
          state.users&#91;userIndex] = updatedUser;
        }
      })
      .addCase(deleteUser.fulfilled, (state, action) =&gt; {
        state.users = state.users.filter((user) =&gt; user._id !== action.payload);
      })
      .addCase(addUser.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(addUser.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.users.push(action.payload.user); // add a new user to the state - 將新用戶增加到狀態中
      })
      .addCase(addUser.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload.message;
      });
  },
});

export default adminSlice.reducer;
</code></pre>



<h2 class="wp-block-heading">製作訂單管理功能 (Admin)</h2>



<ul class="wp-block-list">
<li>修正金額小數點問題</li>



<li>除錯: Custmoer 名稱有的沒有顯示<br>修正 Update 的 findById</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/components/Admin/OrderManagement.jsx
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import {
  fetchAllOrders,
  updateOrderStatus,
} from "../../redux/slices/adminOrderSlice";

const OrderManagement = () =&gt; {
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const { user } = useSelector((state) =&gt; state.auth);
  const { orders, loading, error } = useSelector((state) =&gt; state.adminOrders);

  useEffect(() =&gt; {
    if (!user || user.role !== "admin") {
      navigate("/");
    } else {
      dispatch(fetchAllOrders());
    }
  }, &#91;dispatch, user, navigate]);

  const handleStatusChange = (orderId, status) =&gt; {
    // console.log({ id: orderId, status });
    dispatch(updateOrderStatus({ id: orderId, status }));
  };

  if (loading) return &lt;p&gt;Loading...&lt;/p&gt;;
  if (error) return &lt;p&gt;Error: {error}&lt;/p&gt;;

  return (
    &lt;div className="max-w-7xl mx-auto p-6"&gt;
      &lt;h2 className="text-2xl font-bold mb-6"&gt;Order Management&lt;/h2&gt;

      &lt;div className="overflow-x-auto shadow-md sm:rounded-lg"&gt;
        &lt;table className="min-w-full text-left text-gray-500"&gt;
          &lt;thead className="bg-gray-100 text-xs uppercase text-gray-700"&gt;
            &lt;tr&gt;
              &lt;th className="py-3 px-4"&gt;Order ID&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Customer&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Total Price&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Status&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Actions&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            {orders.length &gt; 0 ? (
              orders.map((order) =&gt; (
                &lt;tr
                  key={order._id}
                  className="border-b hover:bg-gray-50 cursor-pointer"
                &gt;
                  &lt;td className="py-4 px-4 font-medium text-gray-900 whitespace-nowrap"&gt;
                    #{order._id}
                  &lt;/td&gt;
                  &lt;td className="p-4"&gt;{order.user.name}&lt;/td&gt;
                  &lt;td className="p-4"&gt;${order.totalPrice.toFixed(2)}&lt;/td&gt;
                  &lt;td className="p-4"&gt;
                    &lt;select
                      value={order.status}
                      onChange={(e) =&gt;
                        handleStatusChange(order._id, e.target.value)
                      }
                      className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5"
                    &gt;
                      &lt;option value="Processing"&gt;Processing&lt;/option&gt;
                      &lt;option value="Shipped"&gt;Shipped&lt;/option&gt;
                      &lt;option value="Delivered"&gt;Delivered&lt;/option&gt;
                      &lt;option value="Cancelled"&gt;Cancelled&lt;/option&gt;
                    &lt;/select&gt;
                  &lt;/td&gt;
                  &lt;td className="p-4"&gt;
                    &lt;button
                      onClick={() =&gt; handleStatusChange(order._id, "Delivered")}
                      className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
                    &gt;
                      Mark as Delivered
                    &lt;/button&gt;
                  &lt;/td&gt;
                &lt;/tr&gt;
              ))
            ) : (
              &lt;tr&gt;
                &lt;td colSpan={5} className="p-4 text-center text-gray-500"&gt;
                  No Orders found.
                &lt;/td&gt;
              &lt;/tr&gt;
            )}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default OrderManagement;
</code></pre>



<pre class="wp-block-code"><code>// backend/routes/adminOrderRoutes.jsx
// 修正 Update 的 findById
const express = require("express");
const Order = require("../models/Order");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route GET /api/admin/orders - @路由、使用 GET 方法、API 的路徑
// @desc Get all order (Admin only) - @描述 取得所有訂單 (僅限管理員)
// @access Private/Admin - @訪問權限 私人/管理員
router.get("/", protect, admin, async (req, res) =&gt; {
  try {
    const orders = await Order.find({}).populate("user", "name email");
    res.json(orders);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route PUT /api/admin/orders/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update order status - @描述 更新訂單狀態
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const order = await Order.findById(req.params.id).populate("user", "name");
    if (order) {
      order.status = req.body.status || order.status;
      order.isDelivered =
        req.body.status === "Delivered" ? true : order.isDelivered;
      order.deliveredAt =
        req.body.status === "Delivered" ? Date.now() : order.deliveredAt;

      const updatedOrder = await order.save();
      res.json(updatedOrder);
    } else {
      res.status(404).json({ message: "Order not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route DELETE /api/admin/orders/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete an order - @描述 刪除訂單
// @access Private/Admin - @訪問權限 私人/管理員
router.delete("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const order = await Order.findById(req.params.id);
    if (order) {
      await order.deleteOne();
      res.json({ message: "Order removed" });
    } else {
      res.status(404).json({ message: "Order not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">製作產品管理功能 (Admin)</h2>



<ul class="wp-block-list">
<li>除錯: 無法刪除產品<br>修正 deleteProduct 刪除產品路徑</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/components/Admin/ProductManagement.jsx
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import {
  deleteProduct,
  fetchAdminProducts,
} from "../../redux/slices/adminProductSlice";

const ProductManagement = () =&gt; {
  const dispatch = useDispatch();
  const { products, loading, error } = useSelector(
    (state) =&gt; state.adminProducts
  );

  useEffect(() =&gt; {
    dispatch(fetchAdminProducts());
  }, &#91;dispatch]);

  const handleDelete = (id) =&gt; {
    if (window.confirm("Are you sure you want to delete the Product?")) {
      // console.log("Delete Product with id:", id);
      dispatch(deleteProduct(id));
    }
  };

  if (loading) return &lt;p&gt;Loading ...&lt;/p&gt;;
  if (error) return &lt;p&gt;Error: {error}&lt;/p&gt;;

  return (
    &lt;div className="max-w-7xl mx-auto p-6"&gt;
      &lt;h2 className="text-2xl font-bold mb-6"&gt;Product Management&lt;/h2&gt;
      &lt;div className="overflow-x-auto shadow-md sm:rounded-lg"&gt;
        &lt;table className="min-w-full text-left text-gray-500"&gt;
          &lt;thead className="bg-gray-100 text-xs uppercase text-gray-700"&gt;
            &lt;tr&gt;
              &lt;th className="py-3 px-4"&gt;Name&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Price&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;SKU&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Actions&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            {products.length &gt; 0 ? (
              products.map((product) =&gt; (
                &lt;tr
                  key={product._id}
                  className="border-b hover:bg-gray-50 cursor-pointer"
                &gt;
                  &lt;td className="p-4 font-medium text-gray-900 whitespace-nowrap"&gt;
                    {product.name}
                  &lt;/td&gt;
                  &lt;td className="p-4"&gt;${product.price}&lt;/td&gt;
                  &lt;td className="p-4"&gt;{product.sku}&lt;/td&gt;
                  &lt;td className="p-4"&gt;
                    &lt;Link
                      to={`/admin/products/${product._id}/edit`}
                      className="bg-yellow-500 text-white px-2 py-1 rounded mr-2 hover:bg-yellow-600"
                    &gt;
                      Edit
                    &lt;/Link&gt;
                    &lt;button
                      onClick={() =&gt; handleDelete(product._id)}
                      className="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600"
                    &gt;
                      Delete
                    &lt;/button&gt;
                  &lt;/td&gt;
                &lt;/tr&gt;
              ))
            ) : (
              &lt;tr&gt;
                &lt;td colSpan={4} className="p-4 text-center text-gray-500"&gt;
                  No Products found.
                &lt;/td&gt;
              &lt;/tr&gt;
            )}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default ProductManagement;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/adminProductSlice.jsx
// 除錯: 無法刪除產品
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

const API_URL = `${import.meta.env.VITE_BACKEND_URL}`;
const USER_TOKEN = `Bearer ${localStorage.getItem("userToken")}`;

// async thunk to fetch admin products - 獲取管理員產品的非同步函數
export const fetchAdminProducts = createAsyncThunk(
  "adminProducts/fetchProducts",
  async () =&gt; {
    const response = await axios.get(`${API_URL}/api/admin/products`, {
      headers: {
        Authorization: USER_TOKEN,
      },
    });
    return response.data;
  }
);

// async function to create a new product - 創建新產品的非同步函數
export const createProduct = createAsyncThunk(
  "adminProducts/createProduct",
  async (productData) =&gt; {
    const response = await axios.post(
      `${API_URL}/api/admin/products`,
      productData,
      {
        headers: {
          Authorization: USER_TOKEN,
        },
      }
    );
    return response.data;
  }
);

// async thunk to update an existing product - 更新現有產品的非同步函數
export const updateProduct = createAsyncThunk(
  "adminProducts/updateProduct",
  async ({ id, productData }) =&gt; {
    const response = await axios.put(
      `${API_URL}/api/admin/products/${id}`,
      productData,
      {
        headers: {
          Authorization: USER_TOKEN,
        },
      }
    );
    return response.data;
  }
);

// async thunk to delete a product - 刪除產品的非同步函數
export const deleteProduct = createAsyncThunk(
  "adminProducts/deleteProduct",
  async (id) =&gt; {
    await axios.delete(`${API_URL}/api/products/${id}`, {
      headers: { Authorization: USER_TOKEN },
    });
    return id;
  }
);

const adminProductSlice = createSlice({
  name: "adminProducts",
  initialState: {
    products: &#91;],
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) =&gt; {
    builder
      .addCase(fetchAdminProducts.pending, (state) =&gt; {
        state.loading = true;
      })
      .addCase(fetchAdminProducts.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.products = action.payload;
      })
      .addCase(fetchAdminProducts.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      })
      // Create Product - 新增產品
      .addCase(createProduct.fulfilled, (state, action) =&gt; {
        state.products.push(action.payload);
      })
      // Update Product - 更新產品
      .addCase(updateProduct.fulfilled, (state, action) =&gt; {
        const index = state.products.findIndex(
          (product) =&gt; product._id === action.payload._id
        );
        if (index !== -1) {
          state.products&#91;index] = action.payload;
        }
      })
      // Delete Product - 刪除產品
      .addCase(deleteProduct.fulfilled, (state, action) =&gt; {
        state.products = state.products.filter(
          (product) =&gt; product._id !== action.payload
        );
      });
  },
});

export default adminProductSlice.reducer;
</code></pre>



<h2 class="wp-block-heading">製作更新產品功能 (Admin)</h2>



<pre class="wp-block-code"><code>// frontend/src/components/Admin/EditProductPage.jsx
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import {
  fetchProductDetails,
  updateProduct,
} from "../../redux/slices/productsSlice";
import axios from "axios";

const EditProductPage = () =&gt; {
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const { id } = useParams();
  const { selectedProduct, loading, error } = useSelector(
    (state) =&gt; state.products
  );

  const &#91;productData, setProductData] = useState({
    name: "",
    description: "",
    price: 0,
    countInStock: 0,
    sku: "",
    category: "",
    brand: "",
    sizes: &#91;],
    colors: &#91;],
    collections: "",
    material: "",
    gender: "",
    images: &#91;],
  });

  const &#91;uploading, setUploading] = useState(false); // Image uploading state - 圖片上傳狀態

  useEffect(() =&gt; {
    if (id) {
      dispatch(fetchProductDetails(id));
    }
  }, &#91;dispatch, id]);

  useEffect(() =&gt; {
    if (selectedProduct) {
      setProductData(selectedProduct);
    }
  }, &#91;selectedProduct]);

  const handleChange = (e) =&gt; {
    const { name, value } = e.target;
    setProductData((prevData) =&gt; ({ ...prevData, &#91;name]: value }));
  };

  const handleImageUpload = async (e) =&gt; {
    const file = e.target.files&#91;0];
    // console.log(file);
    const formData = new FormData();
    formData.append("image", file);

    try {
      setUploading(true);
      const { data } = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/upload`,
        formData,
        {
          headers: { "Content-Type": "multipart/form-data" },
        }
      );
      setProductData((prevData) =&gt; ({
        ...prevData,
        images: &#91;...prevData.images, { url: data.imageUrl, altText: "" }],
      }));
      setUploading(false);
    } catch (error) {
      console.error(error);
      setUploading(false);
    }
  };

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    // console.log(productData);
    dispatch(updateProduct({ id, productData }));
    navigate("/admin/products");
  };

  if (loading) return &lt;p&gt;Loading...&lt;/p&gt;;
  if (error) return &lt;p&gt;Error: {error}&lt;/p&gt;;

  return (
    &lt;div className="max-w-5xl mx-auto p-6 shadow-md rounded-md"&gt;
      &lt;h2 className="text-3xl font-bold mb-6"&gt;Edit Product&lt;/h2&gt;
      &lt;form onSubmit={handleSubmit}&gt;
        {/* Name - 名稱 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;Product Name&lt;/label&gt;
          &lt;input
            type="text"
            name="name"
            value={productData.name}
            onChange={handleChange}
            className="w-full border border-gray-300 rounded-md p-2"
            required
          /&gt;
        &lt;/div&gt;

        {/* Description - 描述 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;Description&lt;/label&gt;
          &lt;textarea
            name="description"
            value={productData.description}
            onChange={handleChange}
            className="w-full border border-gray-300 rounded-md p-2"
            rows={4}
            required
          /&gt;
        &lt;/div&gt;

        {/* Price - 價格 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;Price&lt;/label&gt;
          &lt;input
            type="number"
            name="price"
            value={productData.price}
            onChange={handleChange}
            className="w-full border border-gray-300 rounded-md p-2"
          /&gt;
        &lt;/div&gt;

        {/* Count In stock - 庫存數量 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;Count in Stock&lt;/label&gt;
          &lt;input
            type="number"
            name="countInStock"
            value={productData.countInStock}
            onChange={handleChange}
            className="w-full border border-gray-300 rounded-md p-2"
          /&gt;
        &lt;/div&gt;

        {/* SKU - 庫存單位 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;SKU&lt;/label&gt;
          &lt;input
            type="text"
            name="sku"
            value={productData.sku}
            onChange={handleChange}
            className="w-full border border-gray-300 rounded-md p-2"
          /&gt;
        &lt;/div&gt;

        {/* Sizes - 尺寸 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;
            Sizes (comma-separated)
          &lt;/label&gt;
          &lt;input
            type="text"
            name="sizes"
            value={productData.sizes.join(", ")}
            onChange={(e) =&gt;
              setProductData({
                ...productData,
                sizes: e.target.value.split(",").map((size) =&gt; size.trim()),
              })
            }
            className="w-full border border-gray-300 rounded-md p-2"
          /&gt;
        &lt;/div&gt;

        {/* Colors - 顏色 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;
            Colors (comma-separated)
          &lt;/label&gt;
          &lt;input
            type="text"
            name="colors"
            value={productData.colors.join(", ")}
            onChange={(e) =&gt;
              setProductData({
                ...productData,
                colors: e.target.value.split(",").map((color) =&gt; color.trim()),
              })
            }
            className="w-full border border-gray-300 rounded-md p-2"
          /&gt;
        &lt;/div&gt;

        {/* Image Upload - 圖片上傳 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;Upload Image&lt;/label&gt;
          &lt;input type="file" onChange={handleImageUpload} /&gt;
          {uploading &amp;&amp; &lt;p&gt;Uploading image...&lt;/p&gt;}
          &lt;div className="flex gap-4 mt-4"&gt;
            {productData.images.map((image, index) =&gt; (
              &lt;div key={index}&gt;
                &lt;img
                  src={image.url}
                  alt={image.altText || "Product Image"}
                  className="w-20 h-20 object-cover rounded-md shadow-md"
                /&gt;
              &lt;/div&gt;
            ))}
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;button
          type="submit"
          className="w-full bg-green-500 text-white py-2 rounded-md hover:bg-green-600 transition-colors"
        &gt;
          Update Product
        &lt;/button&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
};

export default EditProductPage;
</code></pre>



<h2 class="wp-block-heading">製作登入時的效果</h2>



<pre class="wp-block-code"><code>// frontend/src/pages/Login.jsx
import React, { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import login from "../assets/login.webp";
import { loginUser } from "../redux/slices/authSlice";
import { useDispatch, useSelector } from "react-redux";
import { mergeCart } from "../redux/slices/cartSlice";

const Login = () =&gt; {
  const &#91;email, setEmail] = useState("");
  const &#91;password, setPassword] = useState("");
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const location = useLocation();
  const { user, guestId, loading } = useSelector((state) =&gt; state.auth);
  const { cart } = useSelector((state) =&gt; state.cart);

  // Get redirect parameter and check if it's checkout or something - 獲取重定向參數並檢查它是否為結帳或其他東西
  const redirect = new URLSearchParams(location.search).get("redirect") || "/";
  const isCheckoutRedirect = redirect.includes("checkout");

  useEffect(() =&gt; {
    if (user) {
      if (cart?.products.length &gt; 0 &amp;&amp; guestId) {
        dispatch(mergeCart({ guestId, user })).then(() =&gt; {
          navigate(isCheckoutRedirect ? "/checkout" : "/");
        });
      } else {
        navigate(isCheckoutRedirect ? "/checkout" : "/");
      }
    }
  }, &#91;user, guestId, cart, navigate, isCheckoutRedirect, dispatch]);

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    // console.log("User Login:", { email, password });
    dispatch(loginUser({ email, password }));
  };

  return (
    &lt;div className="flex"&gt;
      &lt;div className="w-full md:w-1/2 flex flex-col justify-center items-center p-8 md:p-12"&gt;
        &lt;form
          onSubmit={handleSubmit}
          className="w-full max-w-md bg-white p-8 rounded-lg border shadow-sm"
        &gt;
          &lt;div className="flex justify-center mb-6"&gt;
            &lt;h2 className="text-xl font-medium"&gt;Rabbit&lt;/h2&gt;
          &lt;/div&gt;
          &lt;h2 className="text-2xl font-bold text-center mb-6"&gt;Hey there! &lt;/h2&gt;
          &lt;p className="text-center mb-6"&gt;
            Enter your username and password to Login.
          &lt;/p&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Email&lt;/label&gt;
            &lt;input
              type="email"
              value={email}
              onChange={(e) =&gt; setEmail(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your email address"
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Password&lt;/label&gt;
            &lt;input
              type="password"
              value={password}
              onChange={(e) =&gt; setPassword(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your password"
            /&gt;
          &lt;/div&gt;
          &lt;button
            type="submit"
            className="w-full bg-black text-white p-2 rounded-lg font-semibold hover:bg-gray-800 transition"
          &gt;
            {loading ? "loading..." : "Sign In"}
          &lt;/button&gt;
          &lt;p className="mt-6 text-center text-sm"&gt;
            Don't have an account?{" "}
            &lt;Link
              to={`/register?redirect=${encodeURIComponent(redirect)}`}
              className="text-blue-500"
            &gt;
              Register
            &lt;/Link&gt;
          &lt;/p&gt;
        &lt;/form&gt;
      &lt;/div&gt;

      &lt;div className="hidden md:block w-1/2 bg-gray-800"&gt;
        &lt;div className="h-full flex flex-col justify-center items-center"&gt;
          &lt;img
            src={login}
            alt="Login to Account"
            className="h-&#91;750px] w-full object-cover"
          /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Login;
</code></pre>



<h2 class="wp-block-heading">製作註冊時的效果</h2>



<pre class="wp-block-code"><code>// frontend/src/pages/Register.jsx
import React, { useEffect, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import register from "../assets/register.webp";
import { registerUser } from "../redux/slices/authSlice";
import { useDispatch, useSelector } from "react-redux";
import { mergeCart } from "../redux/slices/cartSlice";

const Register = () =&gt; {
  const &#91;name, setName] = useState("");
  const &#91;email, setEmail] = useState("");
  const &#91;password, setPassword] = useState("");
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const location = useLocation();
  const { user, guestId, loading } = useSelector((state) =&gt; state.auth);
  const { cart } = useSelector((state) =&gt; state.cart);

  // Get redirect parameter and check if it's checkout or something - 獲取重定向參數並檢查它是否為結帳或其他東西
  const redirect = new URLSearchParams(location.search).get("redirect") || "/";
  const isCheckoutRedirect = redirect.includes("checkout");

  useEffect(() =&gt; {
    if (user) {
      if (cart?.products.length &gt; 0 &amp;&amp; guestId) {
        dispatch(mergeCart({ guestId, user })).then(() =&gt; {
          navigate(isCheckoutRedirect ? "/checkout" : "/");
        });
      } else {
        navigate(isCheckoutRedirect ? "/checkout" : "/");
      }
    }
  }, &#91;user, guestId, cart, navigate, isCheckoutRedirect, dispatch]);

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    // console.log("User Registered:", { name, email, password });
    dispatch(registerUser({ name, email, password }));
  };

  return (
    &lt;div className="flex"&gt;
      &lt;div className="w-full md:w-1/2 flex flex-col justify-center items-center p-8 md:p-12"&gt;
        &lt;form
          onSubmit={handleSubmit}
          className="w-full max-w-md bg-white p-8 rounded-lg border shadow-sm"
        &gt;
          &lt;div className="flex justify-center mb-6"&gt;
            &lt;h2 className="text-xl font-medium"&gt;Rabbit&lt;/h2&gt;
          &lt;/div&gt;
          &lt;h2 className="text-2xl font-bold text-center mb-6"&gt;Hey there! &lt;/h2&gt;
          &lt;p className="text-center mb-6"&gt;
            Enter your username and password to Login.
          &lt;/p&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Name&lt;/label&gt;
            &lt;input
              type="text"
              value={name}
              onChange={(e) =&gt; setName(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your name"
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Email&lt;/label&gt;
            &lt;input
              type="email"
              value={email}
              onChange={(e) =&gt; setEmail(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your email address"
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Password&lt;/label&gt;
            &lt;input
              type="password"
              value={password}
              onChange={(e) =&gt; setPassword(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your password"
            /&gt;
          &lt;/div&gt;
          &lt;button
            type="submit"
            className="w-full bg-black text-white p-2 rounded-lg font-semibold hover:bg-gray-800 transition"
          &gt;
            {loading ? "loading..." : "Sign up"}
          &lt;/button&gt;
          &lt;p className="mt-6 text-center text-sm"&gt;
            Don't have an account?{" "}
            &lt;Link
              to={`/login?redirect=${encodeURIComponent(redirect)}`}
              className="text-blue-500"
            &gt;
              Login
            &lt;/Link&gt;
          &lt;/p&gt;
        &lt;/form&gt;
      &lt;/div&gt;

      &lt;div className="hidden md:block w-1/2 bg-gray-800"&gt;
        &lt;div className="h-full flex flex-col justify-center items-center"&gt;
          &lt;img
            src={register}
            alt="Login to Account"
            className="h-&#91;750px] w-full object-cover"
          /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Register;
</code></pre>



<h2 class="wp-block-heading">修正訂單確認頁面</h2>



<ul class="wp-block-list">
<li>修正訂單確認後清空購物車<br>navigate 的 /my-order 要改為 /my-orders</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/pages/OrderConfirmationPage.jsx
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { clearCart } from "../redux/slices/cartSlice";

const OrderConfirmationPage = () =&gt; {
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const { checkout } = useSelector((state) =&gt; state.checkout);

  // Clear the cart when the order is confirmed - 訂單確認後清空購物車
  useEffect(() =&gt; {
    if (checkout &amp;&amp; checkout._id) {
      dispatch(clearCart());
      localStorage.removeItem("cart");
    } else {
      navigate("/my-orders");
    }
  }, &#91;checkout, dispatch, navigate]);

  const calculateEstimatedDelivery = (createdAt) =&gt; {
    const orderDate = new Date(createdAt);
    orderDate.setDate(orderDate.getDate() + 10); // Add 10 days to the order date - 訂單日期增加10天
    return orderDate.toLocaleDateString();
  };

  return (
    &lt;div className="max-w-4xl mx-auto p-6 bg-white"&gt;
      &lt;h1 className="text-4xl font-bold text-center text-emerald-700 mb-8"&gt;
        Thank You for Your Order!
      &lt;/h1&gt;

      {checkout &amp;&amp; (
        &lt;div className="p-6 rounded-lg border"&gt;
          &lt;div className="flex justify-between mb-20"&gt;
            {/* Order Id and Date - 訂單編號和日期 */}
            &lt;div&gt;
              &lt;h2 className="text-xl font-semibold"&gt;
                Order ID: {checkout._id}
              &lt;/h2&gt;
              &lt;p className="text-gray-500"&gt;
                Order date: {new Date(checkout.createdAt).toLocaleDateString()}
              &lt;/p&gt;
            &lt;/div&gt;
            {/* Estimated Delivery- 預計送達 */}
            &lt;div&gt;
              &lt;p className="text-emerald-700 text-sm"&gt;
                Estimated Delivery:{" "}
                {calculateEstimatedDelivery(checkout.createdAt)}
              &lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* Ordered Items - 訂單項目 */}
          &lt;div className="mb-20"&gt;
            {checkout.checkoutItems.map((item) =&gt; (
              &lt;div key={item.productId} className="flex items-center mb-4"&gt;
                &lt;img
                  src={item.image}
                  alt={item.name}
                  className="w-16 h-16 object-cover rounded-md mr-4"
                /&gt;
                &lt;div&gt;
                  &lt;h4 className="text-md font-semibold"&gt;{item.name}&lt;/h4&gt;
                  &lt;p className="text-sm text-gray-500"&gt;
                    {item.color} | {item.size}
                  &lt;/p&gt;
                &lt;/div&gt;
                &lt;div className="ml-auto text-right"&gt;
                  &lt;p className="text-md"&gt;${item.price}&lt;/p&gt;
                  &lt;p className="text-sm text-gray-500"&gt;Qty: {item.quantity}&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            ))}
          &lt;/div&gt;
          {/* Payment and Delivery Info - 付款與送貨資訊 */}
          &lt;div className="grid grid-cols-2 gap-8"&gt;
            {/* Payment Info - 付款資訊 */}
            &lt;div&gt;
              &lt;h4 className="text-lg font-semibold mb-2"&gt;Payment&lt;/h4&gt;
              &lt;p className="text-gray-600"&gt;PayPal&lt;/p&gt;
            &lt;/div&gt;

            {/* Delivery Info - 送貨資訊 */}
            &lt;div&gt;
              &lt;h4 className="text-lg font-semibold mb-2"&gt;Delivery&lt;/h4&gt;
              &lt;p className="text-gray-600"&gt;
                {checkout.shippingAddress.address}
              &lt;/p&gt;
              &lt;p className="text-gray-600"&gt;
                {checkout.shippingAddress.city},{" "}
                {checkout.shippingAddress.country}
              &lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  );
};

export default OrderConfirmationPage;
</code></pre>



<p>以上完成了前端、後端、Redux(狀態管理)的開發與整合。</p>



<p></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Build &#038; Deploy Full Stack E-commerce Website &#124; Redux &#124; MERN Stack – 03</title>
		<link>/wordpress_blog/full-stack-rabbit-03/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Wed, 26 Mar 2025 00:45:04 +0000</pubDate>
				<category><![CDATA[Youtube]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=889</guid>

					<description><![CDATA[學習來自 YT:&#160;compiletab影片:&#038;nbsp [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>學習來自 YT:&nbsp;<a rel="noreferrer noopener" href="https://www.youtube.com/@compiletab" target="_blank">compiletab</a><br>影片:&nbsp;<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=hpgh2BTtac8" target="_blank">Build &amp; Deploy Full Stack E-commerce Website | Redux | MERN Stack Project</a><br>Github assets:&nbsp;<a rel="noreferrer noopener" href="https://github.com/kushald/rabbit-assets" target="_blank">連結</a><br>Thank you, teacher</p>



<h1 class="wp-block-heading">建立 &amp; 部署全端商業網站</h1>



<h2 class="wp-block-heading">影片時間戳</h2>



<p>00:00:00 – Introduction<br>00:01:56 – Demo<br>00:05:26 – Installation &amp; set up<br>05:50:25 – Admin UI<br>07:20:25 – Backend setup<br>07:32:40 – user routes<br>08:05:40 – Products routes<br>09:18:56 – cart routes<br>10:13:26 – checkout routes<br>10:57:55 – Admin routes<br>11:58:30 – Redux</p>



<h2 class="wp-block-heading">Redux</h2>



<p>Redux 透過提供統一的狀態儲存、規範化的狀態更新流程，讓開發者能夠更容易地管理大型應用的狀態，並提高應用的可維護性和可擴展性。</p>



<ul class="wp-block-list">
<li><a href="https://redux.js.org/" target="_blank" rel="noreferrer noopener">Redux</a> – A JS library for predictable and maintainable global state management</li>
</ul>



<h2 class="wp-block-heading">安裝套件</h2>



<pre class="wp-block-code"><code>// frontend
// terminal - 終端機
npm i react-redux @reduxjs/toolkit axios
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/store.js
import { configureStore } from "@reduxjs/toolkit";

const store = configureStore({
  reducer: {
},
});

export default store;
</code></pre>



<pre class="wp-block-code"><code>// frontend/.env
VITE_PAYPAL_CLIENT_ID=你的client-id
VITE_BACKEND_URL=你後端的localhost
</code></pre>



<h2 class="wp-block-heading">Auth Slice (身份驗證切片)</h2>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/authSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// Retrieve user info and token from localStorage if available - 如果可用，從本地存儲中獲取用戶資訊和令牌
const userFromStorage = localStorage.getItem("userInfo")
  ? JSON.parse(localStorage.getItem("userInfo"))
  : null;

// Check for an existing guest ID in the localStorage or generate a new one - 檢查本地存儲中是否已有現有的訪客 ID，若沒有則生成一個新的
const initialGuestId =
  localStorage.getItem("guestId") || `guest_${new Date().getTime()}`;
localStorage.setItem("guestId", initialGuestId);

// Initial state - 初始狀態
const initialState = {
  user: userFromStorage,
  guestId: initialGuestId,
  loading: false,
  error: null,
};

// Async Thunk for User Login - 用於用戶登入的非同步延遲函數
export const loginUser = createAsyncThunk(
  "auth/loginUser",
  async (userData, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/users/login`,
        userData
      );
      localStorage.setItem("userInfo", JSON.stringify(response.data.user));
      localStorage.setItem("userToken", response.data.token);

      return response.data.user; // Return the user object from the response - 從回應中返回用戶對象
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Async Thunk for User Registration - 用於用戶註冊的非同步延遲函數
export const registerUser = createAsyncThunk(
  "auth/registerUser",
  async (userData, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/users/register`,
        userData
      );
      localStorage.setItem("userInfo", JSON.stringify(response.data.user));
      localStorage.setItem("userToken", response.data.token);

      return response.data.user; // Return the user object from the response - 從回應中返回用戶對象
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Slice - 切片
const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {
    logout: (state) =&gt; {
      state.user = null;
      state.guestId = `guest_${new Date().getTime()}`; // Reset guest ID on logout - 在登出時重置訪客 ID
      localStorage.removeItem("userInfo");
      localStorage.removeItem("userToken");
      localStorage.setItem("guestId", state.guestId); // Set new guest ID in localStorage - 在本地存儲中設置新的訪客 ID
    },
    generateNewGuestId: (state) =&gt; {
      state.guestId = `guest_${new Date().getTime()}`;
      localStorage.setItem("guestId", state.guestId);
    },
  },
  extraReducers: (builder) =&gt; {
    builder
      .addCase(loginUser.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(loginUser.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
      })
      .addCase(loginUser.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload.message;
      })
      .addCase(registerUser.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(registerUser.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
      })
      .addCase(registerUser.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload.message;
      });
  },
});

export const { logout, generateNewGuestId } = authSlice.actions;
export default authSlice.reducer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UserLayout from "./components/Layout/UserLayout";
import Home from "./pages/Home";
import { Toaster } from "sonner";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Profile from "./pages/Profile";
import CollectionPage from "./pages/CollectionPage";
import ProductDetails from "./components/Products/ProductDetails";
import Checkout from "./components/Cart/Checkout";
import OrderConfirmationPage from "./pages/OrderConfirmationPage";
import OrderDetailsPage from "./pages/OrderDetailsPage";
import MyOrdersPage from "./pages/MyOrdersPage";
import AdminLayout from "./components/Admin/AdminLayout";
import AdminHomePage from "./pages/AdminHomePage";
import UserManagement from "./components/Admin/UserManagement";
import ProductManagement from "./components/Admin/ProductManagement";
import EditProductPage from "./components/Admin/EditProductPage";
import OrderManagement from "./components/Admin/OrderManagement";

import { Provider } from "react-redux";
import store from "./redux/store";

const App = () =&gt; {
  return (
    &lt;Provider store={store}&gt;
      &lt;BrowserRouter&gt;
        &lt;Toaster position="top-right" /&gt;
        &lt;Routes&gt;
          &lt;Route path="/" element={&lt;UserLayout /&gt;}&gt;
            {/* User Layout - 使用者佈局 */}
            &lt;Route index element={&lt;Home /&gt;} /&gt;
            &lt;Route path="login" element={&lt;Login /&gt;} /&gt;
            &lt;Route path="register" element={&lt;Register /&gt;} /&gt;
            &lt;Route path="profile" element={&lt;Profile /&gt;} /&gt;
            &lt;Route
              path="collections/:collection"
              element={&lt;CollectionPage /&gt;}
            /&gt;
            &lt;Route path="product/:id" element={&lt;ProductDetails /&gt;} /&gt;
            &lt;Route path="checkout" element={&lt;Checkout /&gt;} /&gt;
            &lt;Route
              path="order-confirmation"
              element={&lt;OrderConfirmationPage /&gt;}
            /&gt;
            &lt;Route path="order/:id" element={&lt;OrderDetailsPage /&gt;} /&gt;
            &lt;Route path="my-orders" element={&lt;MyOrdersPage /&gt;} /&gt;
          &lt;/Route&gt;
          &lt;Route path="/admin" element={&lt;AdminLayout /&gt;}&gt;
            {/* Admin Layout - 管理員佈局 */}
            &lt;Route index element={&lt;AdminHomePage /&gt;} /&gt;
            &lt;Route path="users" element={&lt;UserManagement /&gt;} /&gt;
            &lt;Route path="products" element={&lt;ProductManagement /&gt;} /&gt;
            &lt;Route path="products/:id/edit" element={&lt;EditProductPage /&gt;} /&gt;
            &lt;Route path="orders" element={&lt;OrderManagement /&gt;} /&gt;
          &lt;/Route&gt;
        &lt;/Routes&gt;
      &lt;/BrowserRouter&gt;
    &lt;/Provider&gt;
  );
};

export default App;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Login.jsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import login from "../assets/login.webp";
import { loginUser } from "../redux/slices/authSlice";
import { useDispatch } from "react-redux";

const Login = () =&gt; {
  const &#91;email, setEmail] = useState("");
  const &#91;password, setPassword] = useState("");
  const dispatch = useDispatch();

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    // console.log("User Login:", { email, password });
    dispatch(loginUser({ email, password }));
  };

  return (
    &lt;div className="flex"&gt;
      &lt;div className="w-full md:w-1/2 flex flex-col justify-center items-center p-8 md:p-12"&gt;
        &lt;form
          onSubmit={handleSubmit}
          className="w-full max-w-md bg-white p-8 rounded-lg border shadow-sm"
        &gt;
          &lt;div className="flex justify-center mb-6"&gt;
            &lt;h2 className="text-xl font-medium"&gt;Rabbit&lt;/h2&gt;
          &lt;/div&gt;
          &lt;h2 className="text-2xl font-bold text-center mb-6"&gt;Hey there! &lt;/h2&gt;
          &lt;p className="text-center mb-6"&gt;
            Enter your username and password to Login.
          &lt;/p&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Email&lt;/label&gt;
            &lt;input
              type="email"
              value={email}
              onChange={(e) =&gt; setEmail(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your email address"
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Password&lt;/label&gt;
            &lt;input
              type="password"
              value={password}
              onChange={(e) =&gt; setPassword(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your password"
            /&gt;
          &lt;/div&gt;
          &lt;button
            type="submit"
            className="w-full bg-black text-white p-2 rounded-lg font-semibold hover:bg-gray-800 transition"
          &gt;
            Sign In
          &lt;/button&gt;
          &lt;p className="mt-6 text-center text-sm"&gt;
            Don't have an account?{" "}
            &lt;Link to="/register" className="text-blue-500"&gt;
              Register
            &lt;/Link&gt;
          &lt;/p&gt;
        &lt;/form&gt;
      &lt;/div&gt;

      &lt;div className="hidden md:block w-1/2 bg-gray-800"&gt;
        &lt;div className="h-full flex flex-col justify-center items-center"&gt;
          &lt;img
            src={login}
            alt="Login to Account"
            className="h-&#91;750px] w-full object-cover"
          /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Login;
</code></pre>



<h2 class="wp-block-heading">測試登入頁面功能</h2>



<ul class="wp-block-list">
<li>http://localhost:5173/login</li>



<li>輸入電子信箱、密碼 > 登入</li>



<li>檢查 > Network<br>可以看到 token 和 user 資料</li>



<li>檢查 > Application<br>從 Local storage 可以看到 userInfo、userToken</li>



<li>檢查 > Console<br>出現一些問題，進行除錯</li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/redux/store.js
// 除錯
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";

const store = configureStore({
  reducer: {
    auth: authReducer,
  },
});

export default store;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Register.jsx
</code></pre>



<h2 class="wp-block-heading">測試註冊頁面功能</h2>



<ul class="wp-block-list">
<li>http://localhost:5173/register</li>



<li>輸入名字、電子信箱、密碼</li>



<li>檢查 > Network<br>可以看到 user 和 token</li>



<li>檢查 > Application<br>從 Local storage 可以看到 userInfo、userToken</li>
</ul>



<h2 class="wp-block-heading">查看 MongoDB Atlas 資料庫</h2>



<ul class="wp-block-list">
<li>Browse collections (瀏覽集合) > users<br>查看是否有更新使用者</li>
</ul>



<h2 class="wp-block-heading">Product Slice (產品切片)</h2>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/productsSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// Async Thunk to Fetch Products by Collection and optional Filters - 使用非同步函數根據集合和可選過濾條件獲取產品
export const fetchProductsByFilters = createAsyncThunk(
  "products/fetchByFilters",
  async ({
    collection,
    size,
    color,
    gender,
    minPrice,
    maxPrice,
    sortBy,
    search,
    category,
    material,
    brand,
    limit,
  }) =&gt; {
    const query = new URLSearchParams();
    if (collection) query.append("collection", collection);
    if (size) query.append("size", size);
    if (color) query.append("color", color);
    if (gender) query.append("gender", gender);
    if (minPrice) query.append("minPrice", minPrice);
    if (maxPrice) query.append("maxPrice", maxPrice);
    if (sortBy) query.append("sortBy", sortBy);
    if (search) query.append("search", search);
    if (category) query.append("category", category);
    if (material) query.append("material", material);
    if (brand) query.append("brand", brand);
    if (limit) query.append("limit", limit);

    const response = await axios.get(
      `${import.meta.env.VITE_BACKEND_URL}/api/products?${query.toString()}`
    );
    return response.data;
  }
);

// Async thunk to fetch a single product by ID - 使用非同步函數根據 ID 獲取單個產品
export const fetchProductDetails = createAsyncThunk(
  "products/fetchProductDetails",
  async (id) =&gt; {
    const response = await axios.get(
      `${import.meta.env.VITE_BACKEND_URL}/api/products/${id}`
    );
    return response.data;
  }
);

// Async thunk to fetch update existing products - 使用非同步函數獲取並更新現有產品
export const updateProduct = createAsyncThunk(
  "products/updateProduct",
  async ({ id, productData }) =&gt; {
    const response = await axios.put(
      `${import.meta.env.VITE_BACKEND_URL}/api/products/${id}`,
      productData,
      {
        headers: {
          Authorization: `Bearer ${localStorage.getItem("userToken")}`,
        },
      }
    );
    return response.data;
  }
);

// Async thunk to fetch similar products - 使用非同步函數獲取相似產品
export const fetchSimilarProducts = createAsyncThunk(
  "products/fetchSimilarProducts",
  async ({ id }) =&gt; {
    const response = await axios.get(
      `${import.meta.env.VITE_BACKEND_URL}/api/products/similar/${id}`
    );
    return response.data;
  }
);

const productsSlice = createSlice({
  name: "products",
  initialState: {
    products: &#91;],
    selectedProduct: null, // Store the details of the single Product - 儲存單一產品的詳細資料
    similarProducts: &#91;],
    loading: false,
    error: null,
    filters: {
      category: "",
      size: "",
      color: "",
      gender: "",
      brand: "",
      minPrice: "",
      maxPrice: "",
      sortBy: "",
      search: "",
      material: "",
      collection: "",
    },
  },
  reducers: {
    setFilters: (state, action) =&gt; {
      state.filters = { ...state.filters, ...action.payload };
    },
    clearFilters: (state) =&gt; {
      state.filters = {
        category: "",
        size: "",
        color: "",
        gender: "",
        brand: "",
        minPrice: "",
        maxPrice: "",
        sortBy: "",
        search: "",
        material: "",
        collection: "",
      };
    },
  },
  extraReducers: (builder) =&gt; {
    builder
      // Handle fetching products with filter - 處理使用篩選條件獲取產品
      .addCase(fetchProductsByFilters.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchProductsByFilters.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.products = Array.isArray(action.payload) ? action.payload : &#91;];
      })
      .addCase(fetchProductsByFilters.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      })
      // Handle fetching single product details - 處理獲取單一產品詳細資料
      .addCase(fetchProductDetails.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchProductDetails.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.selectedProduct = action.payload;
      })
      .addCase(fetchProductDetails.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      })
      // Handle updating product - 處理更新產品
      .addCase(updateProduct.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(updateProduct.fulfilled, (state, action) =&gt; {
        state.loading = false;
        const updatedProduct = action.payload;
        const index = state.products.findIndex(
          (product) =&gt; product._id === updateProduct._id
        );
        if (index !== -1) {
          state.products&#91;index] = updateProduct;
        }
      })
      .addCase(updateProduct.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      })
      // Handle fetching similar products - 處理獲取相似產品
      .addCase(fetchSimilarProducts.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchSimilarProducts.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.products = action.payload;
      })
      .addCase(fetchSimilarProducts.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      });
  },
});

export const { setFilters, clearFilters } = productsSlice.actions;
export default productsSlice.reducer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/store.js
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";
import productReducer from "./slices/productsSlice";

const store = configureStore({
  reducer: {
    auth: authReducer,
    products: productReducer,
  },
});

export default store;
</code></pre>



<h2 class="wp-block-heading">Cart Slice (購物車切片)</h2>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/cartSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// Helper function to load cart from localStorage - 從本地儲存載入購物車的輔助函數
const loadCartFromStorage = () =&gt; {
  const storedCart = localStorage.getItem("cart");
  return storedCart ? JSON.parse(storedCart) : { products: &#91;] };
};

// Helper function to save cart to localStorage - 將購物車儲存到本地儲存的輔助函數
const saveCartToStorage = (cart) =&gt; {
  localStorage.setItem("cart", JSON.stringify(cart));
};

// Fetch cart for a user or guest - 為用戶或訪客獲取購物車
export const fetchCart = createAsyncThunk(
  "cart/fetchCart",
  async ({ userId, guestId }, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.get(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        {
          params: { userId, guestId },
        }
      );
      return response.data;
    } catch (error) {
      console.error(error);
      return rejectWithValue(error.response.data);
    }
  }
);

// Add an item to the cart for a user or guest - 為用戶或訪客將商品加入購物車
export const addToCart = createAsyncThunk(
  "cart/addToCart",
  async (
    { productId, quantity, size, color, guestId, userId },
    { rejectWithValue }
  ) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        {
          productId,
          quantity,
          size,
          color,
          guestId,
          userId,
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Update the quantity of an item in the cart - 更新購物車中商品的數量
export const updateCartItemQuantity = createAsyncThunk(
  "cart/updateCartItemQuantity",
  async (
    { productId, quantity, guestId, userId, size, color },
    { rejectWithValue }
  ) =&gt; {
    try {
      const response = await axios.put(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        {
          productId,
          quantity,
          guestId,
          userId,
          size,
          color,
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Remove an item from the cart - 從購物車中移除商品
export const removeFromCart = createAsyncThunk(
  "cart/removeFromCart",
  async ({ productId, guestId, userId, size, color }, { rejectWithValue }) =&gt; {
    try {
      const response = await axios({
        method: "DELETE",
        url: `${import.meta.env.VITE_BACKEND_URL}/api/cart`,
        data: {
          productId,
          guestId,
          userId,
          size,
          color,
        },
      });
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Merge guest cart into user cart
export const mergeCart = createAsyncThunk(
  "cart/mergeCart",
  async ({ guestId, user }, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/cart/merge`,
        { guestId, user },
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

const cartSlice = createSlice({
  name: "cart",
  initialState: {
    cart: loadCartFromStorage(),
    loading: false,
    error: null,
  },
  reducers: {
    clearCart: (state) =&gt; {
      state.cart = { products: &#91;] };
      localStorage.removeItem("cart");
    },
  },
  extraReducers: (builder) =&gt; {
    builder
      .addCase(fetchCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(fetchCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message || "Failed to fetch cart";
      })
      .addCase(addToCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(addToCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(addToCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload?.message || "Failed to add to cart";
      })
      .addCase(updateCartItemQuantity.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(updateCartItemQuantity.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(updateCartItemQuantity.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error =
          action.payload?.message || "Failed to update item quantity";
      })
      .addCase(removeFromCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(removeFromCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(removeFromCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload?.message || "Failed to remove item";
      })
      .addCase(mergeCart.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(mergeCart.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload;
        saveCartToStorage(action.payload);
      })
      .addCase(mergeCart.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload?.message || "Failed to merge cart";
      });
  },
});

export const { clearCart } = cartSlice.actions;
export default cartSlice.reducer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/store.js
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";
import productReducer from "./slices/productsSlice";
import cartReducer from "./slices/cartSlice";

const store = configureStore({
  reducer: {
    auth: authReducer,
    products: productReducer,
    cart: cartReducer,
  },
});

export default store;
</code></pre>



<h2 class="wp-block-heading">Checkout Slice (結帳切片)</h2>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/checkoutSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// Async thunk to create a checkout session - 創建結帳會話的非同步函數
export const createCheckout = createAsyncThunk(
  "checkout/createCheckout",
  async (checkoutdata, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/checkout`,
        checkoutdata,
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

const checkoutSlice = createSlice({
  name: "checkout",
  initialState: {
    checkout: null,
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) =&gt; {
    builder.addCase(createCheckout.pending, (state) =&gt; {
      state.loading = true;
      state.error = null;
    })
    .addCase(createCheckout.fulfilled, (state, action) =&gt; {
      state.loading = false;
      state.checkout = action.payload;
    });
    .addCase(createCheckout.rejected, (state, action) =&gt; {
      state.loading = true;
      state.error = action.payload.message;
    });
  },
});

export default checkoutSlice.reducer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/store.js
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";
import productReducer from "./slices/productsSlice";
import cartReducer from "./slices/cartSlice";
import checkoutReducer from "./slices/checkoutSlice";

const store = configureStore({
  reducer: {
    auth: authReducer,
    products: productReducer,
    cart: cartReducer,
    checkout: checkoutReducer,
  },
});

export default store;
</code></pre>



<h2 class="wp-block-heading">Order Slice (訂單切片)</h2>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/orderSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// Async Thunk to fetch user orders - 用於獲取用戶訂單的非同步函數
export const fetchUserOrders = createAsyncThunk(
  "orders/fetchUserOrders",
  async (_, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.get(
        `${import.meta.env.VITE_BACKEND_URL}/api/orders/my-orders`,
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Async thunk to fetch orders details by ID - 根據 ID 獲取訂單詳情的非同步函數
export const fetchOrderDetails = createAsyncThunk(
  "orders/fetchOrderDetails",
  async (orderId, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.get(
        `${import.meta.env.VITE_BACKEND_URL}/api/orders/${orderId}`,
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      return response.data;
    } catch (error) {
      rejectWithValue(error.response.data);
    }
  }
);

const orderSlice = createSlice({
  name: "orders",
  initialState: {
    orders: &#91;],
    totalOrders: 0,
    orderDetails: null,
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) =&gt; {
    builder
      // Fetch user order - 獲取用戶訂單
      .addCase(fetchUserOrders.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUserOrders.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.orders = action.payload;
      })
      .addCase(fetchUserOrders.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload.message;
      })
      // Fetch order details - 獲取訂單詳情
      .addCase(fetchOrderDetails.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchOrderDetails.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.orderDetails = action.payload;
      })
      .addCase(fetchOrderDetails.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload.message;
      });
  },
});

export default orderSlice.reducer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/store.js
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";
import productReducer from "./slices/productsSlice";
import cartReducer from "./slices/cartSlice";
import checkoutReducer from "./slices/checkoutSlice";
import orderReducer from "./slices/orderSlice";

const store = configureStore({
  reducer: {
    auth: authReducer,
    products: productReducer,
    cart: cartReducer,
    checkout: checkoutReducer,
    orders: orderReducer,
  },
});

export default store;
</code></pre>



<h2 class="wp-block-heading">Admin Slice (管理員切片)</h2>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/adminSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// fetch all users (admin only) - 獲取所有用戶 (僅限管理員)
export const fetchUsers = createAsyncThunk("admin/fetchUsers", async () =&gt; {
  const response = await axios.get(
    `${import.meta.env.VITE_BACKEND_URL}/api/admin/users`,
    {
      headers: { Authorization: `Bearer ${localStorage.getItem("userToken")}` },
    }
  );
  response.data;
});

// Add the create user action - 新增創建用戶的動作
export const addUser = createAsyncThunk(
  "admin/addUser",
  async (userData, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.post(
        `${import.meta.env.VITE_BACKEND_URL}/api/admin/users`,
        userData,
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Update user info - 更新用戶資訊
export const updateUser = createAsyncThunk(
  "admin/updateUser",
  async ({ id, name, email, role }) =&gt; {
    const response = await axios.put(
      `${import.meta.env.VITE_BACKEND_URL}/api/admin/users/${id}`,
      { name, email, role },
      {
        headers: {
          Authorization: `Bearer ${localStorage.getItem("userToken")}`,
        },
      }
    );
    return response.data;
  }
);

// Delete a user - 刪除用戶
export const deleteUser = createAsyncThunk("admin/deleteUser", async (id) =&gt; {
  await axios.delete(
    `${import.meta.env.VITE_BACKEND_URL}/api/admin/users/${id}`,
    {
      headers: {
        Authorization: `Bearer ${localStorage.getItem("userToken")}`,
      },
    }
  );
  return id;
});

const adminSlice = createSlice({
  name: "admin",
  initialState: {
    users: &#91;],
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) =&gt; {
    builder
      .addCase(fetchUsers.pending, (state) =&gt; {
        state.loading = true;
      })
      .addCase(fetchUsers.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.users = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      })
      .addCase(updateUser.fulfilled, (state, action) =&gt; {
        const updatedUser = action.payload;
        const userIndex = state.users.findIndex(
          (user) =&gt; user._id === updatedUser._id
        );
        if (userIndex !== -1) {
          state.users&#91;userIndex] = updatedUser;
        }
      })
      .addCase(deleteUser.fulfilled, (state, action) =&gt; {
        state.users = state.users.filter((user) =&gt; user._id !== action.payload);
      })
      .addCase(addUser.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(addUser.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.users.push(action.payload.user); // add a new user to the state - 將新用戶增加到狀態中
      })
      .addCase(addUser.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload.message;
      });
  },
});

export default adminSlice.reducer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/store.js
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";
import productReducer from "./slices/productsSlice";
import cartReducer from "./slices/cartSlice";
import checkoutReducer from "./slices/checkoutSlice";
import orderReducer from "./slices/orderSlice";
import adminReducer from "./slices/adminSlice";

const store = configureStore({
  reducer: {
    auth: authReducer,
    products: productReducer,
    cart: cartReducer,
    checkout: checkoutReducer,
    orders: orderReducer,
    admin: adminReducer,
  },
});

export default store;
</code></pre>



<h2 class="wp-block-heading">Admin Product Slice (管理員產品切片)</h2>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/adminProductSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

const API_URL = `${import.meta.env.VITE_BACKEND_URL}`;
const USER_TOKEN = `Bearer ${localStorage.getItem("userToken")}`;

// async thunk to fetch admin products - 獲取管理員產品的非同步函數
export const fetchAdminProducts = createAsyncThunk(
  "adminProducts/fetchProducts",
  async () =&gt; {
    const response = await axios.get(`${API_URL}/api/admin/products`, {
      headers: {
        Authorization: USER_TOKEN,
      },
    });
    return response.data;
  }
);

// async function to create a new product - 創建新產品的非同步函數
export const createProduct = createAsyncThunk(
  "adminProducts/createProduct",
  async (productData) =&gt; {
    const response = await axios.post(
      `${API_URL}/api/admin/products`,
      productData,
      {
        headers: {
          Authorization: USER_TOKEN,
        },
      }
    );
    return response.data;
  }
);

// async thunk to update an existing product - 更新現有產品的非同步函數
export const updateProduct = createAsyncThunk(
  "adminProducts/updateProduct",
  async ({ id, productData }) =&gt; {
    const response = await axios.put(
      `${API_URL}/api/admin/products/${id}`,
      productData,
      {
        headers: {
          Authorization: USER_TOKEN,
        },
      }
    );
    return response.data;
  }
);

// async thunk to delete a product - 刪除產品的非同步函數
export const deleteProduct = createAsyncThunk(
  "adminProducts/deleteProduct",
  async (id) =&gt; {
    await axios.delete(`${API_URL}/api/admin/products/${id}`, {
      headers: { Authorization: USER_TOKEN },
    });
    return id;
  }
);

const adminProductSlice = createSlice({
  name: "adminProducts",
  initialState: {
    products: &#91;],
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) =&gt; {
    builder
      .addCase(fetchAdminProducts.pending, (state) =&gt; {
        state.loading = true;
      })
      .addCase(fetchAdminProducts.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.products = action.payload;
      })
      .addCase(fetchAdminProducts.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.error.message;
      })
      // Create Product - 新增產品
      .addCase(createProduct.fulfilled, (state, action) =&gt; {
        state.products.push(action.payload);
      })
      // Update Product - 更新產品
      .addCase(updateProduct.fulfilled, (state, action) =&gt; {
        const index = state.products.findIndex(
          (product) =&gt; product._id === action.payload._id
        );
        if (index !== -1) {
          state.products&#91;index] = action.payload;
        }
      })
      // Delete Product - 刪除產品
      .addCase(deleteProduct.fulfilled, (state, action) =&gt; {
        state.products = state.products.filter(
          (product) =&gt; product._id !== action.payload
        );
      });
  },
});

export default adminProductSlice.reducer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/store.js
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";
import productReducer from "./slices/productsSlice";
import cartReducer from "./slices/cartSlice";
import checkoutReducer from "./slices/checkoutSlice";
import orderReducer from "./slices/orderSlice";
import adminReducer from "./slices/adminSlice";
import adminProductReducer from "./slices/adminProductSlice";

const store = configureStore({
  reducer: {
    auth: authReducer,
    products: productReducer,
    cart: cartReducer,
    checkout: checkoutReducer,
    orders: orderReducer,
    admin: adminReducer,
    adminProducts: adminProductReducer,
  },
});

export default store;
</code></pre>



<h2 class="wp-block-heading">Admin Order Slice (管理員訂單切片)</h2>



<pre class="wp-block-code"><code>// frontend/src/redux/slices/adminOrderSlice.js
import {
  createSlice,
  createAsyncThunk,
  __DO_NOT_USE__ActionTypes,
} from "@reduxjs/toolkit";
import axios from "axios";

// Fetch all orders (admin only) - 獲取全部訂單 (僅限管理員)
export const fetchAllOrders = createAsyncThunk(
  "adminOrders/fetchAllOrders",
  async (_, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.get(
        `${import.meta.env.VITE_BACKEND_URL}/api/admin/orders`,
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Update order delivery status - 更新訂單配送狀態
export const updateOrderStatus = createAsyncThunk(
  "adminOrders/updateOrderStatus",
  async ({ id, status }, { rejectWithValue }) =&gt; {
    try {
      const response = await axios.put(
        `${import.meta.env.VITE_BACKEND_URL}/api/admin/orders/${id}`,
        { status },
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      return response.data;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

// Delete an order - 刪除訂單
export const deleteOrder = createAsyncThunk(
  "adminOrders/deleteOrder",
  async (id, { rejectWithValue }) =&gt; {
    try {
      await axios.delete(
        `${import.meta.env.VITE_BACKEND_URL}/api/admin/orders/${id}`,
        {
          headers: {
            Authorization: `Bearer ${localStorage.getItem("userToken")}`,
          },
        }
      );
      return id;
    } catch (error) {
      return rejectWithValue(error.response.data);
    }
  }
);

const adminOrderSlice = createSlice({
  name: "adminOrders",
  initialState: {
    orders: &#91;],
    totalOrders: 0,
    totalSales: 0,
    loading: false,
    error: null,
  },
  reducers: {},
  extraReducers: (builder) =&gt; {
    builder
      // Fetch all orders - 獲取所有訂單
      .addCase(fetchAllOrders.pending, (state) =&gt; {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchAllOrders.fulfilled, (state, action) =&gt; {
        state.loading = false;
        state.orders = action.payload;
        state.totalOrders = action.payload.length;

        // Calculate total sales - 計算總銷售額
        const totalSales = action.payload.reduce((acc, order) =&gt; {
          return acc + order.totalPrice;
        }, 0);
        state.totalSales = totalSales;
      })
      .addCase(fetchAllOrders.rejected, (state, action) =&gt; {
        state.loading = false;
        state.error = action.payload.message;
      })
      // Update order status - 更新訂單狀態
      .addCase(updateOrderStatus.fulfilled, (state, action) =&gt; {
        const updatedOrder = action.payload;
        const orderIndex = state.orders.findIndex(
          (order) =&gt; order._id === updatedOrder._id
        );
        if (orderIndex !== -1) {
          state.orders&#91;orderIndex] = updatedOrder;
        }
      })
      // Delete order - 刪除訂單
      .addCase(deleteOrder.fulfilled, (state, action) =&gt; {
        state.orders = state.orders.filter(
          (order) =&gt; order._id !== action.payload
        );
      });
  },
});

export default adminOrderSlice.reducer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/redux/store.js
import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/authSlice";
import productReducer from "./slices/productsSlice";
import cartReducer from "./slices/cartSlice";
import checkoutReducer from "./slices/checkoutSlice";
import orderReducer from "./slices/orderSlice";
import adminReducer from "./slices/adminSlice";
import adminProductReducer from "./slices/adminProductSlice";
import adminOrdersReducer from "./slices/adminOrderSlice";

const store = configureStore({
  reducer: {
    auth: authReducer,
    products: productReducer,
    cart: cartReducer,
    checkout: checkoutReducer,
    orders: orderReducer,
    admin: adminReducer,
    adminProducts: adminProductReducer,
    adminOrders: adminOrdersReducer,
  },
});

export default store;</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Build &#038; Deploy Full Stack E-commerce Website &#124; Redux &#124; MERN Stack – 02</title>
		<link>/wordpress_blog/full-stack-rabbit-02/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Tue, 18 Mar 2025 09:12:43 +0000</pubDate>
				<category><![CDATA[Youtube]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=882</guid>

					<description><![CDATA[學習來自 YT:&#160;compiletab影片:&#038;nbsp [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>學習來自 YT:&nbsp;<a rel="noreferrer noopener" href="https://www.youtube.com/@compiletab" target="_blank">compiletab</a><br>影片:&nbsp;<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=hpgh2BTtac8" target="_blank">Build &amp; Deploy Full Stack E-commerce Website | Redux | MERN Stack Project</a><br>Github assets:&nbsp;<a rel="noreferrer noopener" href="https://github.com/kushald/rabbit-assets" target="_blank">連結</a><br>Thank you, teacher</p>



<h1 class="wp-block-heading">建立 &amp; 部署全端商業網站</h1>



<h2 class="wp-block-heading">影片時間戳</h2>



<p>00:00:00 – Introduction<br>00:01:56 – Demo<br>00:05:26 – Installation &amp; set up<br>05:50:25 – Admin UI<br>07:20:25 – Backend setup<br>07:32:40 – user routes<br>08:05:40 – Products routes<br>09:18:56 – cart routes<br>10:13:26 – checkout routes<br>10:57:55 – Admin routes<br>11:58:30 – Redux</p>



<h2 class="wp-block-heading">Backend (後端)</h2>



<h2 class="wp-block-heading">npm init -y 初始化建立 package.json 檔案</h2>



<h2 class="wp-block-heading">安裝套件</h2>



<ul class="wp-block-list">
<li>express</li>



<li>mongoose</li>



<li>dotenv</li>



<li>jsonwebtoken</li>



<li>bcryptjs</li>



<li>cors</li>



<li>nodemon</li>
</ul>



<pre class="wp-block-code"><code>// backend - 後端
// TERMINAL - 終端機
npm install express mongoose dotenv jsonwebtoken bcryptjs cors nodemon</code></pre>



<h2 class="wp-block-heading">後端環境建立</h2>



<pre class="wp-block-code"><code>// backend/server.js
// 建立伺服器的進入點
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<pre class="wp-block-code"><code>// backend/package.json
{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node backend/server.js",
    "dev": "nodemon backend/server.js"
  },
  "keywords": &#91;],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^3.0.2",
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "express": "^4.21.2",
    "jsonwebtoken": "^9.0.2",
    "mongoose": "^8.12.1",
    "nodemon": "^3.1.9"
  }
}
</code></pre>



<pre class="wp-block-code"><code>// backend/.env
PORT=9000</code></pre>



<h2 class="wp-block-heading">使用 MongoDB Altas 建立資料庫</h2>



<ol class="wp-block-list">
<li>New Project – 新建專案</li>



<li>Name Your Project: rabbit – 命名你的專案: rabbit</li>



<li>Create Project – 建立專案</li>



<li>Create a cluster – 建立集群</li>



<li>Deploy your cluster – 部署你的集群<br>Free、AWS – 免費、亞馬遜雲端服務<br>Create Deployment – 建立部署</li>



<li>Connect to Cluster0 – 連接到集群<br>Copy – 複製<br>Create Database User – 建立資料庫使用者</li>



<li>Add a connection IP Address – 增加連接的IP地址<br>Add IP ADDRESS – 增加IP地址<br>ALLOW ACCESS FROM ANYWHERE – 允許來自任何地方的訪問<br>(我這裡先設定一星期)<br>CONFIRM – 確認</li>



<li>Choose a connection method – 選擇連接方法<br>Connect to your application – 連接你的應用程式<br>Using this connection string in your application – 使用連接字串在你的應用程式</li>
</ol>



<pre class="wp-block-code"><code>// backend/.env
PORT=9000
MONGO_URI=mongodb+srv://&lt;username&gt;:&lt;password&gt;@&lt;cluster-address&gt;/&lt;database-name&gt;?retryWrites=true&amp;w=majority&amp;appName=&lt;app-name&gt;
</code></pre>



<pre class="wp-block-code"><code>// backend/config/db.js
const mongoose = require("mongoose");

const connectDB = async () =&gt; {
  try {
    await mongoose.connect(process.env.MONGO_URI);
    console.log("MongoDB connected successfully");
  } catch (err) {
    console.error("MongoDB connection failed.", err);
    process.exit(1);
  }
};

module.exports = connectDB;
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

// Connect to MongoDB - 連接到 MongoDB
connectDB();

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<h2 class="wp-block-heading">製作處理用戶功能</h2>



<h3 class="wp-block-heading">USERS (用戶)</h3>



<ul class="wp-block-list">
<li>Register</li>



<li>Login</li>



<li>Profile</li>
</ul>



<h3 class="wp-block-heading">User Schema (用戶模式)</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Field Name (欄位名稱)</td><td>Type (類型)</td><td>Constraints (約束)</td></tr><tr><td>_id</td><td>ObjectId</td><td>Primary key, auto-generated by MongoDB</td></tr><tr><td>name</td><td>String</td><td>Required, trimmed of whitespace</td></tr><tr><td>email</td><td>String</td><td>Required, unique, trimmed, email validated</td></tr><tr><td>password</td><td>String</td><td>Required, minimum length: 6</td></tr><tr><td>role</td><td>String</td><td>Enum: [“customer”, “admin”], Default: “customer”</td></tr><tr><td>createdAt</td><td>Date</td><td>Auto-generated timestamp</td></tr><tr><td>updateAt</td><td>Date</td><td>Auto-updated timestamp</td></tr></tbody></table></figure>



<pre class="wp-block-code"><code>// backend/models/User.js
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");

const userSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: true,
      trim: true,
    },
    email: {
      type: String,
      required: true,
      unique: true,
      trim: true,
      match: &#91;/.+\@.+\..+/, "Please enter a valid email address"],
    },
    password: {
      type: String,
      required: true,
      minLength: 6,
    },
    role: {
      type: String,
      enum: &#91;"customer", "admin"],
      default: "customer",
    },
  },
  {
    timestamps: true,
  }
);

// Password Hash middleware - 密碼哈希中介軟體
userSchema.pre("save", async function (next) {
  if (!this.isModified("password")) return next();
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// Match User entered password to Hashed password - 將用戶輸入的密碼與哈希密碼匹配
userSchema.methods.matchPassword = async function (enteredPassword) {
  return await bcrypt.compare(enteredPassword, this.password);
};

module.exports = mongoose.model("User", userSchema);
</code></pre>



<pre class="wp-block-code"><code>// backend/routes/userRoutes.js
const express = require("express");
const User = require("../models/User");
const jwt = require("jsonwebtoken");
const { protect } = require("../middleware/authMiddleware");
const router = express.Router();

// @route POST /api/users/register - @路由、使用 POST 方法、API 的路徑
// @desc Register a new user - @描述 註冊一個新用戶
// @access Public - @訪問權限 公開
router.post("/register", async (req, res) =&gt; {
  const { name, email, password } = req.body;

  try {
    // Registration logic - 註冊邏輯
    // 測試1
    // res.send({ name, email, password });

    let user = await User.findOne({ email });
    if (user) return res.status(400).json({ message: "User already exists" });
    user = new User({ name, email, password });
    await user.save();

    // 測試2
    // res.status(201).json({
    //   user: {
    //     _id: user._id,
    //     name: user.name,
    //     email: user.email,
    //     role: user.role,
    //   },
    // });

    // Create JWT Payload - 建立 JWT 負載
    const payload = { user: { id: user._id, role: user.role } };
    // Sign and return the token along with user data - 簽署並返回令牌以及用戶資料
    jwt.sign(
      payload,
      process.env.JWT_SECRET,
      {
        expiresIn: "40h",
      },
      (err, token) =&gt; {
        if (err) throw err;
        // Send the user and token in response
        res.status(201).json({
          user: {
            _id: user._id,
            name: user.name,
            email: user.email,
            role: user.role,
          },
          token,
        });
      }
    );
  } catch (error) {
    console.log(error);
    res.status(500).send("Server Error");
  }
});

// @route POST /api/user/login - @路由、使用 POST 方法、API 的路徑
// @desc Authenticate user - @描述 - 驗證用戶
// @access Public - @訪問權限 公開
router.post("/login", async (req, res) =&gt; {
  const { email, password } = req.body;

  try {
    // Find ther user by email
    let user = await User.findOne({
      email,
    });

    if (!user)
      return res.status(400).json({
        message: "Invalid Credentials",
      });
    const isMatch = await user.matchPassword(password);

    if (!isMatch)
      return res.status(400).json({ message: "Invalid Credentials" });

    // Create JWT Payload - 建立 JWT 負載
    const payload = { user: { id: user._id, role: user.role } };

    // Sign and return the token along with user data - 簽署並返回令牌以及用戶資料
    jwt.sign(
      payload,
      process.env.JWT_SECRET,
      {
        expiresIn: "40h",
      },
      (err, token) =&gt; {
        if (err) throw err;

        // Send the user and token in response
        res.json({
          user: {
            _id: user._id,
            name: user.name,
            email: user.email,
            role: user.role,
          },
          token,
        });
      }
    );
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/users/profile - @路由、使用 POST 方法、API 的路徑
// @desc Get logged-in user's profile (Protected Route) - @描述 - 獲取已登入用戶的個人資料 (受保護的路由)
// @access Private - @訪問權限 私人
router.get("/profile", protect, async (req, res) =&gt; {
  res.json(req.user);
});

module.exports = router;
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

// Connect to MongoDB - 連接到 MongoDB
connectDB();

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

// API Routes - API 路由
app.use("/api/users", userRoutes);

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li><a href="https://www.postman.com/" target="_blank" rel="noreferrer noopener">Postman</a>&nbsp;– Postman Agent</li>



<li>Create Workspace – 建立工作區</li>



<li>Name: Rabbit – 名稱: Rabbit<br>Create – 建立</li>



<li>Create Collection – 建立集合<br>Users</li>



<li>Add a request – 新增請求<br>Register、POST、http://localhost:9000/api/users/register<br>Login、POST、http://localhost:9000/api/users/login<br>Profile、GET、http://localhost:9000/api/users/profile<br>Save – 儲存</li>



<li>Body &gt; raw</li>



<li>Send</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Users/Register
// Body &gt; raw
{
    "name": "John",
    "email": "john@example.com",
    "password": 123456
}</code></pre>



<pre class="wp-block-code"><code>// Postman - Users/Login
// Body &gt; raw
{
    "email": "john1@example.com",
    "password": 123456
}</code></pre>



<pre class="wp-block-code"><code>// Postman - Users/Profile
// Headers
Key: Authorization
Value: Bearer 你的Token</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas</h2>



<ul class="wp-block-list">
<li>Browse Collection (瀏覽集合) &gt; users</li>
</ul>



<pre class="wp-block-code"><code>// backend/middleware/authMiddleware.js
const jwt = require("jsonwebtoken");
const User = require("../models/User");

// Middleware to protect routes
const protect = async (req, res, next) =&gt; {
  let token;

  if (
    req.headers.authorization &amp;&amp;
    req.headers.authorization.startsWith("Bearer")
  ) {
    try {
      token = req.headers.authorization.split(" ")&#91;1];
      const decoded = jwt.verify(token, process.env.JWT_SECRET);

      req.user = await User.findById(decoded.user.id).select("-password"); // Exclude password - 排除密碼
      next();
    } catch (error) {
      console.error("Token verification failed:", error);
      res.status(401).json({ message: "Not authorized, token failed" });
    }
  } else {
    res.status(401).json({ message: "Not authorized, no token provided" });
  }
};

module.exports = { protect };
</code></pre>



<ul class="wp-block-list">
<li><a href="https://jwt.io/" target="_blank" rel="noreferrer noopener">JSON Web Tokens</a></li>
</ul>



<pre class="wp-block-code"><code>// backend/.env
PORT=9000
MONGO_URI=mongodb+srv://&lt;username&gt;:&lt;password&gt;@&lt;cluster-address&gt;/&lt;database-name&gt;?retryWrites=true&amp;w=majority&amp;appName=&lt;app-name&gt;
JWT_SECRET=你的JWT密鑰</code></pre>



<h2 class="wp-block-heading">後端製作流程 (個人整理)</h2>



<ol class="wp-block-list">
<li>初始化專案</li>



<li>創建 .env 文件</li>



<li>製作 server.js (伺服器入口)</li>



<li>製作資料庫配置 config/db.js</li>



<li>製作資料模型 models/User.js</li>



<li>製作路由 routes/userRoutes.js</li>



<li>製作中介軟體 middleware/authMiddleware.js</li>
</ol>



<h2 class="wp-block-heading">製作產品功能</h2>



<h3 class="wp-block-heading">PRODUCTS (產品)</h3>



<ul class="wp-block-list">
<li>Create Product</li>



<li>Update Product</li>



<li>Delete Product</li>



<li>All Products</li>



<li>Single Product</li>



<li>Best Seller</li>



<li>Similar Products</li>



<li>New Arrivals</li>
</ul>



<h2 class="wp-block-heading">Product Schema (產品模式)</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Field Name (欄位名稱)</td><td>Type (類型)</td><td>Constraints/Description (約束/描述)</td></tr><tr><td>_id</td><td>ObjectId</td><td>Auto-generated by MondoDB</td></tr><tr><td>name</td><td>String</td><td>Required, trimmed</td></tr><tr><td>description</td><td>String</td><td>Required</td></tr><tr><td>price</td><td>Number</td><td>Required</td></tr><tr><td>discountPrice</td><td>Number</td><td>Optional</td></tr><tr><td>countInStock</td><td>Number</td><td>Required, default: 0</td></tr><tr><td>sku</td><td>String</td><td>Required, unique</td></tr><tr><td>category</td><td>String</td><td>Required</td></tr><tr><td>brand</td><td>String</td><td>Optional</td></tr><tr><td>sizes</td><td>[String]</td><td>Required, example: [‘S’, ‘M’, ‘L’]</td></tr><tr><td>colors</td><td>[String]</td><td>Required, example: [‘Red’, ‘Blue’]</td></tr><tr><td>collections</td><td>String</td><td>Required, example: ‘Summercollection’</td></tr><tr><td>material</td><td>String</td><td>Optional</td></tr><tr><td>gender</td><td>String</td><td>Enum: [‘Men’, ‘Women’, ‘Unisex’]</td></tr><tr><td>images</td><td>Array of Objects</td><td>Required, each object contains:<br>– url: String, required<br>– altText: String, optional</td></tr><tr><td>isFeatured</td><td>Boolean</td><td>Default: false</td></tr><tr><td>isPublished</td><td>Boolean</td><td>Default: false</td></tr><tr><td>rating</td><td>Number</td><td>Default: 0</td></tr><tr><td>numReviews</td><td>Number</td><td>Default: 0</td></tr><tr><td>tags</td><td>[String]</td><td>Optional</td></tr><tr><td>user</td><td>ObjectId</td><td>Reference to User, required</td></tr><tr><td>metaTitle</td><td>String</td><td>Optional</td></tr><tr><td>metaDescription</td><td>String</td><td>Optional</td></tr><tr><td>metaKeywords</td><td>String</td><td>Optional</td></tr><tr><td>dimensions</td><td>Object</td><td>Optional, contains:<br>– length: Number<br>– width: Number<br>– height: Number</td></tr><tr><td>weight</td><td>Number</td><td>Optional</td></tr><tr><td>createdAt</td><td>Date</td><td>Auto-generated by timestamps</td></tr><tr><td>updatedAt</td><td>Date</td><td>Auto-updated by timestamps</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Create Product (建立產品)</h2>



<pre class="wp-block-code"><code>// backend/models/Product.js
const mongoose = require("mongoose");

const productSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: true,
      trim: true,
    },
    description: {
      type: String,
      required: true,
    },
    price: {
      type: Number,
      required: true,
    },
    discountPrice: {
      type: Number,
    },
    countInStock: {
      type: Number,
      required: true,
      default: 0,
    },
    sku: {
      type: String,
      unique: true,
      required: true,
    },
    category: {
      type: String,
      required: true,
    },
    brand: {
      type: String,
    },
    sizes: {
      type: &#91;String],
      required: true,
    },
    colors: {
      type: &#91;String],
      required: true,
    },
    collections: {
      type: String,
      required: true,
    },
    material: {
      type: String,
    },
    gender: {
      type: String,
      enum: &#91;"Men", "Women", "Unisex"],
    },
    images: &#91;
      {
        url: {
          type: String,
          required: true,
        },
        altText: {
          type: String,
        },
      },
    ],
    isFeatured: {
      type: Boolean,
      default: false,
    },
    isPublished: {
      type: Boolean,
      default: false,
    },
    rating: {
      type: Number,
      default: 0,
    },
    numReviews: {
      type: Number,
      default: 0,
    },
    tags: &#91;String],
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
      required: true,
    },
    metaTitle: {
      type: String,
    },
    metaDescription: {
      type: String,
    },
    metaKeywords: {
      type: String,
    },
    dimensions: {
      length: Number,
      width: Number,
      height: Number,
    },
    weight: Number,
  },
  { timestamps: true }
);

module.exports = mongoose.model("Product", productSchema);
</code></pre>



<pre class="wp-block-code"><code>// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 - 建立新的產品
// @access Private/Admin - @訪問權限 - 私人/管理員
router.post("/", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    const product = new Product({
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
      user: req.user._id, // Reference to the admin user who created it
    });

    const createdProduct = await product.save();
    res.status(201).json(createdProduct);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Create new collection – 建立新的集合<br>Products</li>



<li>Add a request – 新增請求<br>Create、POST、http://localhost:9000/api/products</li>



<li>Body &gt; raw</li>



<li>Headers &gt; Key、Value</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Products/Create
// Body &gt; raw
{
  "name": "Classic Denim Jacket",
  "description": "A timeless denim jacket perfect for any season. Comfortable fit and durable material",
  "price": 59.99,
  "discountPrice": 49.99,
  "countInStock": 200,
  "category": "Apparel",
  "brand": "UrbanWear",
  "sizes": &#91;"XS", "S", "M", "L", "XL"],
  "colors": &#91;"Blue", "Black"],
  "collections": "Spring Collection",
  "material": "Denim",
  "gender": "Unisex",
  "images": &#91;
    {
      "url": "https://picsum.photos/seed/denim1/500/500",
      "altText": "Front view of the denim jacket"
    },
    {
      "url": "https://picsum.photos/seed/denim2/500/500",
      "altText": "Back view of the denim jacket"
    }
  ],
  "isFeatured": true,
  "isPublished": true,
  "tags": &#91;"denim", "jacket", "casual", "spring"],
  "dimensions": {
    "length": 12,
    "width": 8,
    "height": 1
  },
  "weight": 1.5,
  "sku": "CLTH12345"
}
</code></pre>



<pre class="wp-block-code"><code>// Postman - Products/Create
// Headers
Key: Authorization
Value: Bearer 你的Token</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

// Connect to MongoDB - 連接到 MongoDB
connectDB();

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas</h2>



<ul class="wp-block-list">
<li>Browse collections (瀏覽集合) &gt; products</li>
</ul>



<pre class="wp-block-code"><code>// backend/middleware/authMiddleware.js
const jwt = require("jsonwebtoken");
const User = require("../models/User");

// Middleware to protect routes
const protect = async (req, res, next) =&gt; {
  let token;

  if (
    req.headers.authorization &amp;&amp;
    req.headers.authorization.startsWith("Bearer")
  ) {
    try {
      token = req.headers.authorization.split(" ")&#91;1];
      const decoded = jwt.verify(token, process.env.JWT_SECRET);

      req.user = await User.findById(decoded.user.id).select("-password"); // Exclude password - 排除密碼
      next();
    } catch (error) {
      console.error("Token verification failed:", error);
      res.status(401).json({ message: "Not authorized, token failed" });
    }
  } else {
    res.status(401).json({ message: "Not authorized, no token provided" });
  }
};

// Middleware to check if the user is an admin - 中介軟體檢查用戶是否為管理員
const admin = (req, res, next) =&gt; {
  if (req.user &amp;&amp; req.user.role === "admin") {
    next();
  } else {
    res.status(403).json({ message: "Not authorized as an admin" });
  }
};

module.exports = { protect, admin };
</code></pre>



<h2 class="wp-block-heading">手動調整 MongoDB 資料庫</h2>



<ul class="wp-block-list">
<li>customer 手動調整為 admin</li>
</ul>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Postman – Products/Create 測試管理員功能<br>name、sku 都需要做調整</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Products/Create
// Body &gt; raw
{
    "name": "Classic Denim Jeans",
    "description": "A timeless denim jacket perfect for any season. Comfortable fit and durable material",
    "price": 59.99,
    "discountPrice": 49.99,
    "countInStock": 200,
    "sku": "CLTH123456",
    "category": "Apparel",
    "brand": "UrbanWear",
    "sizes": &#91;
        "XS",
        "S",
        "M",
        "L",
        "XL"
    ],
    "colors": &#91;
        "Blue",
        "Black"
    ],
    "collections": "Spring Collection",
    "material": "Denim",
    "gender": "Unisex",
    "images": &#91;
        {
            "url": "https://picsum.photos/seed/denim1/500/500",
            "altText": "Front view of the denim jacket",
            "_id": "67d0f15ef9aec7935e64f4be"
        },
        {
            "url": "https://picsum.photos/seed/denim2/500/500",
            "altText": "Back view of the denim jacket",
            "_id": "67d0f15ef9aec7935e64f4bf"
        }
    ],
    "isFeatured": true,
    "isPublished": true,
    "rating": 0,
    "numReviews": 0,
    "tags": &#91;
        "denim",
        "jacket",
        "casual",
        "spring"
    ],
    "user": "67ced561a2b8dd3a07c40918",
    "dimensions": {
        "length": 12,
        "width": 8,
        "height": 1
    },
    "weight": 1.5,
    "_id": "67d0f15ef9aec7935e64f4bd",
    "createdAt": "2025-03-12T02:28:46.647Z",
    "updatedAt": "2025-03-12T02:28:46.647Z",
    "__v": 0
}
</code></pre>



<h2 class="wp-block-heading">Update Product (更新產品)</h2>



<pre class="wp-block-code"><code>// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    const product = new Product({
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
      user: req.user._id, // Reference to the admin user who created it
    });

    const createdProduct = await product.save();
    res.status(201).json(createdProduct);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    // Find product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Update product fields - 更新產品欄位
      product.name = name || product.name;
      product.description = description || product.description;
      product.price = price || product.price;
      product.discountPrice = discountPrice || product.discountPrice;
      product.countInStock = countInStock || product.countInStock;
      product.category = category || product.category;
      product.brand = brand || product.brand;
      product.sizes = sizes || product.sizes;
      product.colors = colors || product.colors;
      product.collections = collections || product.collections;
      product.material = material || product.material;
      product.gender = gender || product.gender;
      product.images = images || product.images;
      product.isFeatured =
        isFeatured !== undefined ? isFeatured : product.isFeatured;
      product.isPublished =
        isPublished !== undefined ? isPublished : product.isPublished;
      product.tags = tags || product.tags;
      product.dimensions = dimensions || product.dimensions;
      product.weight = weight || product.weight;
      product.sku = sku || product.sku;

      // Save the updated product - 儲存更新的產品
      const updatedProduct = await product.save();
      res.json(updatedProduct);
    } else {
      res.status(404).json({
        message: "Product not found",
      });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Collections: Products – 集合: 產品</li>



<li>Add request – 新增請求<br>Update、PUT、http://localhost:9000/api/products/67d0f15ef9aec7935e64f4bd</li>



<li>Headers &gt; Key、Value</li>



<li>Body &gt; raw</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Products/Update
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<pre class="wp-block-code"><code>// Postman - Products/Update
// Body &gt; raw
{
    "name": "Winter Jacket"
}
</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas 資料庫</h2>



<ul class="wp-block-list">
<li>查看是否有更新產品名稱</li>



<li>測試更新 name、description、sizes 欄位的值</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Products/Update
// Body &gt; raw
{
    "name": "Winter Jacket long Sleeves",
    "description": "Long Sleeves Jackets",
    "sizes": &#91;"S", "M"]
}
</code></pre>



<h2 class="wp-block-heading">Delete Product (刪除產品)</h2>



<pre class="wp-block-code"><code>// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    const product = new Product({
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
      user: req.user._id, // Reference to the admin user who created it
    });

    const createdProduct = await product.save();
    res.status(201).json(createdProduct);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    // Find product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Update product fields - 更新產品欄位
      product.name = name || product.name;
      product.description = description || product.description;
      product.price = price || product.price;
      product.discountPrice = discountPrice || product.discountPrice;
      product.countInStock = countInStock || product.countInStock;
      product.category = category || product.category;
      product.brand = brand || product.brand;
      product.sizes = sizes || product.sizes;
      product.colors = colors || product.colors;
      product.collections = collections || product.collections;
      product.material = material || product.material;
      product.gender = gender || product.gender;
      product.images = images || product.images;
      product.isFeatured =
        isFeatured !== undefined ? isFeatured : product.isFeatured;
      product.isPublished =
        isPublished !== undefined ? isPublished : product.isPublished;
      product.tags = tags || product.tags;
      product.dimensions = dimensions || product.dimensions;
      product.weight = weight || product.weight;
      product.sku = sku || product.sku;

      // Save the updated product - 儲存更新的產品
      const updatedProduct = await product.save();
      res.json(updatedProduct);
    } else {
      res.status(404).json({
        message: "Product not found",
      });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) =&gt; {
  try {
    // Find the product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Remove the product from DB - 從資料庫移除產品
      await product.deleteOne();
      res.json({ message: "Product remove" });
    } else {
      res.status(404).json({ message: "Product not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Collections: Products – 集合: 產品</li>



<li>Add request – 新增請求<br>Delete、DELETE、http://localhost:9000/api/products/67d0f15ef9aec7935e64f4bd</li>



<li>Headers &gt; Key、Value</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Products/Delete
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas 資料庫</h2>



<ul class="wp-block-list">
<li>查看是否有刪除產品</li>
</ul>



<h2 class="wp-block-heading">下載使用產品資料</h2>



<ul class="wp-block-list">
<li><a href="https://github.com/kushald/rabbit-assets/blob/main/data/products.js" target="_blank" rel="noreferrer noopener">data/product.js</a></li>
</ul>



<pre class="wp-block-code"><code>// backend/data/products.js
// product.js:

const products = &#91;
  {
    name: "Classic Oxford Button-Down Shirt",
    description:
      "This classic Oxford shirt is tailored for a polished yet casual look. Crafted from high-quality cotton, it features a button-down collar and a comfortable, slightly relaxed fit. Perfect for both formal and casual occasions, it comes with long sleeves, a button placket, and a yoke at the back. The shirt is finished with a gently rounded hem and adjustable button cuffs.",
    price: 39.99,
    discountPrice: 34.99,
    countInStock: 20,
    sku: "OX-SH-001",
    category: "Top Wear",
    brand: "Urban Threads",
    sizes: &#91;"S", "M", "L", "XL", "XXL"],
    colors: &#91;"Red", "Blue", "Yellow"],
    collections: "Business Casual",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=39",
        altText: "Classic Oxford Button-Down Shirt Front View",
      },
      {
        url: "https://picsum.photos/500/500?random=40",
        altText: "Classic Oxford Button-Down Shirt Back View",
      },
    ],
    rating: 4.5,
    numReviews: 12,
  },
  {
    name: "Slim-Fit Stretch Shirt",
    description:
      "A versatile slim-fit shirt perfect for business or evening events. Designed with a fitted silhouette, the added stretch provides maximum comfort throughout the day. Features a crisp turn-down collar, button placket, and adjustable cuffs.",
    price: 29.99,
    discountPrice: 24.99,
    countInStock: 35,
    sku: "SLIM-SH-002",
    category: "Top Wear",
    brand: "Modern Fit",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "Navy Blue", "Burgundy"],
    collections: "Formal Wear",
    material: "Cotton Blend",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=41",
        altText: "Slim-Fit Stretch Shirt Front View",
      },
      {
        url: "https://picsum.photos/500/500?random=42",
        altText: "Slim-Fit Stretch Shirt Back View",
      },
    ],
    rating: 4.8,
    numReviews: 15,
  },
  {
    name: "Casual Denim Shirt",
    description:
      "This casual denim shirt is made from lightweight cotton denim. It features a regular fit, snap buttons, and a straight hem. With Western-inspired details, this shirt is perfect for layering or wearing solo.",
    price: 49.99,
    discountPrice: 44.99,
    countInStock: 15,
    sku: "CAS-DEN-003",
    category: "Top Wear",
    brand: "Street Style",
    sizes: &#91;"S", "M", "L", "XL", "XXL"],
    colors: &#91;"Light Blue", "Dark Wash"],
    collections: "Casual Wear",
    material: "Denim",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=43",
        altText: "Casual Denim Shirt Front View",
      },
      {
        url: "https://picsum.photos/500/500?random=44",
        altText: "Casual Denim Shirt Back View",
      },
    ],
    rating: 4.6,
    numReviews: 8,
  },
  {
    name: "Printed Resort Shirt",
    description:
      "Designed for summer, this printed resort shirt is perfect for vacation or weekend getaways. It features a relaxed fit, short sleeves, and a camp collar. The all-over tropical print adds a playful vibe.",
    price: 29.99,
    discountPrice: 22.99,
    countInStock: 25,
    sku: "PRNT-RES-004",
    category: "Top Wear",
    brand: "Beach Breeze",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Tropical Print", "Navy Palms"],
    collections: "Vacation Wear",
    material: "Viscose",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=45",
        altText: "Printed Resort Shirt Front View",
      },
      {
        url: "https://picsum.photos/500/500?random=1",
        altText: "Printed Resort Shirt Back View",
      },
    ],
    rating: 4.4,
    numReviews: 10,
  },
  {
    name: "Slim-Fit Easy-Iron Shirt",
    description:
      "A slim-fit, easy-iron shirt in woven cotton fabric with a fitted silhouette. Features a turn-down collar, classic button placket, and a yoke at the back. Long sleeves and adjustable button cuffs with a rounded hem.",
    price: 34.99,
    discountPrice: 29.99,
    countInStock: 30,
    sku: "SLIM-EIR-005",
    category: "Top Wear",
    brand: "Urban Chic",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Gray"],
    collections: "Business Wear",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=47",
        altText: "Slim-Fit Easy-Iron Shirt Front View",
      },
      {
        url: "https://picsum.photos/500/500?random=2",
        altText: "Slim-Fit Easy-Iron Shirt Front View",
      },
    ],
    rating: 5,
    numReviews: 14,
  },
  {
    name: "Polo T-Shirt with Ribbed Collar",
    description:
      "A wardrobe classic, this polo t-shirt features a ribbed collar and cuffs. Made from 100% cotton, it offers breathability and comfort throughout the day. Tailored in a slim fit with a button placket at the neckline.",
    price: 24.99,
    discountPrice: 19.99,
    countInStock: 50,
    sku: "POLO-TSH-006",
    category: "Top Wear",
    brand: "Polo Classics",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Navy", "Red"],
    collections: "Casual Wear",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=3",
        altText: "Polo T-Shirt Front View",
      },
      {
        url: "https://picsum.photos/500/500?random=4",
        altText: "Polo T-Shirt Back View",
      },
    ],
    rating: 4.3,
    numReviews: 22,
  },
  {
    name: "Oversized Graphic T-Shirt",
    description:
      "An oversized graphic t-shirt that combines comfort with street style. Featuring bold prints across the chest, this relaxed fit tee offers a modern vibe, perfect for pairing with jeans or joggers.",
    price: 19.99,
    discountPrice: 15.99,
    countInStock: 40,
    sku: "OVS-GRF-007",
    category: "Top Wear",
    brand: "Street Vibes",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "Gray"],
    collections: "Streetwear",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=5",
        altText: "Oversized Graphic T-Shirt Front View",
      },
    ],
    rating: 4.6,
    numReviews: 30,
  },
  {
    name: "Regular-Fit Henley Shirt",
    description:
      "A modern take on the classic Henley shirt, this regular-fit style features a buttoned placket and ribbed cuffs. Made from a soft cotton blend with a touch of elastane for stretch.",
    price: 22.99,
    discountPrice: 18.99,
    countInStock: 35,
    sku: "REG-HEN-008",
    category: "Top Wear",
    brand: "Heritage Wear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Heather Gray", "Olive", "Black"],
    collections: "Casual Wear",
    material: "Cotton Blend",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=6",
        altText: "Regular-Fit Henley Shirt Front View",
      },
    ],
    rating: 4.5,
    numReviews: 25,
  },
  {
    name: "Long-Sleeve Thermal Tee",
    description:
      "Stay warm with this long-sleeve thermal tee, made from soft cotton with a waffle-knit texture. Ideal for layering in cooler months, the slim-fit design ensures a snug yet comfortable fit.",
    price: 27.99,
    discountPrice: 22.99,
    countInStock: 20,
    sku: "LST-THR-009",
    category: "Top Wear",
    brand: "Winter Basics",
    sizes: &#91;"S", "M", "L", "XL", "XXL"],
    colors: &#91;"Charcoal", "Dark Green", "Navy"],
    collections: "Winter Essentials",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=7",
        altText: "Long-Sleeve Thermal Tee Front View",
      },
    ],
    rating: 4.4,
    numReviews: 18,
  },
  {
    name: "V-Neck Classic T-Shirt",
    description:
      "A classic V-neck t-shirt for everyday wear. This regular-fit tee is made from breathable cotton and features a clean, simple design with a flattering V-neckline. Lightweight fabric and soft texture make it perfect for casual looks.",
    price: 14.99,
    discountPrice: 11.99,
    countInStock: 60,
    sku: "VNECK-CLS-010",
    category: "Top Wear",
    brand: "Everyday Comfort",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Black", "Navy"],
    collections: "Basics",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=8",
        altText: "V-Neck Classic T-Shirt Front View",
      },
    ],
    rating: 4.7,
    numReviews: 28,
  },
  {
    name: "Slim Fit Joggers",
    description:
      "Slim-fit joggers with an elasticated drawstring waist. Features ribbed hems and side pockets. Ideal for casual outings or workouts.",
    price: 40,
    discountPrice: 35,
    countInStock: 20,
    sku: "BW-001",
    category: "Bottom Wear",
    brand: "ActiveWear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "Gray", "Navy"],
    collections: "Casual Collection",
    material: "Cotton Blend",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=9",
        altText: "Slim Fit Joggers Front View",
      },
    ],
    rating: 4.5,
    numReviews: 12,
  },
  {
    name: "Cargo Joggers",
    description:
      "Relaxed-fit cargo joggers featuring multiple pockets for functionality. Drawstring waist and cuffed hems for a modern look.",
    price: 45,
    discountPrice: 40,
    countInStock: 15,
    sku: "BW-002",
    category: "Bottom Wear",
    brand: "UrbanStyle",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Olive", "Black"],
    collections: "Urban Collection",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=10",
        altText: "Cargo Joggers Front View",
      },
    ],
    rating: 4.7,
    numReviews: 20,
  },
  {
    name: "Tapered Sweatpants",
    description:
      "Tapered sweatpants designed for comfort. Elastic waistband with adjustable drawstring, perfect for lounging or athletic activities.",
    price: 35,
    discountPrice: 30,
    countInStock: 25,
    sku: "BW-003",
    category: "Bottom Wear",
    brand: "ChillZone",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Gray", "Charcoal", "Blue"],
    collections: "Lounge Collection",
    material: "Fleece",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=11",
        altText: "Tapered Sweatpants Front View",
      },
    ],
    rating: 4.3,
    numReviews: 18,
  },
  {
    name: "Denim Jeans",
    description:
      "Classic slim-fit denim jeans with a slight stretch for comfort. Features a zip fly and five-pocket styling for a timeless look.",
    price: 60,
    discountPrice: 50,
    countInStock: 30,
    sku: "BW-004",
    category: "Bottom Wear",
    brand: "DenimCo",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Dark Blue", "Light Blue"],
    collections: "Denim Collection",
    material: "Denim",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=12",
        altText: "Denim Jeans Front View",
      },
    ],
    rating: 4.6,
    numReviews: 22,
  },
  {
    name: "Chino Pants",
    description:
      "Slim-fit chino pants made from stretch cotton twill. Features a button closure and front and back pockets. Ideal for both casual and semi-formal wear.",
    price: 55,
    discountPrice: 48,
    countInStock: 40,
    sku: "BW-005",
    category: "Bottom Wear",
    brand: "CasualLook",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Beige", "Navy", "Black"],
    collections: "Smart Casual Collection",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=13",
        altText: "Chino Pants Front View",
      },
    ],
    rating: 4.8,
    numReviews: 15,
  },
  {
    name: "Track Pants",
    description:
      "Comfortable track pants with an elasticated waistband and tapered leg. Features side stripes for a sporty look. Ideal for athletic and casual wear.",
    price: 40,
    discountPrice: 35,
    countInStock: 20,
    sku: "BW-006",
    category: "Bottom Wear",
    brand: "SportX",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "Red", "Blue"],
    collections: "Activewear Collection",
    material: "Polyester",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=14",
        altText: "Track Pants Front View",
      },
    ],
    rating: 4.2,
    numReviews: 17,
  },
  {
    name: "Slim Fit Trousers",
    description:
      "Tailored slim-fit trousers with belt loops and a hook-and-eye closure. Suitable for formal occasions or smart-casual wear.",
    price: 65,
    discountPrice: 55,
    countInStock: 15,
    sku: "BW-007",
    category: "Bottom Wear",
    brand: "ExecutiveStyle",
    sizes: &#91;"M", "L", "XL"],
    colors: &#91;"Gray", "Black"],
    collections: "Office Wear",
    material: "Polyester",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=15",
        altText: "Slim Fit Trousers Front View",
      },
    ],
    rating: 4.7,
    numReviews: 10,
  },
  {
    name: "Cargo Pants",
    description:
      "Loose-fit cargo pants with multiple utility pockets. Features adjustable ankle cuffs and a drawstring waist for versatility and comfort.",
    price: 50,
    discountPrice: 45,
    countInStock: 25,
    sku: "BW-008",
    category: "Bottom Wear",
    brand: "StreetWear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Olive", "Brown", "Black"],
    collections: "Street Style Collection",
    material: "Cotton",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=16",
        altText: "Cargo Pants Front View",
      },
    ],
    rating: 4.5,
    numReviews: 13,
  },
  {
    name: "Relaxed Fit Sweatpants",
    description:
      "Relaxed-fit sweatpants made from soft fleece fabric. Features an elastic waist and adjustable drawstring for a custom fit.",
    price: 35,
    discountPrice: 30,
    countInStock: 35,
    sku: "BW-009",
    category: "Bottom Wear",
    brand: "LoungeWear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Gray", "Black", "Navy"],
    collections: "Lounge Collection",
    material: "Fleece",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=17",
        altText: "Relaxed Fit Sweatpants Front View",
      },
    ],
    rating: 4.3,
    numReviews: 14,
  },
  {
    name: "Formal Dress Pants",
    description:
      "Classic formal dress pants with a slim fit. Made from lightweight, wrinkle-resistant fabric for a polished look at the office or formal events.",
    price: 70,
    discountPrice: 60,
    countInStock: 20,
    sku: "BW-010",
    category: "Bottom Wear",
    brand: "ElegantStyle",
    sizes: &#91;"M", "L", "XL"],
    colors: &#91;"Black", "Navy"],
    collections: "Formal Collection",
    material: "Polyester",
    gender: "Men",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=18",
        altText: "Formal Dress Pants Front View",
      },
    ],
    rating: 4.9,
    numReviews: 8,
  },
  {
    name: "High-Waist Skinny Jeans",
    description:
      "High-waist skinny jeans in stretch denim with a button and zip fly. Features a flattering fit that hugs your curves and enhances your silhouette.",
    price: 50,
    discountPrice: 45,
    countInStock: 30,
    sku: "BW-W-001",
    category: "Bottom Wear",
    brand: "DenimStyle",
    sizes: &#91;"XS", "S", "M", "L", "XL"],
    colors: &#91;"Dark Blue", "Black", "Light Blue"],
    collections: "Denim Collection",
    material: "Denim",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=19",
        altText: "High-Waist Skinny Jeans",
      },
    ],
    rating: 4.8,
    numReviews: 20,
  },
  {
    name: "Wide-Leg Trousers",
    description:
      "Flowy, wide-leg trousers with a high waist and side pockets. Perfect for an elegant look that combines comfort and style.",
    price: 60,
    discountPrice: 55,
    countInStock: 25,
    sku: "BW-W-002",
    category: "Bottom Wear",
    brand: "ElegantWear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Beige", "Black", "White"],
    collections: "Formal Collection",
    material: "Polyester",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=20",
        altText: "Wide-Leg Trousers Front View",
      },
    ],
    rating: 4.7,
    numReviews: 15,
  },
  {
    name: "Stretch Leggings",
    description:
      "Soft, stretch leggings in a high-rise style. Perfect for lounging, working out, or casual wear, with a smooth fit that flatters your body.",
    price: 25,
    discountPrice: 20,
    countInStock: 40,
    sku: "BW-W-003",
    category: "Bottom Wear",
    brand: "ComfyFit",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "Gray", "Navy"],
    collections: "Activewear Collection",
    material: "Cotton Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=21",
        altText: "Stretch Leggings Front View",
      },
    ],
    rating: 4.5,
    numReviews: 30,
  },
  {
    name: "Pleated Midi Skirt",
    description:
      "Elegant pleated midi skirt with a high waistband and soft fabric that drapes beautifully. Ideal for both formal and casual occasions.",
    price: 55,
    discountPrice: 50,
    countInStock: 20,
    sku: "BW-W-004",
    category: "Bottom Wear",
    brand: "ChicStyle",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"Pink", "Navy", "Black"],
    collections: "Spring Collection",
    material: "Polyester",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=22",
        altText: "Pleated Midi Skirt Front View",
      },
    ],
    rating: 4.6,
    numReviews: 18,
  },
  {
    name: "Flared Palazzo Pants",
    description:
      "High-waist palazzo pants with a loose, flowing fit. Comfortable and stylish, making them perfect for casual outings or beach days.",
    price: 45,
    discountPrice: 40,
    countInStock: 35,
    sku: "BW-W-005",
    category: "Bottom Wear",
    brand: "BreezyVibes",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Beige", "Light Blue"],
    collections: "Summer Collection",
    material: "Linen Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=23",
        altText: "Flared Palazzo Pants Front View",
      },
    ],
    rating: 4.4,
    numReviews: 22,
  },
  {
    name: "High-Rise Joggers",
    description:
      "Comfortable high-rise joggers with an elastic waistband and drawstring for a perfect fit. Great for lounging or working out.",
    price: 40,
    discountPrice: 35,
    countInStock: 30,
    sku: "BW-W-006",
    category: "Bottom Wear",
    brand: "ActiveWear",
    sizes: &#91;"XS", "S", "M", "L"],
    colors: &#91;"Black", "Gray", "Pink"],
    collections: "Loungewear Collection",
    material: "Cotton Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=24",
        altText: "High-Rise Joggers Front View",
      },
    ],
    rating: 4.3,
    numReviews: 25,
  },
  {
    name: "Paperbag Waist Shorts",
    description:
      "Stylish paperbag waist shorts with a belted waist and wide legs. Perfect for summer outings and keeping cool in style.",
    price: 35,
    discountPrice: 30,
    countInStock: 20,
    sku: "BW-W-007",
    category: "Bottom Wear",
    brand: "SunnyStyle",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"White", "Khaki", "Blue"],
    collections: "Summer Collection",
    material: "Cotton",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=25",
        altText: "Paperbag Waist Shorts Front View",
      },
    ],
    rating: 4.5,
    numReviews: 19,
  },
  {
    name: "Stretch Denim Shorts",
    description:
      "Comfortable stretch denim shorts with a high-waisted fit and raw hem. Perfect for pairing with your favorite tops during warmer months.",
    price: 40,
    discountPrice: 35,
    countInStock: 25,
    sku: "BW-W-008",
    category: "Bottom Wear",
    brand: "DenimStyle",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Blue", "Black", "White"],
    collections: "Denim Collection",
    material: "Denim",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=26",
        altText: "Stretch Denim Shorts Front View",
      },
    ],
    rating: 4.7,
    numReviews: 15,
  },
  {
    name: "Culottes",
    description:
      "Wide-leg culottes with a flattering high waist and cropped length. The perfect blend of comfort and style for any casual occasion.",
    price: 50,
    discountPrice: 45,
    countInStock: 30,
    sku: "BW-W-009",
    category: "Bottom Wear",
    brand: "ChicStyle",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "White", "Olive"],
    collections: "Casual Collection",
    material: "Polyester",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=27",
        altText: "Culottes Front View",
      },
    ],
    rating: 4.6,
    numReviews: 23,
  },
  {
    name: "Classic Pleated Trousers",
    description:
      "Timeless pleated trousers with a tailored fit. A wardrobe essential for workwear or formal occasions.",
    price: 70,
    discountPrice: 65,
    countInStock: 25,
    sku: "BW-W-010",
    category: "Bottom Wear",
    brand: "ElegantWear",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Navy", "Black", "Gray"],
    collections: "Formal Collection",
    material: "Wool Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=28",
        altText: "Classic Pleated Trousers Front View",
      },
    ],
    rating: 4.8,
    numReviews: 20,
  },
  {
    name: "Knitted Cropped Top",
    description:
      "A stylish knitted cropped top with a flattering fitted silhouette. Perfect for pairing with high-waisted jeans or skirts for a casual look.",
    price: 40,
    discountPrice: 35,
    countInStock: 25,
    sku: "TW-W-001",
    category: "Top Wear",
    brand: "ChicKnit",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"Beige", "White"],
    collections: "Knits Collection",
    material: "Cotton Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=29",
        altText: "Knitted Cropped Top",
      },
    ],
    rating: 4.6,
    numReviews: 15,
  },
  {
    name: "Boho Floral Blouse",
    description:
      "Flowy boho blouse with floral patterns, featuring a relaxed fit and balloon sleeves. Ideal for casual summer days.",
    price: 50,
    discountPrice: 45,
    countInStock: 30,
    sku: "TW-W-002",
    category: "Top Wear",
    brand: "BohoVibes",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Pink"],
    collections: "Summer Collection",
    material: "Viscose",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=30",
        altText: "Boho Floral Blouse",
      },
    ],
    rating: 4.7,
    numReviews: 20,
  },
  {
    name: "Casual T-Shirt",
    description:
      "A soft, breathable casual t-shirt with a classic fit. Features a round neckline and short sleeves, perfect for everyday wear.",
    price: 25,
    discountPrice: 20,
    countInStock: 50,
    sku: "TW-W-003",
    category: "Top Wear",
    brand: "ComfyTees",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Black", "White", "Gray"],
    collections: "Essentials",
    material: "Cotton",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=31",
        altText: "Casual T-Shirt",
      },
    ],
    rating: 4.5,
    numReviews: 25,
  },
  {
    name: "Off-Shoulder Top",
    description:
      "An elegant off-shoulder top with ruffled sleeves and a flattering fit. Ideal for adding a touch of femininity to your outfit.",
    price: 45,
    discountPrice: 40,
    countInStock: 35,
    sku: "TW-W-004",
    category: "Top Wear",
    brand: "Elegance",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"Red", "White", "Blue"],
    collections: "Evening Collection",
    material: "Polyester",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=32",
        altText: "Off-Shoulder Top",
      },
    ],
    rating: 4.7,
    numReviews: 18,
  },
  {
    name: "Lace-Trimmed Cami Top",
    description:
      "A delicate cami top with lace trim and adjustable straps. The lightweight fabric makes it perfect for layering or wearing alone during warmer weather.",
    price: 35,
    discountPrice: 30,
    countInStock: 40,
    sku: "TW-W-005",
    category: "Top Wear",
    brand: "DelicateWear",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"Black", "White"],
    collections: "Lingerie-Inspired",
    material: "Silk Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=33",
        altText: "Lace-Trimmed Cami Top",
      },
    ],
    rating: 4.8,
    numReviews: 22,
  },
  {
    name: "Graphic Print Tee",
    description:
      "A trendy graphic print tee with a relaxed fit. Pair it with jeans or skirts for a cool and casual look.",
    price: 30,
    discountPrice: 25,
    countInStock: 45,
    sku: "TW-W-006",
    category: "Top Wear",
    brand: "StreetStyle",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Black"],
    collections: "Urban Collection",
    material: "Cotton",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=34",
        altText: "Graphic Print Tee",
      },
    ],
    rating: 4.6,
    numReviews: 30,
  },
  {
    name: "Ribbed Long-Sleeve Top",
    description:
      "A cozy ribbed long-sleeve top that offers comfort and style. Perfect for layering during cooler months.",
    price: 55,
    discountPrice: 50,
    countInStock: 30,
    sku: "TW-W-007",
    category: "Top Wear",
    brand: "ComfortFit",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"Gray", "Pink", "Brown"],
    collections: "Fall Collection",
    material: "Cotton Blend",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=35",
        altText: "Ribbed Long-Sleeve Top",
      },
    ],
    rating: 4.7,
    numReviews: 26,
  },
  {
    name: "Ruffle-Sleeve Blouse",
    description:
      "A lightweight ruffle-sleeve blouse with a flattering fit. Perfect for a feminine touch to any outfit.",
    price: 45,
    discountPrice: 40,
    countInStock: 20,
    sku: "TW-W-008",
    category: "Top Wear",
    brand: "FeminineWear",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"White", "Navy", "Lavender"],
    collections: "Summer Collection",
    material: "Viscose",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=36",
        altText: "Ruffle-Sleeve Blouse",
      },
    ],
    rating: 4.5,
    numReviews: 19,
  },
  {
    name: "Classic Button-Up Shirt",
    description:
      "A versatile button-up shirt that can be dressed up or down. Made from soft fabric with a tailored fit, it's perfect for both casual and formal occasions.",
    price: 60,
    discountPrice: 55,
    countInStock: 25,
    sku: "TW-W-009",
    category: "Top Wear",
    brand: "ClassicStyle",
    sizes: &#91;"S", "M", "L", "XL"],
    colors: &#91;"White", "Light Blue", "Black"],
    collections: "Office Collection",
    material: "Cotton",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=37",
        altText: "Classic Button-Up Shirt",
      },
    ],
    rating: 4.8,
    numReviews: 25,
  },
  {
    name: "V-Neck Wrap Top",
    description:
      "A chic v-neck wrap top with a tie waist. Its elegant style makes it perfect for both casual and semi-formal occasions.",
    price: 50,
    discountPrice: 45,
    countInStock: 30,
    sku: "TW-W-010",
    category: "Top Wear",
    brand: "ChicWrap",
    sizes: &#91;"S", "M", "L"],
    colors: &#91;"Red", "Black", "White"],
    collections: "Evening Collection",
    material: "Polyester",
    gender: "Women",
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=38",
        altText: "V-Neck Wrap Top",
      },
    ],
    rating: 4.7,
    numReviews: 22,
  },
];

module.exports = products;
</code></pre>



<pre class="wp-block-code"><code>// backend/seeder.js
const mongoose = require("mongoose");
const dotenv = require("dotenv");
const Product = require("./models/Product");
const User = require("./models/User");
const products = require("./data/products");

dotenv.config();

// Conntect to mongoDB - 連接到 MongoDB
mongoose.connect(process.env.MONGO_URI);

// Function to seed data - 數據初始化函數

const seedData = async () =&gt; {
  try {
    // Clear existing data - 清除現有數據
    await Product.deleteMany();
    await User.deleteMany();

    // Create a default admin User 建立默認管理員用戶
    const createdUser = await User.create({
      name: "Admin User",
      email: "admin@example.com",
      password: "123456",
      role: "admin",
    });

    // Assign the default user ID to each product - 將默認用戶 ID 分配給每個產品
    const userID = createdUser._id;

    const sampleProducts = products.map((product) =&gt; {
      return { ...product, user: userID };
    });

    // Insert the products into the database - 將產品插入資料庫
    await Product.insertMany(sampleProducts);

    console.log("Product data seeded successfully!");
    process.exit();
  } catch (error) {
    console.error("Error seeding the data:", error);
    process.exit(1);
  }
};

seedData();
</code></pre>



<pre class="wp-block-code"><code>// backend/package.json
{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node backend/server.js",
    "dev": "nodemon backend/server.js",
    "seed": "node seeder.js"
  },
  "keywords": &#91;],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^3.0.2",
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "express": "^4.21.2",
    "jsonwebtoken": "^9.0.2",
    "mongoose": "^8.12.1",
    "nodemon": "^3.1.9"
  }
}
</code></pre>



<h2 class="wp-block-heading">初始化資料庫資料</h2>



<ul class="wp-block-list">
<li>npm run seed</li>
</ul>



<h2 class="wp-block-heading">查看 MongoDB Atlas 資料庫</h2>



<ul class="wp-block-list">
<li>查看資料是否有初始化資料庫</li>
</ul>



<h2 class="wp-block-heading">All Products (全部產品)</h2>



<pre class="wp-block-code"><code>// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    const product = new Product({
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
      user: req.user._id, // Reference to the admin user who created it
    });

    const createdProduct = await product.save();
    res.status(201).json(createdProduct);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    // Find product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Update product fields - 更新產品欄位
      product.name = name || product.name;
      product.description = description || product.description;
      product.price = price || product.price;
      product.discountPrice = discountPrice || product.discountPrice;
      product.countInStock = countInStock || product.countInStock;
      product.category = category || product.category;
      product.brand = brand || product.brand;
      product.sizes = sizes || product.sizes;
      product.colors = colors || product.colors;
      product.collections = collections || product.collections;
      product.material = material || product.material;
      product.gender = gender || product.gender;
      product.images = images || product.images;
      product.isFeatured =
        isFeatured !== undefined ? isFeatured : product.isFeatured;
      product.isPublished =
        isPublished !== undefined ? isPublished : product.isPublished;
      product.tags = tags || product.tags;
      product.dimensions = dimensions || product.dimensions;
      product.weight = weight || product.weight;
      product.sku = sku || product.sku;

      // Save the updated product - 儲存更新的產品
      const updatedProduct = await product.save();
      res.json(updatedProduct);
    } else {
      res.status(404).json({
        message: "Product not found",
      });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) =&gt; {
  try {
    // Find the product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Remove the product from DB - 從資料庫移除產品
      await product.deleteOne();
      res.json({ message: "Product remove" });
    } else {
      res.status(404).json({ message: "Product not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products with optional query filters - @描述 獲取所有產品，並可選擇性地使用查詢過濾器
// @access Public - @訪問權限 公開
router.get("/", async (req, res) =&gt; {
  try {
    const {
      collection,
      size,
      color,
      gender,
      minPrice,
      maxPrice,
      sortBy,
      search,
      category,
      material,
      brand,
      limit,
    } = req.query;

    let query = {};

    // Filter logic - 過濾邏輯
    if (collection &amp;&amp; collection.toLocaleLowerCase() !== "all") {
      query.collections = collection;
    }

    if (category &amp;&amp; category.toLocaleLowerCase() !== "all") {
      query.category = category;
    }

    if (material) {
      query.material = { $in: material.split(",") };
    }

    if (brand) {
      query.brand = { $in: brand.split(",") };
    }

    if (size) {
      query.sizes = { $in: size.split(",") };
    }

    if (color) {
      query.colors = { $in: &#91;color] };
    }

    if (gender) {
      query.gender = gender;
    }

    if (minPrice || maxPrice) {
      query.price = {};
      if (minPrice) query.price.$gte = Number(minPrice);
      if (maxPrice) query.price.$lte = Number(maxPrice);
    }

    if (search) {
      query.$or = &#91;
        { name: { $regex: search, $options: "i" } },
        { description: { $regex: search, $options: "i" } },
      ];
    }

    // Sort Logic - 排序邏輯
    let sort = {};
    if (sortBy) {
      switch (sortBy) {
        case "priceAsc":
          sort = { price: 1 };
          break;
        case "priceDesc":
          sort = { price: -1 };
          break;
        case "popularity":
          sort = { rating: -1 };
          break;
        default:
          break;
      }
    }

    // Fetch products and apply sorting and limit - 獲取產品並應用排序和限制
    let products = await Product.find(query)
      .sort(sort)
      .limit(Number(limit) || 0);
    res.json(products);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Collections: Products – 集合: 產品</li>



<li>Add request – 新增請求<br>All Products、GET、http://localhost:9000/api/products/</li>



<li>測試分類篩選<br>http://localhost:9000/api/products/?category=Top Wear</li>



<li>測試分類、材質篩選<br>http://localhost:9000/api/products/?category=Top Wear&amp;material=Cotton</li>



<li>測試分類、材質、性別篩選<br>http://localhost:9000/api/products/?category=Top Wear&amp;material=Cotton&amp;gender=Women</li>



<li>測試分類、材質、性別、搜尋篩選<br>http://localhost:9000/api/products/?category=Top Wear&amp;material=Cotton&amp;gender=Women&amp;search=Casual</li>



<li>測試上升排序<br>http://localhost:9000/api/products/?sortBy=priceAsc</li>



<li>測試下降排序<br>http://localhost:9000/api/products/?sortBy=priceDesc</li>



<li>測試最大價格<br>http://localhost:9000/api/products/?maxPrice=30</li>



<li>測試最大價格、最小價格<br>http://localhost:9000/api/products/?maxPrice=50&amp;minPrice=30</li>
</ul>



<h2 class="wp-block-heading">Single Product (單一產品)</h2>



<pre class="wp-block-code"><code>// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    const product = new Product({
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
      user: req.user._id, // Reference to the admin user who created it
    });

    const createdProduct = await product.save();
    res.status(201).json(createdProduct);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    // Find product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Update product fields - 更新產品欄位
      product.name = name || product.name;
      product.description = description || product.description;
      product.price = price || product.price;
      product.discountPrice = discountPrice || product.discountPrice;
      product.countInStock = countInStock || product.countInStock;
      product.category = category || product.category;
      product.brand = brand || product.brand;
      product.sizes = sizes || product.sizes;
      product.colors = colors || product.colors;
      product.collections = collections || product.collections;
      product.material = material || product.material;
      product.gender = gender || product.gender;
      product.images = images || product.images;
      product.isFeatured =
        isFeatured !== undefined ? isFeatured : product.isFeatured;
      product.isPublished =
        isPublished !== undefined ? isPublished : product.isPublished;
      product.tags = tags || product.tags;
      product.dimensions = dimensions || product.dimensions;
      product.weight = weight || product.weight;
      product.sku = sku || product.sku;

      // Save the updated product - 儲存更新的產品
      const updatedProduct = await product.save();
      res.json(updatedProduct);
    } else {
      res.status(404).json({
        message: "Product not found",
      });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) =&gt; {
  try {
    // Find the product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Remove the product from DB - 從資料庫移除產品
      await product.deleteOne();
      res.json({ message: "Product remove" });
    } else {
      res.status(404).json({ message: "Product not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products with optional query filters - @描述 獲取所有產品，並可選擇性地使用查詢過濾器
// @access Public - @訪問權限 公開
router.get("/", async (req, res) =&gt; {
  try {
    const {
      collection,
      size,
      color,
      gender,
      minPrice,
      maxPrice,
      sortBy,
      search,
      category,
      material,
      brand,
      limit,
    } = req.query;

    let query = {};

    // Filter logic - 過濾邏輯
    if (collection &amp;&amp; collection.toLocaleLowerCase() !== "all") {
      query.collections = collection;
    }

    if (category &amp;&amp; category.toLocaleLowerCase() !== "all") {
      query.category = category;
    }

    if (material) {
      query.material = { $in: material.split(",") };
    }

    if (brand) {
      query.brand = { $in: brand.split(",") };
    }

    if (size) {
      query.sizes = { $in: size.split(",") };
    }

    if (color) {
      query.colors = { $in: &#91;color] };
    }

    if (gender) {
      query.gender = gender;
    }

    if (minPrice || maxPrice) {
      query.price = {};
      if (minPrice) query.price.$gte = Number(minPrice);
      if (maxPrice) query.price.$lte = Number(maxPrice);
    }

    if (search) {
      query.$or = &#91;
        { name: { $regex: search, $options: "i" } },
        { description: { $regex: search, $options: "i" } },
      ];
    }

    // Sort Logic - 排序邏輯
    let sort = {};
    if (sortBy) {
      switch (sortBy) {
        case "priceAsc":
          sort = { price: 1 };
          break;
        case "priceDesc":
          sort = { price: -1 };
          break;
        case "popularity":
          sort = { rating: -1 };
          break;
        default:
          break;
      }
    }

    // Fetch products and apply sorting and limit - 獲取產品並應用排序和限制
    let products = await Product.find(query)
      .sort(sort)
      .limit(Number(limit) || 0);
    res.json(products);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products/:id - @路由、使用 GET 方法、API 的路徑
// @desc Get a single product by ID - @描述 通過 ID 獲取單一產品
// @access Public - @訪問權限 公開
router.get("/:id", async (req, res) =&gt; {
  try {
    const product = await Product.findById(req.params.id);
    if (product) {
      res.json(product);
    } else {
      res.status(404).json({ message: "Product Not Found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Collections: Products – 集合: 產品</li>



<li>Add request – 新增請求<br>Product Details、GET、http://localhost:9000/api/products/67d13557ef893a6a945de533</li>
</ul>



<h2 class="wp-block-heading">Similar Products (相似產品)</h2>



<pre class="wp-block-code"><code>// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    const product = new Product({
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
      user: req.user._id, // Reference to the admin user who created it
    });

    const createdProduct = await product.save();
    res.status(201).json(createdProduct);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    // Find product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Update product fields - 更新產品欄位
      product.name = name || product.name;
      product.description = description || product.description;
      product.price = price || product.price;
      product.discountPrice = discountPrice || product.discountPrice;
      product.countInStock = countInStock || product.countInStock;
      product.category = category || product.category;
      product.brand = brand || product.brand;
      product.sizes = sizes || product.sizes;
      product.colors = colors || product.colors;
      product.collections = collections || product.collections;
      product.material = material || product.material;
      product.gender = gender || product.gender;
      product.images = images || product.images;
      product.isFeatured =
        isFeatured !== undefined ? isFeatured : product.isFeatured;
      product.isPublished =
        isPublished !== undefined ? isPublished : product.isPublished;
      product.tags = tags || product.tags;
      product.dimensions = dimensions || product.dimensions;
      product.weight = weight || product.weight;
      product.sku = sku || product.sku;

      // Save the updated product - 儲存更新的產品
      const updatedProduct = await product.save();
      res.json(updatedProduct);
    } else {
      res.status(404).json({
        message: "Product not found",
      });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) =&gt; {
  try {
    // Find the product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Remove the product from DB - 從資料庫移除產品
      await product.deleteOne();
      res.json({ message: "Product remove" });
    } else {
      res.status(404).json({ message: "Product not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products with optional query filters - @描述 獲取所有產品，並可選擇性地使用查詢過濾器
// @access Public - @訪問權限 公開
router.get("/", async (req, res) =&gt; {
  try {
    const {
      collection,
      size,
      color,
      gender,
      minPrice,
      maxPrice,
      sortBy,
      search,
      category,
      material,
      brand,
      limit,
    } = req.query;

    let query = {};

    // Filter logic - 過濾邏輯
    if (collection &amp;&amp; collection.toLocaleLowerCase() !== "all") {
      query.collections = collection;
    }

    if (category &amp;&amp; category.toLocaleLowerCase() !== "all") {
      query.category = category;
    }

    if (material) {
      query.material = { $in: material.split(",") };
    }

    if (brand) {
      query.brand = { $in: brand.split(",") };
    }

    if (size) {
      query.sizes = { $in: size.split(",") };
    }

    if (color) {
      query.colors = { $in: &#91;color] };
    }

    if (gender) {
      query.gender = gender;
    }

    if (minPrice || maxPrice) {
      query.price = {};
      if (minPrice) query.price.$gte = Number(minPrice);
      if (maxPrice) query.price.$lte = Number(maxPrice);
    }

    if (search) {
      query.$or = &#91;
        { name: { $regex: search, $options: "i" } },
        { description: { $regex: search, $options: "i" } },
      ];
    }

    // Sort Logic - 排序邏輯
    let sort = {};
    if (sortBy) {
      switch (sortBy) {
        case "priceAsc":
          sort = { price: 1 };
          break;
        case "priceDesc":
          sort = { price: -1 };
          break;
        case "popularity":
          sort = { rating: -1 };
          break;
        default:
          break;
      }
    }

    // Fetch products and apply sorting and limit - 獲取產品並應用排序和限制
    let products = await Product.find(query)
      .sort(sort)
      .limit(Number(limit) || 0);
    res.json(products);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products/:id - @路由、使用 GET 方法、API 的路徑
// @desc Get a single product by ID - @描述 通過 ID 獲取單一產品
// @access Public - @訪問權限 公開
router.get("/:id", async (req, res) =&gt; {
  try {
    const product = await Product.findById(req.params.id);
    if (product) {
      res.json(product);
    } else {
      res.status(404).json({ message: "Product Not Found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products/similar/:id - @路由、使用 GET 方法、API 的路徑
// @desc Retrieve similar products based on the current product's gender and category - @描述 根據當前的產品的性別和類別檢索相似產品
// @access Public - @訪問權限 公開
router.get("/similar/:id", async (req, res) =&gt; {
  const { id } = req.params;
  // console.log(id); // 測試

  try {
    const product = await Product.findById(id);

    if (!product) {
      return res.status(404).json({ message: "Product Not Found" });
    }

    const similarProducts = await Product.find({
      _id: { $ne: id }, // Exclude the current product ID - 排除當前產品 ID
      gender: product.gender,
      category: product.category,
    }).limit(4);

    res.json(similarProducts);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Collections: Products – 集合: 產品</li>



<li>Add request – 新增請求<br>Similar Products、GET、http://localhost:9000/api/products/similar/:id</li>



<li>Params &gt; Path Variables &gt; Key、Value</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Products/Similar Products
// Params &gt; Path Variables &gt; Key、Value
Key: id
Value: 67d13557ef893a6a945de533</code></pre>



<h2 class="wp-block-heading">Best Seller (暢銷產品)</h2>



<pre class="wp-block-code"><code>// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    const product = new Product({
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
      user: req.user._id, // Reference to the admin user who created it
    });

    const createdProduct = await product.save();
    res.status(201).json(createdProduct);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    // Find product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Update product fields - 更新產品欄位
      product.name = name || product.name;
      product.description = description || product.description;
      product.price = price || product.price;
      product.discountPrice = discountPrice || product.discountPrice;
      product.countInStock = countInStock || product.countInStock;
      product.category = category || product.category;
      product.brand = brand || product.brand;
      product.sizes = sizes || product.sizes;
      product.colors = colors || product.colors;
      product.collections = collections || product.collections;
      product.material = material || product.material;
      product.gender = gender || product.gender;
      product.images = images || product.images;
      product.isFeatured =
        isFeatured !== undefined ? isFeatured : product.isFeatured;
      product.isPublished =
        isPublished !== undefined ? isPublished : product.isPublished;
      product.tags = tags || product.tags;
      product.dimensions = dimensions || product.dimensions;
      product.weight = weight || product.weight;
      product.sku = sku || product.sku;

      // Save the updated product - 儲存更新的產品
      const updatedProduct = await product.save();
      res.json(updatedProduct);
    } else {
      res.status(404).json({
        message: "Product not found",
      });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) =&gt; {
  try {
    // Find the product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Remove the product from DB - 從資料庫移除產品
      await product.deleteOne();
      res.json({ message: "Product remove" });
    } else {
      res.status(404).json({ message: "Product not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products with optional query filters - @描述 獲取所有產品，並可選擇性地使用查詢過濾器
// @access Public - @訪問權限 公開
router.get("/", async (req, res) =&gt; {
  try {
    const {
      collection,
      size,
      color,
      gender,
      minPrice,
      maxPrice,
      sortBy,
      search,
      category,
      material,
      brand,
      limit,
    } = req.query;

    let query = {};

    // Filter logic - 過濾邏輯
    if (collection &amp;&amp; collection.toLocaleLowerCase() !== "all") {
      query.collections = collection;
    }

    if (category &amp;&amp; category.toLocaleLowerCase() !== "all") {
      query.category = category;
    }

    if (material) {
      query.material = { $in: material.split(",") };
    }

    if (brand) {
      query.brand = { $in: brand.split(",") };
    }

    if (size) {
      query.sizes = { $in: size.split(",") };
    }

    if (color) {
      query.colors = { $in: &#91;color] };
    }

    if (gender) {
      query.gender = gender;
    }

    if (minPrice || maxPrice) {
      query.price = {};
      if (minPrice) query.price.$gte = Number(minPrice);
      if (maxPrice) query.price.$lte = Number(maxPrice);
    }

    if (search) {
      query.$or = &#91;
        { name: { $regex: search, $options: "i" } },
        { description: { $regex: search, $options: "i" } },
      ];
    }

    // Sort Logic - 排序邏輯
    let sort = {};
    if (sortBy) {
      switch (sortBy) {
        case "priceAsc":
          sort = { price: 1 };
          break;
        case "priceDesc":
          sort = { price: -1 };
          break;
        case "popularity":
          sort = { rating: -1 };
          break;
        default:
          break;
      }
    }

    // Fetch products and apply sorting and limit - 獲取產品並應用排序和限制
    let products = await Product.find(query)
      .sort(sort)
      .limit(Number(limit) || 0);
    res.json(products);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products/best-seller - @路由、使用 GET 方法、API 的路徑
// @dec Retrieve the product with highest rating - 檢索評分最高的產品
// @access Public - @訪問權限 公開
// Best Seller 路由程式碼需要撰寫在 Single Prodcut 路由程式碼的上方
router.get("/best-seller", async (req, res) =&gt; {
  try {
    // res.send("this should work"); // 測試

    const bestSeller = await Product.findOne().sort({ rating: -1 });
    if (bestSeller) {
      res.json(bestSeller);
    } else {
      res.status(404).json({ message: "No best seller found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products/:id - @路由、使用 GET 方法、API 的路徑
// @desc Get a single product by ID - @描述 通過 ID 獲取單一產品
// @access Public - @訪問權限 公開
router.get("/:id", async (req, res) =&gt; {
  try {
    const product = await Product.findById(req.params.id);
    if (product) {
      res.json(product);
    } else {
      res.status(404).json({ message: "Product Not Found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products/similar/:id - @路由、使用 GET 方法、API 的路徑
// @desc Retrieve similar products based on the current product's gender and category - @描述 根據當前的產品的性別和類別檢索相似產品
// @access Public - @訪問權限 公開
router.get("/similar/:id", async (req, res) =&gt; {
  const { id } = req.params;
  // console.log(id); // 測試

  try {
    const product = await Product.findById(id);

    if (!product) {
      return res.status(404).json({ message: "Product Not Found" });
    }

    const similarProducts = await Product.find({
      _id: { $ne: id }, // Exclude the current product ID - 排除當前產品 ID
      gender: product.gender,
      category: product.category,
    }).limit(4);

    res.json(similarProducts);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

module.exports = router;
</code></pre>



<ul class="wp-block-list">
<li>Best Seller 路由程式碼需要撰寫在 Single Prodcut 路由程式碼的上方</li>
</ul>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Collections: Products – 集合: 產品</li>



<li>Add request – 新增請求<br>Best Seller、GET、http://localhost:9000/api/products/best-seller</li>
</ul>



<h2 class="wp-block-heading">New Arrivals (新到商品)</h2>



<pre class="wp-block-code"><code>// backend/routes/productRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route POST /api/products - @路由、使用 POST 方法、API 的路徑
// @desc Create a new Product - @描述 建立新的產品
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    const product = new Product({
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
      user: req.user._id, // Reference to the admin user who created it
    });

    const createdProduct = await product.save();
    res.status(201).json(createdProduct);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route PUT /api/products/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update an existing product ID - @描述 更新現有產品ID
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const {
      name,
      description,
      price,
      discountPrice,
      countInStock,
      category,
      brand,
      sizes,
      colors,
      collections,
      material,
      gender,
      images,
      isFeatured,
      isPublished,
      tags,
      dimensions,
      weight,
      sku,
    } = req.body;

    // Find product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Update product fields - 更新產品欄位
      product.name = name || product.name;
      product.description = description || product.description;
      product.price = price || product.price;
      product.discountPrice = discountPrice || product.discountPrice;
      product.countInStock = countInStock || product.countInStock;
      product.category = category || product.category;
      product.brand = brand || product.brand;
      product.sizes = sizes || product.sizes;
      product.colors = colors || product.colors;
      product.collections = collections || product.collections;
      product.material = material || product.material;
      product.gender = gender || product.gender;
      product.images = images || product.images;
      product.isFeatured =
        isFeatured !== undefined ? isFeatured : product.isFeatured;
      product.isPublished =
        isPublished !== undefined ? isPublished : product.isPublished;
      product.tags = tags || product.tags;
      product.dimensions = dimensions || product.dimensions;
      product.weight = weight || product.weight;
      product.sku = sku || product.sku;

      // Save the updated product - 儲存更新的產品
      const updatedProduct = await product.save();
      res.json(updatedProduct);
    } else {
      res.status(404).json({
        message: "Product not found",
      });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route DELETE /api/products/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a product by ID - @描述、根據 ID 刪除產品
// @access Private/Admin - @訪問權限、私人/管理員
router.delete("/:id", protect, admin, async (req, res) =&gt; {
  try {
    // Find the product by ID - 根據 ID 查找產品
    const product = await Product.findById(req.params.id);

    if (product) {
      // Remove the product from DB - 從資料庫移除產品
      await product.deleteOne();
      res.json({ message: "Product remove" });
    } else {
      res.status(404).json({ message: "Product not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products with optional query filters - @描述 獲取所有產品，並可選擇性地使用查詢過濾器
// @access Public - @訪問權限 公開
router.get("/", async (req, res) =&gt; {
  try {
    const {
      collection,
      size,
      color,
      gender,
      minPrice,
      maxPrice,
      sortBy,
      search,
      category,
      material,
      brand,
      limit,
    } = req.query;

    let query = {};

    // Filter logic - 過濾邏輯
    if (collection &amp;&amp; collection.toLocaleLowerCase() !== "all") {
      query.collections = collection;
    }

    if (category &amp;&amp; category.toLocaleLowerCase() !== "all") {
      query.category = category;
    }

    if (material) {
      query.material = { $in: material.split(",") };
    }

    if (brand) {
      query.brand = { $in: brand.split(",") };
    }

    if (size) {
      query.sizes = { $in: size.split(",") };
    }

    if (color) {
      query.colors = { $in: &#91;color] };
    }

    if (gender) {
      query.gender = gender;
    }

    if (minPrice || maxPrice) {
      query.price = {};
      if (minPrice) query.price.$gte = Number(minPrice);
      if (maxPrice) query.price.$lte = Number(maxPrice);
    }

    if (search) {
      query.$or = &#91;
        { name: { $regex: search, $options: "i" } },
        { description: { $regex: search, $options: "i" } },
      ];
    }

    // Sort Logic - 排序邏輯
    let sort = {};
    if (sortBy) {
      switch (sortBy) {
        case "priceAsc":
          sort = { price: 1 };
          break;
        case "priceDesc":
          sort = { price: -1 };
          break;
        case "popularity":
          sort = { rating: -1 };
          break;
        default:
          break;
      }
    }

    // Fetch products and apply sorting and limit - 獲取產品並應用排序和限制
    let products = await Product.find(query)
      .sort(sort)
      .limit(Number(limit) || 0);
    res.json(products);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products/best-seller - @路由、使用 GET 方法、API 的路徑
// @dec Retrieve the product with highest rating - 檢索評分最高的產品
// @access Public - @訪問權限 公開
// Best Seller 路由程式碼需要撰寫在 Single Prodcut 路由程式碼的上方
router.get("/best-seller", async (req, res) =&gt; {
  try {
    // res.send("this should work"); // 測試

    const bestSeller = await Product.findOne().sort({ rating: -1 });
    if (bestSeller) {
      res.json(bestSeller);
    } else {
      res.status(404).json({ message: "No best seller found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products/new-arrivals - @路由、使用 GET 方法、API 的路徑
// @desc Retrieve latest 8 products - Creation date - @描述 根據創建日期檢索最新的 8 款產品
// @access Public - @訪問權限 公開
// New Arrivals 路由程式碼需要撰寫在 Single Prodcut 路由程式碼的上方
router.get("/new-arrivals", async (req, res) =&gt; {
  try {
    // Fetch latest 8 products - 獲取最新的8款產品
    const newArrivals = await Product.find().sort({ createdAt: -1 }).limit(8);
    res.json(newArrivals);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products/:id - @路由、使用 GET 方法、API 的路徑
// @desc Get a single product by ID - @描述 通過 ID 獲取單一產品
// @access Public - @訪問權限 公開
router.get("/:id", async (req, res) =&gt; {
  try {
    const product = await Product.findById(req.params.id);
    if (product) {
      res.json(product);
    } else {
      res.status(404).json({ message: "Product Not Found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

// @route GET /api/products/similar/:id - @路由、使用 GET 方法、API 的路徑
// @desc Retrieve similar products based on the current product's gender and category - @描述 根據當前的產品的性別和類別檢索相似產品
// @access Public - @訪問權限 公開
router.get("/similar/:id", async (req, res) =&gt; {
  const { id } = req.params;
  // console.log(id); // 測試

  try {
    const product = await Product.findById(id);

    if (!product) {
      return res.status(404).json({ message: "Product Not Found" });
    }

    const similarProducts = await Product.find({
      _id: { $ne: id }, // Exclude the current product ID - 排除當前產品 ID
      gender: product.gender,
      category: product.category,
    }).limit(4);

    res.json(similarProducts);
  } catch (error) {
    console.error(error);
    res.status(500).send("Server Error");
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Collections: Products – 集合: 產品</li>



<li>Add request – 新增請求<br>New Arrivals、GET、http://localhost:9000/api/products/new-arrivals</li>
</ul>



<h2 class="wp-block-heading">製作購物車功能</h2>



<h3 class="wp-block-heading">Cart (購物車)</h3>



<ul class="wp-block-list">
<li>Create</li>



<li>Read</li>



<li>Update</li>



<li>Delete</li>



<li>Merge</li>
</ul>



<h2 class="wp-block-heading">說明購物車流程</h2>



<ul class="wp-block-list">
<li>Guest User → Creates Cart</li>



<li>Creates Cart → Logs In (Login Event) →MERGE→ Converts to User Cart</li>
</ul>



<h2 class="wp-block-heading">Cart Schema (購物車模式)</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Field (欄位)</td><td>Type (類型)</td><td>Reference (參考)</td><td>Required (必填)</td><td>Default (預設)</td><td>Description (描述)</td></tr><tr><td>user</td><td>ObjectId</td><td>User</td><td>No</td><td>–</td><td>Reference to the logged-in user owning the cart.</td></tr><tr><td>guestId</td><td>String</td><td>–</td><td>No</td><td>–</td><td>Unique identifier for a guest user’s cart.</td></tr><tr><td>products</td><td>Array of CartItemSchema</td><td>–</td><td>Yes</td><td>–</td><td>List of products in the cart.</td></tr><tr><td>totalPrice</td><td>Number</td><td>–</td><td>Yes</td><td>0</td><td>Total price of all items in the cart.</td></tr><tr><td>timestamps</td><td>Object</td><td>–</td><td>Auto-Managed</td><td>createdAt,<br>updatedAt</td><td>Automatically managed by Mongoose.</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">CartItem Schema (Nested in products) (購物車項目模式 (巢狀於產品中))</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Field (欄位)</td><td>Type (類型)</td><td>Reference (參考)</td><td>Required (必填)</td><td>Default (預設)</td><td>Description (描述)</td></tr><tr><td>productId</td><td>ObjectId</td><td>Product</td><td>Yes</td><td>–</td><td>Reference to the product added to the cart.</td></tr><tr><td>name</td><td>String</td><td>–</td><td>No</td><td>–</td><td>Name of the product.</td></tr><tr><td>image</td><td>String</td><td>–</td><td>No</td><td>–</td><td>URL of the product image.</td></tr><tr><td>price</td><td>String</td><td>–</td><td>No</td><td>–</td><td>Price of the product.</td></tr><tr><td>size</td><td>String</td><td>–</td><td>No</td><td>–</td><td>Size of the product (e.g., M, L).</td></tr><tr><td>color</td><td>String</td><td>–</td><td>No</td><td>–</td><td>Color of the product (e.g., Red, Blue).</td></tr><tr><td>quantity</td><td>Number</td><td>–</td><td>No</td><td>1</td><td>Quantity of the product in the cart.</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Create Cart (建立購物車)</h2>



<pre class="wp-block-code"><code>// backend/models/Cart.js
const mongoose = require("mongoose");

const cartItemSchema = new mongoose.Schema(
  {
    productId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Product",
      required: true,
    },
    name: String,
    image: String,
    price: String,
    size: String,
    color: String,
    quantity: {
      type: Number,
      default: 1,
    },
  },
  { _id: false }
);

const cartSchema = new mongoose.Schema(
  {
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
    },
    guestId: {
      type: String,
    },
    products: &#91;cartItemSchema],
    totalPrice: {
      type: Number,
      required: true,
      default: 0,
    },
  },
  { timestamps: true }
);

module.exports = mongoose.model("Cart", cartSchema);
</code></pre>



<pre class="wp-block-code"><code>// backend/seeder.js
const mongoose = require("mongoose");
const dotenv = require("dotenv");
const Product = require("./models/Product");
const User = require("./models/User");
const Cart = require("./models/Cart");

const products = require("./data/products");

dotenv.config();

// Conntect to mongoDB - 連接到 MongoDB
mongoose.connect(process.env.MONGO_URI);

// Function to seed data - 數據初始化函數

const seedData = async () =&gt; {
  try {
    // Clear existing data - 清除現有數據
    await Product.deleteMany();
    await User.deleteMany();
    await Cart.deleteMany();

    // Create a default admin User 建立默認管理員用戶
    const createdUser = await User.create({
      name: "Admin User",
      email: "admin@example.com",
      password: "123456",
      role: "admin",
    });

    // Assign the default user ID to each product - 將默認用戶 ID 分配給每個產品
    const userID = createdUser._id;

    const sampleProducts = products.map((product) =&gt; {
      return { ...product, user: userID };
    });

    // Insert the products into the database - 將產品插入資料庫
    await Product.insertMany(sampleProducts);

    console.log("Product data seeded successfully!");
    process.exit();
  } catch (error) {
    console.error("Error seeding the data:", error);
    process.exit(1);
  }
};

seedData();
</code></pre>



<pre class="wp-block-code"><code>// backend/routes/cartRoute.js
const express = require("express");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const { protect } = require("../middleware/authMiddleware");

const router = express.Router();

// Helper function to get a cart by user Id or guest ID - 根據用戶 ID 或訪客 ID 獲取購物車的輔助函數
const getCart = async (userId, guestId) =&gt; {
  if (userId) {
    return await Cart.findOne({ user: userId });
  } else if (guestId) {
    return await Cart.findOne({ guestId });
  }
  return null;
};

// @route POST /api/cart - @路由、使用 POST 方法、API 的路徑
// @desc Add a product to the cart for a guest or logged in user - @描述 將產品增加到訪客或已登入用戶的購物車中
// @access Public - @訪問權限 公開
router.post("/", async (req, res) =&gt; {
  const { productId, quantity, size, color, guestId, userId } = req.body;
  try {
    const product = await Product.findById(productId);
    if (!product) return res.status(404).json({ message: "Product not found" });

    // Determine if the user is logged in or guest - 判斷用戶是已登入還是訪客
    let cart = await getCart(userId, guestId);

    // If the cart exists,update it - 如果購物車存在，則更新它
    if (cart) {
      const productIndex = cart.products.findIndex(
        (p) =&gt;
          p.productId.toString() === productId &amp;&amp;
          p.size === size &amp;&amp;
          p.color === color
      );

      if (productIndex &gt; -1) {
        // If the product already exists, update the quantity
        cart.products&#91;productIndex].quantity += quantity;
      } else {
        // add new product - 增加新產品
        cart.products.push({
          productId,
          name: product.name,
          image: product.images&#91;0].url,
          price: product.price,
          size,
          color,
          quantity,
        });
      }

      // Recalculate the total price - 重新計算總價格
      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      // Create a new cart for the guest or user - 為訪客或用戶創建一個新購物車
      const newCart = await Cart.create({
        user: userId ? userId : undefined,
        guestId: guestId ? guestId : "guest_" + new Date().getTime(),
        products: &#91;
          {
            productId,
            name: product.name,
            image: product.images&#91;0].url,
            price: product.price,
            size,
            color,
            quantity,
          },
        ],
        totalPrice: product.price * quantity,
      });
      return res.status(201).json(newCart);
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Create new collection – 建立新的集合<br>Cart</li>



<li>Add a request – 新增請求<br>Create、POST、http://localhost:9000/api/cart</li>



<li>Body &gt; raw</li>



<li>測試相同的 guestId</li>



<li>測試 userId</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Cart/Create
// Body &gt; raw
{
    "productId": "67d13557ef893a6a945de536",
    "size": "M",
    "color": "Red",
    "quantity": 1
}
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

// Connect to MongoDB - 連接到 MongoDB
connectDB();

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<pre class="wp-block-code"><code>// Postman - Cart/Create
// Body &gt; raw
// 測試相同的 guestId
{
    "guestId": "guest_1741855828784",
    "productId": "67d13557ef893a6a945de536",
    "size": "M",
    "color": "Red",
    "quantity": 2
}
</code></pre>



<pre class="wp-block-code"><code>// Postman - Cart/Create
// Body &gt; raw
// 測試 userId
{
    "userId": "67d13557ef893a6a945de525",
    "productId": "67d13557ef893a6a945de536",
    "size": "M",
    "color": "Red",
    "quantity": 2
}
</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas 資料庫</h2>



<ul class="wp-block-list">
<li>查看購物車數量是否有增加</li>



<li>查看 user 屬性是否有新增</li>
</ul>



<h2 class="wp-block-heading">Update Cart (更新購物車)</h2>



<pre class="wp-block-code"><code>// backend/routes/cartRoutes.js
const express = require("express");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const { protect } = require("../middleware/authMiddleware");

const router = express.Router();

// Helper function to get a cart by user Id or guest ID - 根據用戶 ID 或訪客 ID 獲取購物車的輔助函數
const getCart = async (userId, guestId) =&gt; {
  if (userId) {
    return await Cart.findOne({ user: userId });
  } else if (guestId) {
    return await Cart.findOne({ guestId });
  }
  return null;
};

// @route POST /api/cart - @路由、使用 POST 方法、API 的路徑
// @desc Add a product to the cart for a guest or logged in user - @描述 將產品增加到訪客或已登入用戶的購物車中
// @access Public - @訪問權限 公開
router.post("/", async (req, res) =&gt; {
  const { productId, quantity, size, color, guestId, userId } = req.body;
  try {
    const product = await Product.findById(productId);
    if (!product) return res.status(404).json({ message: "Product not found" });

    // Determine if the user is logged in or guest - 判斷用戶是已登入還是訪客
    let cart = await getCart(userId, guestId);

    // If the cart exists,update it - 如果購物車存在，則更新它
    if (cart) {
      const productIndex = cart.products.findIndex(
        (p) =&gt;
          p.productId.toString() === productId &amp;&amp;
          p.size === size &amp;&amp;
          p.color === color
      );

      if (productIndex &gt; -1) {
        // If the product already exists, update the quantity
        cart.products&#91;productIndex].quantity += quantity;
      } else {
        // add new product - 增加新產品
        cart.products.push({
          productId,
          name: product.name,
          image: product.images&#91;0].url,
          price: product.price,
          size,
          color,
          quantity,
        });
      }

      // Recalculate the total price - 重新計算總價格
      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      // Create a new cart for the guest or user - 為訪客或用戶創建一個新購物車
      const newCart = await Cart.create({
        user: userId ? userId : undefined,
        guestId: guestId ? guestId : "guest_" + new Date().getTime(),
        products: &#91;
          {
            productId,
            name: product.name,
            image: product.images&#91;0].url,
            price: product.price,
            size,
            color,
            quantity,
          },
        ],
        totalPrice: product.price * quantity,
      });
      return res.status(201).json(newCart);
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route PUT /api/cart - @路由、使用 PUT 方法、API 的路徑
// @desc Update product quantity in the cart for a guest or logged-in user - 為訪客或已登入用戶更新購物車中的產品數量
// @access Public - @訪問權限 公開
router.put("/", async (req, res) =&gt; {
  const { productId, quantity, size, color, guestId, userId } = req.body;

  try {
    let cart = await getCart(userId, guestId);
    if (!cart) return res.status(404).json({ message: "Cart not found" });

    const productIndex = cart.products.findIndex(
      (p) =&gt;
        p.productId.toString() === productId &amp;&amp;
        p.size === size &amp;&amp;
        p.color === color
    );

    if (productIndex &gt; -1) {
      // update quantity - 更新數量
      if (quantity &gt; 0) {
        cart.products&#91;productIndex].quantity = quantity;
      } else {
        cart.products.splice(productIndex, 1); // Remove product if quantity is 0 - 庫存數量為0時刪除產品
      }

      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      return res.status(404).json({ message: "Product not found in cart" });
    }
  } catch (error) {
    console.error(error);
    return res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Collections: Cart – 集合: 購物車</li>



<li>Add request – 新增請求<br>Update、PUT、http://localhost:9000/api/cart</li>



<li>Body &gt; raw</li>



<li>Headers &gt; Key、Value</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Cart/Update
// Body &gt; raw
{
    "userId": "67d13557ef893a6a945de525",
    "productId": "67d13557ef893a6a945de536",
    "size": "M",
    "color": "Red",
    "quantity": 6
}
</code></pre>



<pre class="wp-block-code"><code>// Postman - Cart/Update
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas 資料庫</h2>



<ul class="wp-block-list">
<li>查看購物車數量是否有更新</li>



<li>測試數量更改為0時是否會從購物車移除</li>
</ul>



<h2 class="wp-block-heading">Delete Cart (移除購物車)</h2>



<pre class="wp-block-code"><code>// backend/routes/cartRoutes.js
const express = require("express");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const { protect } = require("../middleware/authMiddleware");

const router = express.Router();

// Helper function to get a cart by user Id or guest ID - 根據用戶 ID 或訪客 ID 獲取購物車的輔助函數
const getCart = async (userId, guestId) =&gt; {
  if (userId) {
    return await Cart.findOne({ user: userId });
  } else if (guestId) {
    return await Cart.findOne({ guestId });
  }
  return null;
};

// @route POST /api/cart - @路由、使用 POST 方法、API 的路徑
// @desc Add a product to the cart for a guest or logged in user - @描述 將產品增加到訪客或已登入用戶的購物車中
// @access Public - @訪問權限 公開
router.post("/", async (req, res) =&gt; {
  const { productId, quantity, size, color, guestId, userId } = req.body;
  try {
    const product = await Product.findById(productId);
    if (!product) return res.status(404).json({ message: "Product not found" });

    // Determine if the user is logged in or guest - 判斷用戶是已登入還是訪客
    let cart = await getCart(userId, guestId);

    // If the cart exists,update it - 如果購物車存在，則更新它
    if (cart) {
      const productIndex = cart.products.findIndex(
        (p) =&gt;
          p.productId.toString() === productId &amp;&amp;
          p.size === size &amp;&amp;
          p.color === color
      );

      if (productIndex &gt; -1) {
        // If the product already exists, update the quantity
        cart.products&#91;productIndex].quantity += quantity;
      } else {
        // add new product - 增加新產品
        cart.products.push({
          productId,
          name: product.name,
          image: product.images&#91;0].url,
          price: product.price,
          size,
          color,
          quantity,
        });
      }

      // Recalculate the total price - 重新計算總價格
      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      // Create a new cart for the guest or user - 為訪客或用戶創建一個新購物車
      const newCart = await Cart.create({
        user: userId ? userId : undefined,
        guestId: guestId ? guestId : "guest_" + new Date().getTime(),
        products: &#91;
          {
            productId,
            name: product.name,
            image: product.images&#91;0].url,
            price: product.price,
            size,
            color,
            quantity,
          },
        ],
        totalPrice: product.price * quantity,
      });
      return res.status(201).json(newCart);
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route PUT /api/cart - @路由、使用 PUT 方法、API 的路徑
// @desc Update product quantity in the cart for a guest or logged-in user - 為訪客或已登入用戶更新購物車中的產品數量
// @access Public - @訪問權限 公開
router.put("/", async (req, res) =&gt; {
  const { productId, quantity, size, color, guestId, userId } = req.body;

  try {
    let cart = await getCart(userId, guestId);
    if (!cart) return res.status(404).json({ message: "Cart not found" });

    const productIndex = cart.products.findIndex(
      (p) =&gt;
        p.productId.toString() === productId &amp;&amp;
        p.size === size &amp;&amp;
        p.color === color
    );

    if (productIndex &gt; -1) {
      // update quantity - 更新數量
      if (quantity &gt; 0) {
        cart.products&#91;productIndex].quantity = quantity;
      } else {
        cart.products.splice(productIndex, 1); // Remove product if quantity is 0 - 庫存數量為0時刪除產品
      }

      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      return res.status(404).json({ message: "Product not found in cart" });
    }
  } catch (error) {
    console.error(error);
    return res.status(500).json({ message: "Server Error" });
  }
});

// @route DELETE /api/cart - @路由、使用 DELETE 方法、API 的路徑
// @desc Remove a product from the cart - @描述 從購物車中刪除商品
// @access Public - @訪問權限 公開
router.delete("/", async (req, res) =&gt; {
  const { productId, size, color, guestId, userId } = req.body;
  try {
    let cart = await getCart(userId, guestId);

    if (!cart) return res.status(404).json({ message: "Cart not found" });

    const productIndex = cart.products.findIndex(
      (p) =&gt;
        p.productId.toString() === productId &amp;&amp;
        p.size === size &amp;&amp;
        p.color === color
    );

    if (productIndex &gt; -1) {
      cart.products.splice(productIndex, 1);

      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      return res.status(404).json({ message: "Product not found in cart" });
    }
  } catch (error) {
    console.error(error);
    return res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Collections: Cart – 集合: 購物車</li>



<li>Add request – 新增請求<br>Delete、DELETE、http://localhost:9000/api/cart</li>



<li>Headers &gt; Key、Value</li>



<li>Body &gt; raw</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Cart/Delete
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<pre class="wp-block-code"><code>// Postman - Cart/Delete
// Body &gt; raw
{
    "userId": "67d13557ef893a6a945de525",
    "productId": "67d13557ef893a6a945de536",
    "size": "M",
    "color": "Red"
}
</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas 資料庫</h2>



<ul class="wp-block-list">
<li>查看購物車數量是否有刪除</li>
</ul>



<h2 class="wp-block-heading">Read Cart (查看購物車)</h2>



<pre class="wp-block-code"><code>// backend/routes/cartRoutes.js
const express = require("express");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const { protect } = require("../middleware/authMiddleware");

const router = express.Router();

// Helper function to get a cart by user Id or guest ID - 根據用戶 ID 或訪客 ID 獲取購物車的輔助函數
const getCart = async (userId, guestId) =&gt; {
  if (userId) {
    return await Cart.findOne({ user: userId });
  } else if (guestId) {
    return await Cart.findOne({ guestId });
  }
  return null;
};

// @route POST /api/cart - @路由、使用 POST 方法、API 的路徑
// @desc Add a product to the cart for a guest or logged in user - @描述 將產品增加到訪客或已登入用戶的購物車中
// @access Public - @訪問權限 公開
router.post("/", async (req, res) =&gt; {
  const { productId, quantity, size, color, guestId, userId } = req.body;
  try {
    const product = await Product.findById(productId);
    if (!product) return res.status(404).json({ message: "Product not found" });

    // Determine if the user is logged in or guest - 判斷用戶是已登入還是訪客
    let cart = await getCart(userId, guestId);

    // If the cart exists,update it - 如果購物車存在，則更新它
    if (cart) {
      const productIndex = cart.products.findIndex(
        (p) =&gt;
          p.productId.toString() === productId &amp;&amp;
          p.size === size &amp;&amp;
          p.color === color
      );

      if (productIndex &gt; -1) {
        // If the product already exists, update the quantity
        cart.products&#91;productIndex].quantity += quantity;
      } else {
        // add new product - 增加新產品
        cart.products.push({
          productId,
          name: product.name,
          image: product.images&#91;0].url,
          price: product.price,
          size,
          color,
          quantity,
        });
      }

      // Recalculate the total price - 重新計算總價格
      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      // Create a new cart for the guest or user - 為訪客或用戶創建一個新購物車
      const newCart = await Cart.create({
        user: userId ? userId : undefined,
        guestId: guestId ? guestId : "guest_" + new Date().getTime(),
        products: &#91;
          {
            productId,
            name: product.name,
            image: product.images&#91;0].url,
            price: product.price,
            size,
            color,
            quantity,
          },
        ],
        totalPrice: product.price * quantity,
      });
      return res.status(201).json(newCart);
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route PUT /api/cart - @路由、使用 PUT 方法、API 的路徑
// @desc Update product quantity in the cart for a guest or logged-in user - 為訪客或已登入用戶更新購物車中的產品數量
// @access Public - @訪問權限 公開
router.put("/", async (req, res) =&gt; {
  const { productId, quantity, size, color, guestId, userId } = req.body;

  try {
    let cart = await getCart(userId, guestId);
    if (!cart) return res.status(404).json({ message: "Cart not found" });

    const productIndex = cart.products.findIndex(
      (p) =&gt;
        p.productId.toString() === productId &amp;&amp;
        p.size === size &amp;&amp;
        p.color === color
    );

    if (productIndex &gt; -1) {
      // update quantity - 更新數量
      if (quantity &gt; 0) {
        cart.products&#91;productIndex].quantity = quantity;
      } else {
        cart.products.splice(productIndex, 1); // Remove product if quantity is 0 - 庫存數量為0時刪除產品
      }

      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      return res.status(404).json({ message: "Product not found in cart" });
    }
  } catch (error) {
    console.error(error);
    return res.status(500).json({ message: "Server Error" });
  }
});

// @route DELETE /api/cart - @路由、使用 DELETE 方法、API 的路徑
// @desc Remove a product from the cart - @描述 從購物車中刪除商品
// @access Public - @訪問權限 公開
router.delete("/", async (req, res) =&gt; {
  const { productId, size, color, guestId, userId } = req.body;
  try {
    let cart = await getCart(userId, guestId);

    if (!cart) return res.status(404).json({ message: "Cart not found" });

    const productIndex = cart.products.findIndex(
      (p) =&gt;
        p.productId.toString() === productId &amp;&amp;
        p.size === size &amp;&amp;
        p.color === color
    );

    if (productIndex &gt; -1) {
      cart.products.splice(productIndex, 1);

      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      return res.status(404).json({ message: "Product not found in cart" });
    }
  } catch (error) {
    console.error(error);
    return res.status(500).json({ message: "Server Error" });
  }
});

// @route GET /api/cart - @路由、使用 GET 方法、API 的路徑
// @desc Get logged-in user's or guest user's cart - @描述 獲取已登入用戶或訪客用戶的購物車
// @access Public - @訪問權限 公開
router.get("/", async (req, res) =&gt; {
  const { userId, guestId } = req.query;

  try {
    const cart = await getCart(userId, guestId);
    if (cart) {
      res.json(cart);
    } else {
      res.status(404).json({ message: "Cart not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Collections: Cart – 集合: 購物車</li>



<li>Add request – 新增請求<br>Cart Details、GET、http://localhost:9000/api/cart</li>



<li>Headers &gt; Key、Value</li>



<li>Params &gt; Key、Value</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Cart/Cart Details
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<pre class="wp-block-code"><code>// Postman - Cart/Cart Details
// Params
Key: userId
Value: 你的 userId
</code></pre>



<h2 class="wp-block-heading">Merge Cart (合併購物車)</h2>



<pre class="wp-block-code"><code>// backend/routes/cartRoutes.js
const express = require("express");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const { protect } = require("../middleware/authMiddleware");

const router = express.Router();

// Helper function to get a cart by user Id or guest ID - 根據用戶 ID 或訪客 ID 獲取購物車的輔助函數
const getCart = async (userId, guestId) =&gt; {
  if (userId) {
    return await Cart.findOne({ user: userId });
  } else if (guestId) {
    return await Cart.findOne({ guestId });
  }
  return null;
};

// @route POST /api/cart - @路由、使用 POST 方法、API 的路徑
// @desc Add a product to the cart for a guest or logged in user - @描述 將產品增加到訪客或已登入用戶的購物車中
// @access Public - @訪問權限 公開
router.post("/", async (req, res) =&gt; {
  const { productId, quantity, size, color, guestId, userId } = req.body;
  try {
    const product = await Product.findById(productId);
    if (!product) return res.status(404).json({ message: "Product not found" });

    // Determine if the user is logged in or guest - 判斷用戶是已登入還是訪客
    let cart = await getCart(userId, guestId);

    // If the cart exists,update it - 如果購物車存在，則更新它
    if (cart) {
      const productIndex = cart.products.findIndex(
        (p) =&gt;
          p.productId.toString() === productId &amp;&amp;
          p.size === size &amp;&amp;
          p.color === color
      );

      if (productIndex &gt; -1) {
        // If the product already exists, update the quantity
        cart.products&#91;productIndex].quantity += quantity;
      } else {
        // add new product - 增加新產品
        cart.products.push({
          productId,
          name: product.name,
          image: product.images&#91;0].url,
          price: product.price,
          size,
          color,
          quantity,
        });
      }

      // Recalculate the total price - 重新計算總價格
      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      // Create a new cart for the guest or user - 為訪客或用戶創建一個新購物車
      const newCart = await Cart.create({
        user: userId ? userId : undefined,
        guestId: guestId ? guestId : "guest_" + new Date().getTime(),
        products: &#91;
          {
            productId,
            name: product.name,
            image: product.images&#91;0].url,
            price: product.price,
            size,
            color,
            quantity,
          },
        ],
        totalPrice: product.price * quantity,
      });
      return res.status(201).json(newCart);
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route PUT /api/cart - @路由、使用 PUT 方法、API 的路徑
// @desc Update product quantity in the cart for a guest or logged-in user - 為訪客或已登入用戶更新購物車中的產品數量
// @access Public - @訪問權限 公開
router.put("/", async (req, res) =&gt; {
  const { productId, quantity, size, color, guestId, userId } = req.body;

  try {
    let cart = await getCart(userId, guestId);
    if (!cart) return res.status(404).json({ message: "Cart not found" });

    const productIndex = cart.products.findIndex(
      (p) =&gt;
        p.productId.toString() === productId &amp;&amp;
        p.size === size &amp;&amp;
        p.color === color
    );

    if (productIndex &gt; -1) {
      // update quantity - 更新數量
      if (quantity &gt; 0) {
        cart.products&#91;productIndex].quantity = quantity;
      } else {
        cart.products.splice(productIndex, 1); // Remove product if quantity is 0 - 庫存數量為0時刪除產品
      }

      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      return res.status(404).json({ message: "Product not found in cart" });
    }
  } catch (error) {
    console.error(error);
    return res.status(500).json({ message: "Server Error" });
  }
});

// @route DELETE /api/cart - @路由、使用 DELETE 方法、API 的路徑
// @desc Remove a product from the cart - @描述 從購物車中刪除商品
// @access Public - @訪問權限 公開
router.delete("/", async (req, res) =&gt; {
  const { productId, size, color, guestId, userId } = req.body;
  try {
    let cart = await getCart(userId, guestId);

    if (!cart) return res.status(404).json({ message: "Cart not found" });

    const productIndex = cart.products.findIndex(
      (p) =&gt;
        p.productId.toString() === productId &amp;&amp;
        p.size === size &amp;&amp;
        p.color === color
    );

    if (productIndex &gt; -1) {
      cart.products.splice(productIndex, 1);

      cart.totalPrice = cart.products.reduce(
        (acc, item) =&gt; acc + item.price * item.quantity,
        0
      );
      await cart.save();
      return res.status(200).json(cart);
    } else {
      return res.status(404).json({ message: "Product not found in cart" });
    }
  } catch (error) {
    console.error(error);
    return res.status(500).json({ message: "Server Error" });
  }
});

// @route GET /api/cart - @路由、使用 GET 方法、API 的路徑
// @desc Get logged-in user's or guest user's cart - @描述 獲取已登入用戶或訪客用戶的購物車
// @access Public - @訪問權限 公開
router.get("/", async (req, res) =&gt; {
  const { userId, guestId } = req.query;

  try {
    const cart = await getCart(userId, guestId);
    if (cart) {
      res.json(cart);
    } else {
      res.status(404).json({ message: "Cart not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route POST /api/cart/merge - @路由、使用 POST 方法、API 的路徑
// @desc Merge guest cart into user cart on login - @描述 登入時將訪客購物車合併到用戶購物車
// @access Private - @訪問權限 私人
router.post("/merge", protect, async (req, res) =&gt; {
  const { guestId } = req.body;

  try {
    // Find the guest cart and user cart - 查找訪客購物車和用戶購物車
    const guestCart = await Cart.findOne({ guestId });
    const userCart = await Cart.findOne({ user: req.user._id });

    if (guestCart) {
      if (guestCart.products.length === 0) {
        return res.status(400).json({ message: "Guest cart is empty" });
      }

      if (userCart) {
        // Merge guest cart into user cart - 將訪客購物車合併到用戶購物車
        guestCart.products.forEach((guestItem) =&gt; {
          const productIndex = userCart.products.findIndex(
            (item) =&gt;
              item.productId.toString() === guestItem.productId.toString() &amp;&amp;
              item.size === guestItem.size &amp;&amp;
              item.color === guestItem.color
          );

          if (productIndex &gt; -1) {
            // If the items exists in the user cart, update the quantity - 如果商品在用戶購物車中存在，則更新數量
            userCart.products&#91;productIndex].quantity += guestItem.quantity;
          } else {
            // Otherwise, add the guest item to the cart - 否則，將訪客商品增加到購物車
            userCart.products.push(guestItem);
          }
        });

        userCart.totalPrice = userCart.products.reduce(
          (acc, item) =&gt; acc + item.price * item.quantity,
          0
        );
        await userCart.save();

        // Remove the guest cart after merging - 合併後刪除訪客購物車
        try {
          await Cart.findOneAndDelete({ guestId });
        } catch (error) {
          console.error("Error deleting guest cart:", error);
        }
        res.status(200).json(guestCart);
      } else {
        // If the user has no existing cart, assign the guest cart to the user - 如果用戶沒有現有的購物車，則將訪客購物車分配給用戶
        guestCart.user = req.user._id;
        guestCart.guestId = undefined;
        await guestCart.save();

        res.status(200).json(guestCart);
      }
    } else {
      if (userCart) {
        // Guest cart has already been merged, return user cart - 訪客購物車已經合併，返回用戶購物車
        return res.status(200).json(userCart);
      }
      res.status(404).json({ message: "Guest cart not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">初始化資料庫資料</h2>



<ul class="wp-block-list">
<li>npm run seed</li>
</ul>



<h2 class="wp-block-heading">查看 MongoDB Atlas 資料庫</h2>



<ul class="wp-block-list">
<li>查看資料是否有初始化資料庫</li>
</ul>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Collections: Products – 集合: 產品</li>



<li>取得產品 _id<br>All Products、GET、http://localhost:9000/api/products/</li>



<li>Collections: Cart – 集合: 購物車</li>



<li>建立購物車<br>Create、POST、http://localhost:9000/api/cart</li>



<li>Body &gt; raw</li>



<li>Add request – 新增請求<br>Merge、POST、http://localhost:9000/api/cart/merge</li>



<li>Body &gt; raw</li>



<li>Headers &gt; Key、Value</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Cart/Create
// Body &gt; raw
{
    "productId": "67d5186884e2f1407fcf0388",
    "size": "S",
    "color": "Red",
    "quantity": 1
}
</code></pre>



<pre class="wp-block-code"><code>// Postman - Cart/Merge
// Body &gt; raw
{
    "guestId": "guest_1742019216937"
}
</code></pre>



<pre class="wp-block-code"><code>// Postman - Cart/Merge
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<h2 class="wp-block-heading">製作結帳功能</h2>



<h3 class="wp-block-heading">Checkout (結帳)</h3>



<ul class="wp-block-list">
<li>Create</li>



<li>Pay</li>



<li>Finalize</li>
</ul>



<h3 class="wp-block-heading">CheckoutItem Schema (結帳項目模式)</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Field (欄位)</td><td>Type (類型)</td><td>Required (必填)</td><td>Description (描述)</td></tr><tr><td>productId</td><td>ObjectId (ref: Product)</td><td>Yes</td><td>The ID of the product</td></tr><tr><td>name</td><td>String</td><td>Yes</td><td>The name of the product.</td></tr><tr><td>image</td><td>String</td><td>Yes</td><td>The URL of the product image.</td></tr><tr><td>price</td><td>Number</td><td>Yes</td><td>The price of the product.</td></tr><tr><td>size</td><td>String</td><td>No</td><td>The size of the product (e.g., apparel).</td></tr><tr><td>color</td><td>String</td><td>No</td><td>The color of the product.</td></tr><tr><td>quantity</td><td>Number</td><td>Yes</td><td>The quantity of the product in the cart.</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Checkout Schema (結帳模式)</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Field (欄位)</td><td>Type (類型)</td><td>Required (必填)</td><td>Description (描述)</td></tr><tr><td>user</td><td>ObjectId (ref: User)</td><td>Yes</td><td>The ID of the user associated with the checkout.</td></tr><tr><td>checkoutItems</td><td>Array of CheckoutItems</td><td>Yes</td><td>The list of items included in the checkout.</td></tr><tr><td>shippingAddress</td><td>Object</td><td>Yes</td><td>Contains shipping details (address, city, etc.).</td></tr><tr><td>address</td><td>String</td><td>Yes</td><td>Shipping address of the user.</td></tr><tr><td>city</td><td>String</td><td>Yes</td><td>City for shipping.</td></tr><tr><td>postalCode</td><td>String</td><td>Yes</td><td>Postal code for shipping.</td></tr><tr><td>country</td><td>String</td><td>Yes</td><td>Country for shipping.</td></tr><tr><td>paymentMethod</td><td>String</td><td>Yes</td><td>Payment method used (e.g., Paypal).</td></tr><tr><td>totalPrice</td><td>Number</td><td>Yes</td><td>Total price of all items in the checkout.</td></tr><tr><td>isPaid</td><td>Boolean</td><td>No</td><td>Indicates whether the checkout is paid.</td></tr><tr><td>paidAt</td><td>Date</td><td>No</td><td>Timestamp when the payment was made.</td></tr><tr><td>paymentStatus</td><td>String</td><td>No</td><td>Status of the payment (pending, paid, etc.).</td></tr><tr><td>paymentDetails</td><td>Mixed</td><td>No</td><td>Stores details about the payment (e.g., transaction ID).</td></tr><tr><td>isFinalized</td><td>Boolean</td><td>No</td><td>Indicates if the checkout has been converted to an order.</td></tr><tr><td>finalizedAt</td><td>Date</td><td>No</td><td>Timestamp when the checkout was finalized.</td></tr><tr><td>timestamps</td><td>Date</td><td>Auto</td><td>Mongoose will auto-create createdAt and updatedAt.</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">OrderItem Schema (訂單項目模式)</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Field (欄位)</td><td>Type (類型)</td><td>Required (必填)</td><td>Description (描述)</td></tr><tr><td>productId</td><td>ObjectId (ref: Product)</td><td>Yes</td><td>The ID of the product.</td></tr><tr><td>name</td><td>String</td><td>Yes</td><td>The name of the product.</td></tr><tr><td>image</td><td>String</td><td>Yes</td><td>The URL of the product image.</td></tr><tr><td>price</td><td>Number</td><td>Yes</td><td>The price of the product.</td></tr><tr><td>size</td><td>String</td><td>No</td><td>Size of the product (e.g., apparel).</td></tr><tr><td>color</td><td>String</td><td>No</td><td>Color of the product.</td></tr><tr><td>quantity</td><td>Number</td><td>Yes</td><td>The quantity of the product in the order.</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Order Schema (訂單模式)</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Field (欄位)</td><td>Type (類型)</td><td>Required (必填)</td><td>Description (描述)</td></tr><tr><td>user</td><td>ObjectId (ref: User)</td><td>Yes</td><td>The ID of the user who placed the order.</td></tr><tr><td>orderItems</td><td>Array of OrderItems</td><td>Yes</td><td>The list of items included in the order.</td></tr><tr><td>shippingAddress</td><td>Object</td><td>Yes</td><td>Shipping details including address, ciy, etc.</td></tr><tr><td>address</td><td>String</td><td>Yes</td><td>The full shipping address.</td></tr><tr><td>city</td><td>String</td><td>Yes</td><td>The ciy for shipping.</td></tr><tr><td>postalCode</td><td>String</td><td>Yes</td><td>The postal code for shipping.</td></tr><tr><td>country</td><td>String</td><td>Yes</td><td>The country for shipping.</td></tr><tr><td>paymentMethod</td><td>String</td><td>Yes</td><td>Payment method used (e.g., Credit Card, PayPal).</td></tr><tr><td>totalPrice</td><td>Number</td><td>Yes</td><td>Total price of all items in the order.</td></tr><tr><td>isPaid</td><td>Boolean</td><td>No</td><td>Indicates whether the order has been paid.</td></tr><tr><td>paidAt</td><td>Date</td><td>No</td><td>Timestamp when the payment was made.</td></tr><tr><td>isDelivered</td><td>Boolean</td><td>No</td><td>Indicates whether the order has been delivered.</td></tr><tr><td>deliveredAt</td><td>Date</td><td>No</td><td>Timestamp when the order was delivered.</td></tr><tr><td>paymentStatus</td><td>String</td><td>No</td><td>Payment status (pending, paid, etc.).</td></tr><tr><td>status</td><td>String</td><td>No</td><td>Order status (Processing, Shipped, Delivered, Cancelled).</td></tr><tr><td>timestamps</td><td>Date</td><td>Auto</td><td>Auto-created fields for createdAt and updatedAt.</td></tr></tbody></table></figure>



<pre class="wp-block-code"><code>// backend/models/Checkout.js
// 少了 quantity
const mongoose = require("mongoose");

const checkoutItemSchema = new mongoose.Schema(
  {
    productId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Product",
      required: true,
    },
    name: {
      type: String,
      required: true,
    },
    image: {
      type: String,
      requied: true,
    },
    price: {
      type: Number,
      requied: true,
    },
  },
  { _id: false }
);

const checkoutSchema = new mongoose.Schema(
  {
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
      requied: true,
    },
    checkoutItems: &#91;checkoutItemSchema],
    shippingAddress: {
      address: { type: String, required: true },
      city: { type: String, required: true },
      postalCode: { type: String, required: true },
      country: { type: String, required: true },
    },
    paymentMethod: {
      type: String,
      required: true,
    },
    totalPrice: {
      type: Number,
      required: true,
    },
    isPaid: {
      type: Boolean,
      default: false,
    },
    paidAt: {
      type: Date,
    },
    paymentStatus: {
      type: String,
      default: "pending",
    },
    paymentDetails: {
      type: mongoose.Schema.Types.Mixed, // store payment-related details(transaction ID, paypal response) - 存儲與支付相關的詳細資訊(如交易 ID 和 PayPal 回應)
    },
    isFinalized: {
      type: Boolean,
      default: false,
    },
    finalizedAt: {
      type: Date,
    },
  },
  { timestamps: true }
);

module.exports = mongoose.model("Checkout", checkoutSchema);
</code></pre>



<pre class="wp-block-code"><code>// backend/models/Order.js
const mongoose = require("mongoose");

const orderItemSchema = new mongoose.Schema(
  {
    productId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Product",
      required: true,
    },
    name: {
      type: String,
      required: true,
    },
    image: {
      type: String,
      required: true,
    },
    price: {
      type: Number,
      required: true,
    },
    size: String,
    color: String,
    quantity: {
      type: Number,
      required: true,
    },
  },
  { _id: false }
);

const orderSchema = new mongoose.Schema(
  {
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
      required: true,
    },
    orderItems: &#91;orderItemSchema],
    shippingAddress: {
      address: { type: String, required: true },
      city: { type: String, required: true },
      postalCode: { type: String, required: true },
      country: { type: String, required: true },
    },
    paymentMethod: {
      type: String,
      required: true,
    },
    totalPrice: {
      type: Number,
      required: true,
    },
    isPaid: {
      type: Boolean,
      default: false,
    },
    paidAt: {
      type: Date,
    },
    isDelivered: {
      type: Boolean,
      default: false,
    },
    deliveredAt: {
      type: Date,
    },
    paymentStatus: {
      type: String,
      default: "pending",
    },
    status: {
      type: String,
      enum: &#91;"Processing", "Shipped", "Delivered", "Cancelled"],
      default: "Processing",
    },
  },
  { timestamps: true }
);

module.exports = mongoose.model("Order", orderSchema);
</code></pre>



<pre class="wp-block-code"><code>// backend/routes/checkoutRoutes.js
// 創建最終訂單有錯誤
const express = require("express");
const Checkout = require("../models/Checkout");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const Order = require("../models/Order");
const { protect } = require("../middleware/authMiddleware");

const router = express.Router();

// @route POST /api/checkout - @路由、使用 POST 方法、API 的路徑
// @desc Create a new checkout session - @描述 創建新的結帳會話
// @access Private - @訪問權限 私人
router.post("/", protect, async (req, res) =&gt; {
  const { checkoutItems, shippingAddress, paymentMethod, totalPrice } =
    req.body;

  if (!checkoutItems || checkoutItems.length === 0) {
    return res.status(400).json({ message: "No items in checkout" });
  }

  try {
    // Create a new checkout session - 創建新的結帳會話
    const newCheckout = await Checkout.create({
      user: req.user._id,
      checkoutItems: checkoutItems,
      shippingAddress,
      paymentMethod,
      totalPrice,
      paymentStatus: "Pending",
      isPaid: false,
    });
    console.log(`Checkout created for user: ${req.user._id}`);
    res.status(201).json(newCheckout);
  } catch (error) {
    console.error("Error creating checkout session:", error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route PUT /api/checkout/:id/pay - @路由、使用 PUT 方法、API 的路徑
// @desc Update checkout to mark as paid after successful payment - @描述 要在成功付款後更新結帳狀態為已付款
// @access Private
router.put("/:id/pay", protect, async (req, res) =&gt; {
  const { paymentStatus, paymentDetails } = req.body;

  try {
    const checkout = await Checkout.findById(req.params.id);

    if (!checkout) {
      return res.status(404).json({ message: "Checkout not found" });
    }

    if (paymentStatus === "paid") {
      checkout.isPaid = true;
      checkout.paymentStatus = paymentStatus;
      checkout.paymentDetails = paymentDetails;
      checkout.paidAt = Date.now();
      await checkout.save();

      res.status(200).json(checkout);
    } else {
      res.status(400).json({ message: "Invalid Payment Status" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route POST /api/checkout/:id/finalize - @路由、使用 POST 方法、API 的路徑
// @desc Finalize checkout and convert to an order after payment confirmation - @描述 將結帳完成並在付款確認後轉換為訂單
// @access Private - @訪問權限 私人
router.post("/:id/finalize", protect, async (req, res) =&gt; {
  try {
    const checkout = await Checkout.findById(req.params.id);

    if (!checkout) {
      return res.status(404).json({ message: "Checkout not found" });
    }

    if (checkout.isPaid &amp;&amp; !checkout.isFinalized) {
      // Create final order based on the checkout details - 根據結帳詳情創建最終訂單
      const finalOrder = await Order.create({
        user: checkout.user,
        orderItems: checkout.orderItems,
        shippingAddress: checkout.shippingAddress,
        paymentMethod: checkout.paymentMethod,
        totalPrice: checkout.totalPrice,
        isPaid: true,
        paidAt: checkout.paidAt,
        isDelivered: false,
        paymentStatus: "paid",
        paymentDetails: checkout.paymentDetails,
      });

      // Mark the checkout as finalized - 將結帳標記為已完成
      checkout.isFinalized = true;
      checkout.finalizedAt = Date.now();
      await checkout.save();
      // Delete the cart associated with the user - 刪除與用戶相關的購物車
      await Cart.findOneAndDelete({ user: checkout.user });
      res.status(201).json(finalOrder);
    } else if (checkout.isFinalized) {
      res.status(400).json({ message: "Checkout already finalized" });
    } else {
      res.status(400).json({ message: "Checkout is not paid" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

// Connect to MongoDB - 連接到 MongoDB
connectDB();

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Create new collection – 建立新的集合<br>Checkout</li>



<li>Add a request – 新增請求<br>Create、POST、http://localhost:9000/api/checkout</li>



<li>Headers > Key、Value</li>



<li>Body > raw</li>



<li>Add request – 新增請求<br>Pay、PUT、http://localhost:9000/api/checkout/:id/pay</li>



<li>Params > Path Variables > Key、Value</li>



<li>Headers > Key、Value</li>



<li>Body > raw</li>



<li>Duplicate – 複製 Pay 請求<br>Finalize、POST、http://localhost:9000/api/checkout/:id/finalize</li>



<li>Body > raw<br>清空內容</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Checkout/Create
// Headers
Key: Authorization
Value: Bearer 你的Token</code></pre>



<pre class="wp-block-code"><code>// Postman - Checkout/Create
// Body &gt; raw
{
    "checkoutItems": &#91;
        {
            "productId": "67d5186884e2f1407fcf0388",
            "name": "Classic T-shirt",
            "image": "https://picsum.photos/seed/denim1/500/500",
            "price": 19
        }
    ],
    "shippingAddress": {
        "address": "123 main street",
        "city": "New York",
        "postalCode": "10001",
        "country": "USA"
    },
    "paymentMethod": "PayPal",
    "totalPrice": 19
}
</code></pre>



<pre class="wp-block-code"><code>// Postman - Checkout/Pay
// Params &gt; Path Variables &gt; Key、Value
Key: id
Value: 67d782e2320689ef987bfad1
</code></pre>



<pre class="wp-block-code"><code>// Postman - Checkout/Pay
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<pre class="wp-block-code"><code>// Postman - Checkout/Pay
// Body &gt; raw
{
    "paymentStatus": "paid",
    "paymentDetails": {
        "transactionId": "txn_123456789",
        "paymentGateway": "PayPal",
        "amount": 19,
        "currency": "USD"
    }
}
</code></pre>



<pre class="wp-block-code"><code>// Postman - Checkout/Finalize
// Body &gt; raw - 清空內容
</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas</h2>



<ul class="wp-block-list">
<li>Browse collections (瀏覽集合) > checkouts</li>



<li>Browse collections > orders<br>發現 orders > orderItems 有錯誤<br>路由 checkoutRoutes.js 除錯</li>



<li>測試 isFinalized 結帳完成功能<br>出現錯誤、開始除錯</li>
</ul>



<pre class="wp-block-code"><code>// backend/routes/checkoutRoutes.js
const express = require("express");
const Checkout = require("../models/Checkout");
const Cart = require("../models/Cart");
const Product = require("../models/Product");
const Order = require("../models/Order");
const { protect } = require("../middleware/authMiddleware");

const router = express.Router();

// @route POST /api/checkout - @路由、使用 POST 方法、API 的路徑
// @desc Create a new checkout session - @描述 創建新的結帳會話
// @access Private - @訪問權限 私人
router.post("/", protect, async (req, res) =&gt; {
  const { checkoutItems, shippingAddress, paymentMethod, totalPrice } =
    req.body;

  if (!checkoutItems || checkoutItems.length === 0) {
    return res.status(400).json({ message: "No items in checkout" });
  }

  try {
    // Create a new checkout session - 創建新的結帳會話
    const newCheckout = await Checkout.create({
      user: req.user._id,
      checkoutItems: checkoutItems,
      shippingAddress,
      paymentMethod,
      totalPrice,
      paymentStatus: "Pending",
      isPaid: false,
    });
    console.log(`Checkout created for user: ${req.user._id}`);
    res.status(201).json(newCheckout);
  } catch (error) {
    console.error("Error creating checkout session:", error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route PUT /api/checkout/:id/pay - @路由、使用 PUT 方法、API 的路徑
// @desc Update checkout to mark as paid after successful payment - @描述 要在成功付款後更新結帳狀態為已付款
// @access Private
router.put("/:id/pay", protect, async (req, res) =&gt; {
  const { paymentStatus, paymentDetails } = req.body;

  try {
    const checkout = await Checkout.findById(req.params.id);

    if (!checkout) {
      return res.status(404).json({ message: "Checkout not found" });
    }

    if (paymentStatus === "paid") {
      checkout.isPaid = true;
      checkout.paymentStatus = paymentStatus;
      checkout.paymentDetails = paymentDetails;
      checkout.paidAt = Date.now();
      await checkout.save();

      res.status(200).json(checkout);
    } else {
      res.status(400).json({ message: "Invalid Payment Status" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route POST /api/checkout/:id/finalize - @路由、使用 POST 方法、API 的路徑
// @desc Finalize checkout and convert to an order after payment confirmation - @描述 將結帳完成並在付款確認後轉換為訂單
// @access Private - @訪問權限 私人
router.post("/:id/finalize", protect, async (req, res) =&gt; {
  try {
    const checkout = await Checkout.findById(req.params.id);

    if (!checkout) {
      return res.status(404).json({ message: "Checkout not found" });
    }

    if (checkout.isPaid &amp;&amp; !checkout.isFinalized) {
      // Create final order based on the checkout details - 根據結帳詳情創建最終訂單
      const finalOrder = await Order.create({
        user: checkout.user,
        orderItems: checkout.checkoutItems,
        shippingAddress: checkout.shippingAddress,
        paymentMethod: checkout.paymentMethod,
        totalPrice: checkout.totalPrice,
        isPaid: true,
        paidAt: checkout.paidAt,
        isDelivered: false,
        paymentStatus: "paid",
        paymentDetails: checkout.paymentDetails,
      });

      // Mark the checkout as finalized - 將結帳標記為已完成
      checkout.isFinalized = true;
      checkout.finalizedAt = Date.now();
      await checkout.save();
      // Delete the cart associated with the user - 刪除與用戶相關的購物車
      await Cart.findOneAndDelete({ user: checkout.user });
      res.status(201).json(finalOrder);
    } else if (checkout.isFinalized) {
      res.status(400).json({ message: "Checkout already finalized" });
    } else {
      res.status(400).json({ message: "Checkout is not paid" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 工具</h2>



<ul class="wp-block-list">
<li>Checkout/Create 除錯</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Checkout/Create
// Body &gt; raw
{
    "checkoutItems": &#91;
        {
            "productId": "67d5186884e2f1407fcf0388",
            "name": "Classic T-shirt",
            "image": "https://picsum.photos/seed/denim1/500/500",
            "price": 19,
            "quantity": 1
        }
    ],
    "shippingAddress": {
        "address": "123 main street",
        "city": "New York",
        "postalCode": "10001",
        "country": "USA"
    },
    "paymentMethod": "PayPal",
    "totalPrice": 19
}
</code></pre>



<h2 class="wp-block-heading">Checkout Schema 除錯</h2>



<pre class="wp-block-code"><code>// backend/models/Checkout.js
const mongoose = require("mongoose");

const checkoutItemSchema = new mongoose.Schema(
  {
    productId: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "Product",
      required: true,
    },
    name: {
      type: String,
      required: true,
    },
    image: {
      type: String,
      requied: true,
    },
    price: {
      type: Number,
      requied: true,
    },
    quantity: {
      type: Number,
      required: true,
    },
  },
  { _id: false }
);

const checkoutSchema = new mongoose.Schema(
  {
    user: {
      type: mongoose.Schema.Types.ObjectId,
      ref: "User",
      requied: true,
    },
    checkoutItems: &#91;checkoutItemSchema],
    shippingAddress: {
      address: { type: String, required: true },
      city: { type: String, required: true },
      postalCode: { type: String, required: true },
      country: { type: String, required: true },
    },
    paymentMethod: {
      type: String,
      required: true,
    },
    totalPrice: {
      type: Number,
      required: true,
    },
    isPaid: {
      type: Boolean,
      default: false,
    },
    paidAt: {
      type: Date,
    },
    paymentStatus: {
      type: String,
      default: "pending",
    },
    paymentDetails: {
      type: mongoose.Schema.Types.Mixed, // store payment-related details(transaction ID, paypal response) - 存儲與支付相關的詳細資訊(如交易 ID 和 PayPal 回應)
    },
    isFinalized: {
      type: Boolean,
      default: false,
    },
    finalizedAt: {
      type: Date,
    },
  },
  { timestamps: true }
);

module.exports = mongoose.model("Checkout", checkoutSchema);
</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas</h2>



<ul class="wp-block-list">
<li>查看 checkout 集合是否能正確顯示 quantity 屬性</li>
</ul>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>複製 Checkout/Create 的 _id</li>



<li>更新 Checkout/Pay 的 id</li>



<li>更新 Checkout/Finalize 的 id</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Checkout/Pay
// Params &gt; Path Variables &gt; Key、Value
Key: id
Value: 67d78fdaae89363d137c322e
</code></pre>



<pre class="wp-block-code"><code>// Postman - Checkout/Finalize
// Params &gt; Path Variables &gt; Key、Value
Key: id
Value: 67d78fdaae89363d137c322e
</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas 資料庫</h2>



<ul class="wp-block-list">
<li>查看 orders > orderItems 資料是否能正常建立、不會被結帳操作而被清空</li>
</ul>



<h2 class="wp-block-heading">製作訂單功能</h2>



<pre class="wp-block-code"><code>// backend/routes/orderRoutes.js
const express = require("express");
const Order = require("../models/Order");
const { protect } = require("../middleware/authMiddleware");

const router = express.Router();

// @route GET /api/orders/my-orders - @路由、使用 GET 方法、API 的路徑
// @desc Get logged-in user's orders - @描述 獲取登入用戶的訂單
// @access Private - @訪問權限 私人
router.get("/my-orders", protect, async (req, res) =&gt; {
  try {
    // Find orders for the authenticated user - 查找已驗證用戶的訂單
    const orders = await Order.find({ user: req.user._id }).sort({
      createdAt: -1,
    }); // sort by most recent orders - 按最新訂單排序
    res.json(orders);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route GET /api/orders/:id - @路由、使用 GET 方法、API 的路徑
// @desc Get order details by ID - @描述 根據 ID 獲取訂單詳情
// @access Private - @訪問權限 私人
router.get("/:id", protect, async (req, res) =&gt; {
  try {
    const order = await Order.findById(req.params.id).populate(
      "user",
      "name email"
    );

    if (!order) {
      return res.status(404).json({ message: "Order not found" });
    }

    // Return the full order details - 返回完整的訂單詳情
    res.json(order);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

// Connect to MongoDB - 連接到 MongoDB
connectDB();

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Create new collection – 建立新的集合<br>Orders</li>



<li>Add a request – 新增請求<br>My orders、GET、http://localhost:9000/api/orders/my-orders</li>



<li>Headers > Key、Value</li>



<li>Duplicate – 複製 My orders 請求<br>Order Details、GET、http://localhost:9000/api/orders/:id<br>複製 Orders/My orders 的 _id</li>



<li>Params > Path Variables > Key、Value</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Orders/My orders
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<pre class="wp-block-code"><code>// Postman - Orders/Order Details
// Params &gt; Path Variables &gt; Key、Value
Key: id
Value: 67d789fd320689ef987bfada</code></pre>



<h2 class="wp-block-heading">製作圖片上傳功能 (管理員)</h2>



<ul class="wp-block-list">
<li>upload images to the products in the admin section (在管理區域將圖片上傳至產品)</li>



<li><a href="https://cloudinary.com/" target="_blank" rel="noreferrer noopener">Cloudinary</a> – Getting Started (開始使用)</li>



<li>View API Keys</li>



<li>安裝相關套件 multer、cloudinary、streamifier</li>
</ul>



<pre class="wp-block-code"><code>// backend/.env
PORT=9000
MONGO_URI=mongodb+srv://&lt;username&gt;:&lt;password&gt;@&lt;cluster-address&gt;/&lt;database-name&gt;?retryWrites=true&amp;w=majority&amp;appName=&lt;app-name&gt;
JWT_SECRET=你的JWT密鑰

CLOUDINARY_CLOUD_NAME=你的Clound name
CLOUDINARY_API_KEY=你的API Key
CLOUDINARY_API_SECRET=你的API secret
</code></pre>



<pre class="wp-block-code"><code>// backend - 後端
// TERMINAL - 終端機
npm install multer cloudinary streamifier
</code></pre>



<pre class="wp-block-code"><code>// backend/routes/uploadRoutes.js
const express = require("express");
const multer = require("multer");
const cloudinary = require("cloudinary").v2;
const streamifier = require("streamifier");

require("dotenv").config();

const router = express.Router();

// Cloudinary Configuration - Cloudinary 配置
cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key: process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
});

// Multer setup using memory storage - 使用記憶體存儲的 Multer 設置
const storage = multer.memoryStorage();
const upload = multer({ storage });

router.post("/", upload.single("image"), async (req, res) =&gt; {
  try {
    if (!req.file) {
      return res.status(400).json({ message: "No file uploaded" });
    }

    // Function to handle the stream upload to Cloudinary - 處理將資料流上傳到 Cloudinary 的函式
    const streamUpload = (fileBuffer) =&gt; {
      return new Promise((resolve, reject) =&gt; {
        const stream = cloudinary.uploader.upload_stream((error, result) =&gt; {
          if (result) {
            resolve(result);
          } else {
            reject(error);
          }
        });

        // Use streamifier to convert file buffer to a stream - 使用 streamifier 將檔案緩衝區轉換為資料流
        streamifier.createReadStream(fileBuffer).pipe(stream);
      });
    };

    // Call the streamUpload function - 呼叫 streamUpload 函數
    const result = await streamUpload(req.file.buffer);

    // Respond with the uploaded image URL - 回應並提供上傳的圖片 URL
    res.json({ imageUrl: result.secure_url });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");
const uploadRoutes = require("./routes/uploadRoutes");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

// Connect to MongoDB - 連接到 MongoDB
connectDB();

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Create new collection – 建立新的集合<br>Upload</li>



<li>Add a request – 新增請求<br>Create、POST、http://localhost:9000/api/upload</li>



<li>Headers > Key、Value</li>



<li>Body > form-data > Key、Value</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Upload/Create
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<pre class="wp-block-code"><code>// Postman - Upload/Create
// Body &gt; form-data
Key: image / File
Value: Select files &gt; New file from local machine
Photo by Ayo Ogunseinde on Unsplash</code></pre>



<h2 class="wp-block-heading">製作電子信箱訂閱功能</h2>



<pre class="wp-block-code"><code>// backend/models/Subscriber.js
const mongoose = require("mongoose");

const subscriberSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowercase: true,
  },
  subscribedAt: {
    type: Date,
    default: Date.now,
  },
});

module.exports = mongoose.model("Subscriber", subscriberSchema);
</code></pre>



<pre class="wp-block-code"><code>// backend/routes/subscribeRoute.js
const express = require("express");
const router = express.Router();
const Subscriber = require("../models/Subscriber");

// @route POST /api/subscribe - @路由、使用 POST 方法、API 的路徑
// @desc Handle newsletter subscription - @描述 處理電子報訂閱
// @access Public - @訪問權限 公開
router.post("/subscribe", async (req, res) =&gt; {
  const { email } = req.body;

  if (!email) {
    return res.status(400).json({ message: "Email is required" });
  }

  try {
    // Check if the email is already subscribed
    let subscriber = await Subscriber.findOne({ email });

    if (subscriber) {
      return res.status(400).json({ message: "Email is already subscribed" });
    }

    // Create a new subscriber - 創建新的訂閱者
    subscriber = new Subscriber({ email });
    await subscriber.save();

    res
      .status(201)
      .json({ message: "Successfully subscribed to the newsletter!" });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");
const uploadRoutes = require("./routes/uploadRoutes");
const subscribeRoute = require("./routes/subscribeRoute");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

// Connect to MongoDB - 連接到 MongoDB
connectDB();

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);
app.use("/api", subscribeRoute);

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Create new collection – 建立新的集合<br>Subscribe</li>



<li>Add a request – 新增請求<br>subscribe、POST、http://localhost:9000/api/subscribe</li>



<li>Body > raw</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Subscribe/subscribe
// Body &gt; raw
{
    "email": "hi@example.com"
}
</code></pre>



<h2 class="wp-block-heading">製作用戶管理功能 (管理員)</h2>



<pre class="wp-block-code"><code>// backend/routes/adminRoutes.js
const express = require("express");
const User = require("../models/User");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route GET /api/admin/users - @路由、使用 GET 方法、API 的路徑
// @desc Get all users (Admin only) - @描述 取得所有用戶 (僅限管理員)
// @access Private/Admin - @訪問權限 私人/管理員
router.get("/", protect, admin, async (req, res) =&gt; {
  try {
    const users = await User.find({});
    res.json(users);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route POST /api/admin/users - @路由、使用 POST 方法、API 的路徑
// @desc Add a new user (admin only) - @描述 新增用戶 (僅限管理員)
// @access Private/Admin - @訪問權限 私人/管理員
router.post("/", protect, admin, async (req, res) =&gt; {
  const { name, email, password, role } = req.body;

  try {
    let user = await User.findOne({ email });
    if (user) {
      return res.status(400).json({ message: "User already exists" });
    }

    user = new User({
      name,
      email,
      password,
      role: role || "customer",
    });

    await user.save();
    res.status(201).json({ message: "User created successfully", user });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route PUT /api/admin/users/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update user info (admin only) - Name, email and role - @描述 更新用戶資訊 (僅限管理員) - 姓名、電子郵件和角色
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const user = await User.findById(req.params.id);
    if (user) {
      user.name = req.body.name || user.name;
      user.email = req.body.email || user.email;
      user.role = req.body.role || user.role;
    }
    const updatedUser = await user.save();
    res.json({ message: "User updated successfully", user: updatedUser });
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route DELETE /api/admin/users/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete a user - @描述 刪除用戶
// @access Private/Admin - @訪問權限 私人/管理員
router.delete("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const user = await User.findById(req.params.id);
    if (user) {
      await user.deleteOne();
      res.json({ message: "User deleted successfully" });
    } else {
      res.status(404).json({ message: "User not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");
const uploadRoutes = require("./routes/uploadRoutes");
const subscribeRoute = require("./routes/subscribeRoute");
const adminRoutes = require("./routes/adminRoutes");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

// Connect to MongoDB - 連接到 MongoDB
connectDB();

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);
app.use("/api", subscribeRoute);

// Admin - 管理員
app.use("/api/admin/users", adminRoutes);

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Create new collection – 建立新的集合<br>Admin User</li>



<li>Add a request – 新增請求<br>All users、GET、http://localhost:9000/api/admin/users<br>Create User、POST、http://localhost:9000/api/admin/users</li>



<li>Headers > Key、Value</li>



<li>Body > raw</li>



<li>Duplicate – 複製 Create User 請求<br>Update User、PUT、http://localhost:9000/api/admin/users/:id</li>



<li>Headers > Key、Value</li>



<li>Body > raw</li>



<li>複製 MongoDB Atlas 使用者的 _id</li>



<li>Params > Path Variables > Key、Value</li>



<li>Duplicate – 複製 Update User 請求<br>Delete User、DELETE、http://localhost:9000/api/admin/users/:id</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Admin User/All users
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<pre class="wp-block-code"><code>// Postman - Admin User/Create User
// Body &gt; raw
{
    "name": "Jimmy",
    "email": "jimmy@example.com",
    "password": "123456"
}
</code></pre>



<pre class="wp-block-code"><code>// Postman - Admin User/Update User
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<pre class="wp-block-code"><code>// Postman - Admin User/Update User
// Body &gt; raw
{
    "name": "Jimmy1",
    "email": "jimmy1@example.com"
}
</code></pre>



<pre class="wp-block-code"><code>// Postman - Admin User/Update User
// Params &gt; Path Variables &gt; Key、Value
Key: id
Value: 67d8eafb4e9e9ace1c78eb25
</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas</h2>



<ul class="wp-block-list">
<li>Browse Collection (瀏覽集合) > users</li>
</ul>



<h2 class="wp-block-heading">製作列出所有產品功能 (管理員)</h2>



<pre class="wp-block-code"><code>// backend/routes/productAdminRoutes.js
const express = require("express");
const Product = require("../models/Product");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route GET /api/admin/products - @路由、使用 GET 方法、API 的路徑
// @desc Get all products (Admin only) - @描述 取得所有產品 (僅限管理員)
// @access Private/Admin - @訪問權限 私人/管理員
router.get("/", protect, admin, async (req, res) =&gt; {
  try {
    const products = await Product.find({});
    res.json(products);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");
const uploadRoutes = require("./routes/uploadRoutes");
const subscribeRoute = require("./routes/subscribeRoute");
const adminRoutes = require("./routes/adminRoutes");
const productAdminRoutes = require("./routes/productAdminRoutes");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

// Connect to MongoDB - 連接到 MongoDB
connectDB();

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);
app.use("/api", subscribeRoute);

// Admin - 管理員
app.use("/api/admin/users", adminRoutes);
app.use("/api/admin/products", productAdminRoutes);

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Create new collection – 建立新的集合<br>Admin Product</li>



<li>Add a request – 新增請求<br>Products、GET、http://localhost:9000/api/admin/products</li>



<li>Headers > Key、Value</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Admin Product/Products
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<h2 class="wp-block-heading">製作訂單管理功能 (管理員)</h2>



<pre class="wp-block-code"><code>// backend/routes/adminOrderRoutes.js
const express = require("express");
const Order = require("../models/Order");
const { protect, admin } = require("../middleware/authMiddleware");

const router = express.Router();

// @route GET /api/admin/orders - @路由、使用 GET 方法、API 的路徑
// @desc Get all order (Admin only) - @描述 取得所有訂單 (僅限管理員)
// @access Private/Admin - @訪問權限 私人/管理員
router.get("/", protect, admin, async (req, res) =&gt; {
  try {
    const orders = await Order.find({}).populate("user", "name email");
    res.json(orders);
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route PUT /api/admin/orders/:id - @路由、使用 PUT 方法、API 的路徑
// @desc Update order status - @描述 更新訂單狀態
// @access Private/Admin - @訪問權限 私人/管理員
router.put("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const order = await Order.findById(req.params.id);
    if (order) {
      order.status = req.body.status || order.status;
      order.isDelivered =
        req.body.status === "Delivered" ? true : order.isDelivered;
      order.deliveredAt =
        req.body.status === "Delivered" ? Date.now() : order.deliveredAt;

      const updatedOrder = await order.save();
      res.json(updatedOrder);
    } else {
      res.status(404).json({ message: "Order not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

// @route DELETE /api/admin/orders/:id - @路由、使用 DELETE 方法、API 的路徑
// @desc Delete an order - @描述 刪除訂單
// @access Private/Admin - @訪問權限 私人/管理員
router.delete("/:id", protect, admin, async (req, res) =&gt; {
  try {
    const order = await Order.findById(req.params.id);
    if (order) {
      await order.deleteOne();
      res.json({ message: "Order removed" });
    } else {
      res.status(404).json({ message: "Order not found" });
    }
  } catch (error) {
    console.error(error);
    res.status(500).json({ message: "Server Error" });
  }
});

module.exports = router;
</code></pre>



<pre class="wp-block-code"><code>// backend/server.js
const express = require("express");
const cors = require("cors");
const dotenv = require("dotenv");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const productRoutes = require("./routes/productRoutes");
const cartRoutes = require("./routes/cartRoutes");
const checkoutRoutes = require("./routes/checkoutRoutes");
const orderRoutes = require("./routes/orderRoutes");
const uploadRoutes = require("./routes/uploadRoutes");
const subscribeRoute = require("./routes/subscribeRoute");
const adminRoutes = require("./routes/adminRoutes");
const productAdminRoutes = require("./routes/productAdminRoutes");
const adminOrderRoutes = require("./routes/adminOrderRoutes");

const app = express();
app.use(express.json());
app.use(cors());

dotenv.config();

// console.log(process.env.PORT);
const PORT = process.env.PORT || 3000;

// Connect to MongoDB - 連接到 MongoDB
connectDB();

app.get("/", (req, res) =&gt; {
  res.send("WELCOME TO RABBIT API!");
});

// API Routes - API 路由
app.use("/api/users", userRoutes);
app.use("/api/products", productRoutes);
app.use("/api/cart", cartRoutes);
app.use("/api/checkout", checkoutRoutes);
app.use("/api/orders", orderRoutes);
app.use("/api/upload", uploadRoutes);
app.use("/api", subscribeRoute);

// Admin - 管理員
app.use("/api/admin/users", adminRoutes);
app.use("/api/admin/products", productAdminRoutes);
app.use("/api/admin/orders", adminOrderRoutes);

app.listen(PORT, () =&gt; {
  console.log(`Server is running on 
});
</code></pre>



<h2 class="wp-block-heading">使用 Postman API 測試工具</h2>



<ul class="wp-block-list">
<li>Create new collection – 建立新的集合<br>Admin Orders</li>



<li>Add a request – 新增請求<br>Orders、GET、http://localhost:9000/api/admin/orders</li>



<li>Headers > Key、Value</li>



<li>Duplicate – 複製 Orders 請求<br>Update Status、PUT、http://localhost:9000/api/admin/orders/:id</li>



<li>複製 MongoDB Atlas 訂單的 _id</li>



<li>Params > Path Variables > Key、Value</li>



<li>Body > raw<br>測試修改狀態 Processing</li>



<li>Duplicate – 複製 Update Status 請求<br>Delete、DELETE、http://localhost:9000/api/admin/orders/:id</li>



<li>複製 MongoDB Atlas 訂單的 _id<br>測試刪除訂單</li>
</ul>



<pre class="wp-block-code"><code>// Postman - Admin Orders/Orders
// Headers
Key: Authorization
Value: Bearer 你的Token
</code></pre>



<pre class="wp-block-code"><code>// Postman - Admin Orders/Update Status
// Params &gt; Path Variables &gt; Key、Value
Key: id
Value: 67d789fd320689ef987bfada
</code></pre>



<pre class="wp-block-code"><code>// Postman - Admin Orders/Update Status
// Body &gt; raw
{
    "status": "Delivered"
}
</code></pre>



<h2 class="wp-block-heading">查看 MongoDB Atlas 資料庫</h2>



<ul class="wp-block-list">
<li>Browse collections (瀏覽集合) > orders<br>查看是否有刪除訂單</li>
</ul>



<p>以上完成這個專案後端的部分。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Build &#038; Deploy Full Stack E-commerce Website &#124; Redux &#124; MERN Stack – 01</title>
		<link>/wordpress_blog/full-stack-rabbit-01/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Tue, 04 Mar 2025 04:10:19 +0000</pubDate>
				<category><![CDATA[Youtube]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=879</guid>

					<description><![CDATA[學習來自 YT:&#160;compiletab影片:&#038;nbsp [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>學習來自 YT:&nbsp;<a rel="noreferrer noopener" href="https://www.youtube.com/@compiletab" target="_blank">compiletab</a><br>影片:&nbsp;<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=hpgh2BTtac8" target="_blank">Build &amp; Deploy Full Stack E-commerce Website | Redux | MERN Stack Project</a><br>Github assets:&nbsp;<a rel="noreferrer noopener" href="https://github.com/kushald/rabbit-assets" target="_blank">連結</a><br>Thank you, teacher</p>



<h1 class="wp-block-heading">建立 &amp; 部署全端商業網站</h1>



<h2 class="wp-block-heading">影片時間戳</h2>



<p>00:00:00 – Introduction<br>00:01:56 – Demo<br>00:05:26 – Installation &amp; set up<br>05:50:25 – Admin UI<br>07:20:25 – Backend setup<br>07:32:40 – user routes<br>08:05:40 – Products routes<br>09:18:56 – cart routes<br>10:13:26 – checkout routes<br>10:57:55 – Admin routes<br>11:58:30 – Redux</p>



<h2 class="wp-block-heading">Frontend (前端)</h2>



<h2 class="wp-block-heading">專案開始</h2>



<ol class="wp-block-list">
<li>建立 rabbit 專案資料夾</li>



<li>在 rabbit 資料夾裡面建立 frontend、backend 資料夾</li>
</ol>



<h2 class="wp-block-heading">Install Tailwind CSS with Vite (使用 Vite 安裝 Tailwind CSS)</h2>



<p><a href="https://v3.tailwindcss.com/docs/guides/vite" target="_blank" rel="noreferrer noopener">tailwindcss v3.4.17 版本文件</a></p>



<h3 class="wp-block-heading">Using React (使用 React)</h3>



<ol class="wp-block-list">
<li>Create your project (建立你的專案)</li>



<li>Install Tailwind CSS (安裝 Tailwind CSS)</li>



<li>Configure your template paths (配置你的模板路徑)</li>



<li>Add the Tailwind directives to your CSS (將 Tailwind 指令添加到你的 CSS 中)</li>



<li>Start your build process (啟動你的構建過程)</li>



<li>Start using Tailwind in your project (開始在你的專案中使用 Tailwind)</li>
</ol>



<pre class="wp-block-code"><code>// 1. Create your project
// Terminal - 終端機
npm create vite@latest my-project -- --template react
cd my-project</code></pre>



<pre class="wp-block-code"><code>// 2. Install Tailwind CSS
// Terminal - 終端機
npm install -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p</code></pre>



<pre class="wp-block-code"><code>// 3. Configure your template paths
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: &#91;
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: &#91;],
}</code></pre>



<pre class="wp-block-code"><code>// 4. Add the Tailwind directives to your CSS
// index.css
@tailwind base;
@tailwind components;
@tailwind utilities;</code></pre>



<pre class="wp-block-code"><code>// 5. Start your build process
// Terminal - 終端機
npm run dev</code></pre>



<pre class="wp-block-code"><code>// 6. Start using Tailwind in your project
// App.jsx
export default function App() {
  return (
    &lt;h1 className="text-3xl font-bold underline"&gt;
      Hello world!
    &lt;/h1&gt;
  )
}</code></pre>



<h2 class="wp-block-heading">載入 Inter Google Fonts</h2>



<ol class="wp-block-list">
<li>選擇在 index.html 載入 &lt;link></li>



<li>修正 index.css 文字樣式</li>
</ol>



<pre class="wp-block-code"><code>// 1. 選擇在 index.html 載入 &lt;link&gt;
// frontend/index.html
&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;link rel="icon" type="image/svg+xml" href="/vite.svg" /&gt;
    &lt;link rel="preconnect" href="https://fonts.googleapis.com/" /&gt;
    &lt;link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin /&gt;
    &lt;link
      href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&amp;display=swap"
      rel="stylesheet"
    /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;title&gt;Rabbit E-Commerce&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id="root"&gt;&lt;/div&gt;
    &lt;script type="module" src="/src/main.jsx"&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<pre class="wp-block-code"><code>// 2. 修正 index.css 文字樣式
@tailwind base;
@tailwind components;
@tailwind utilities;

#root {
  font-family: "Inter", sans-serif;
  font-optical-sizing: auto;
  font-style: normal;
}
</code></pre>



<h2 class="wp-block-heading">安裝 React Router、React Icons 套件</h2>



<pre class="wp-block-code"><code>// frontend
// terminal - 終端機
npm i react-router-dom react-icons
</code></pre>



<h2 class="wp-block-heading">探討資料夾結構</h2>



<ul class="wp-block-list">
<li>components
<ul class="wp-block-list">
<li>Admin</li>



<li>Cart</li>



<li>Common</li>



<li>Layout</li>



<li>Products</li>
</ul>
</li>



<li>pages
<ul class="wp-block-list">
<li>Home page</li>



<li>Admin Home page</li>



<li>Login page</li>



<li>Collection page</li>
</ul>
</li>
</ul>



<h2 class="wp-block-heading">建立 components、pages 資料夾</h2>



<ol class="wp-block-list">
<li>在 frontend/src 資料夾裡面建立 components、pages 資料夾</li>



<li>在 components 資料夾裡面建立 Admin、Cart、Common、Layout、Products …等資料夾</li>
</ol>



<h2 class="wp-block-heading">在 App.jsx 撰寫 React Router 程式碼內容</h2>



<ol class="wp-block-list">
<li>撰寫 React Router</li>



<li>Layout 資料夾裡面建立 UserLayout.jsx 檔案</li>
</ol>



<pre class="wp-block-code"><code>// 1. 撰寫 React Router、載入 UserLayout.jsx 檔案
// App.jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UserLayout from "./components/Layout/UserLayout";

const App = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Routes&gt;
        &lt;Route path="/" element={&lt;UserLayout /&gt;}&gt;
          {/* User Layout - 使用者佈局 */}
        &lt;/Route&gt;
        &lt;Route&gt;{/* Admin Layout - 管理員佈局 */}&lt;/Route&gt;
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default App;
</code></pre>



<pre class="wp-block-code"><code>// 2.
// frontend/src/components/Layout/UserLayout.jsx
import React from "react";

const UserLayout = () =&gt; {
  return &lt;div&gt;UserLayout&lt;/div&gt;;
};

export default UserLayout;
</code></pre>



<h2 class="wp-block-heading">UserLayout 頁面使用者介面設計草圖</h2>



<h2 class="wp-block-heading">UserLayout 建立、撰寫使用者介面相關程式碼</h2>



<pre class="wp-block-code"><code>// frontend/src/components/Layout/UserLayout.jsx
import React from "react";
import Header from "../Common/Header";

const UserLayout = () =&gt; {
  return (
    &lt;&gt;
      {/* Header */}
      &lt;Header /&gt;
      {/* Main content */}
      {/* Footer */}
    &lt;/&gt;
  );
};

export default UserLayout;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Common/Header.jsx
import React from "react";
import Topbar from "../Layout/Topbar";
import Navbar from "./Navbar";

const Header = () =&gt; {
  return (
    &lt;header className="border-b border-gray-200"&gt;
      {/* Topbar - 頂部欄 */}
      &lt;Topbar /&gt;
      {/* Navbar - 導覽列 */}
      &lt;Navbar /&gt;
      {/* Cart Drawer 購物車抽屜 */}
    &lt;/header&gt;
  );
};

export default Header;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Layout/Topbar.jsx
import React from "react";
import { TbBrandMeta } from "react-icons/tb";
import { IoLogoInstagram } from "react-icons/io";
import { RiTwitterXLine } from "react-icons/ri";

const Topbar = () =&gt; {
  return (
    &lt;div className="bg-&#91;#ea2e0e] text-white"&gt;
      &lt;div className="container mx-auto flex justify-between items-center py-3 px-4"&gt;
        &lt;div className="hidden md:flex items-center space-x-4"&gt;
          &lt;a href="#" className="hover:text-gray-300"&gt;
            &lt;TbBrandMeta className="h-5 w-5" /&gt;
          &lt;/a&gt;
          &lt;a href="#" className="hover:text-gray-300"&gt;
            &lt;IoLogoInstagram className="h-5 w-5" /&gt;
          &lt;/a&gt;
          &lt;a href="#" className="hover:text-gray-300"&gt;
            &lt;RiTwitterXLine className="h-4 w-4" /&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div className="text-sm text-center flex-grow"&gt;
          &lt;span&gt;We ship worldwide - Fast and reliable shipping!&lt;/span&gt;
        &lt;/div&gt;
        &lt;div className="text-sm hidden md:block"&gt;
          &lt;a href="tel:+1234567890" className="hover:text-gray-300"&gt;
            +1 (234) 567-890
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Topbar;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Common/Navbar.jsx
import React from "react";
import { Link } from "react-router-dom";
import {
  HiOutlineUser,
  HiOutlineShoppingBag,
  HiBars3BottomRight,
} from "react-icons/hi2";
import SearchBar from "./SearchBar";

const Navbar = () =&gt; {
  return (
    &lt;&gt;
      &lt;nav className="container mx-auto flex items-center justify-between py-4 px-6"&gt;
        {/* Left - Logo -&gt; 左側 - 商標、標誌 */}
        &lt;div&gt;
          &lt;Link to="/" className="text-2xl font-medium"&gt;
            Rabbit
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Center - Navigation Links -&gt; 中間 - 導覽連結 */}
        &lt;div className="hidden md:flex space-x-6"&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Men
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Women
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Top Wear
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Bottom Wear
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Right - Icons -&gt; 右側 - 圖示 */}
        &lt;div className="flex items-center space-x-4"&gt;
          &lt;Link to="/profile" className="hover:text-black"&gt;
            &lt;HiOutlineUser className="h-6 w-6 text-gray-700" /&gt;
          &lt;/Link&gt;
          &lt;button className="relative hover:text-black"&gt;
            &lt;HiOutlineShoppingBag className="h-6 w-6 text-gray-700" /&gt;
            &lt;span className="absolute -top-1 bg-rabbit-red text-white text-xs rounded-full px-2 py-0.5"&gt;
              4
            &lt;/span&gt;
          &lt;/button&gt;
          {/* Search - 搜尋 */}
          &lt;div className="overflow-hidden"&gt;
            &lt;SearchBar /&gt;
          &lt;/div&gt;

          &lt;button className="md:hidden"&gt;
            &lt;HiBars3BottomRight className="h-6 w-6 text-gray-700" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/nav&gt;
    &lt;/&gt;
  );
};

export default Navbar;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Common/SearchBar.jsx
import React, { useState } from "react";
import { HiMagnifyingGlass, HiMiniXMark } from "react-icons/hi2";

const SearchBar = () =&gt; {
  const &#91;searchTerm, setSearchTerm] = useState("");
  const &#91;isOpen, setIsOpen] = useState(false);

  const handleSearchToggle = () =&gt; {
    setIsOpen(!isOpen);
  };

  const handleSearch = (e) =&gt; {
    e.preventDefault();
    console.log("Search Term:", searchTerm);
    setIsOpen(false);
  };

  return (
    &lt;div
      className={`flex items-center justify-center w-full transition-all duration-300 ${
        isOpen ? "absolute top-0 left-0 w-full bg-white h-24 z-50" : "w-auto"
      }`}
    &gt;
      {isOpen ? (
        &lt;form
          onSubmit={handleSearch}
          className="relative flex items-center justify-center w-full"
        &gt;
          &lt;div className="relative w-1/2"&gt;
            &lt;input
              type="text"
              placeholder="Search"
              value={searchTerm}
              onChange={(e) =&gt; setSearchTerm(e.target.value)}
              className="bg-gray-100 px-4 py-2 pl-2 pr-12 rounded-lg focus:outline-none w-full placeholder:text-gray-700"
            /&gt;
            {/* search icon - 搜尋圖示 */}
            &lt;button
              type="submit"
              className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-800"
            &gt;
              &lt;HiMagnifyingGlass className="h-6 w-6" /&gt;
            &lt;/button&gt;
          &lt;/div&gt;
          {/* close button - 關閉的按鈕 */}
          &lt;button
            type="button"
            onClick={handleSearchToggle}
            className="absolute right-4 top-1/2 transform -translate-y-1/2 text-gray-600 hover:text-gray-800"
          &gt;
            &lt;HiMiniXMark className="h-6 w-6" /&gt;
          &lt;/button&gt;
        &lt;/form&gt;
      ) : (
        &lt;button onClick={handleSearchToggle}&gt;
          &lt;HiMagnifyingGlass className="h-6 w-6" /&gt;
        &lt;/button&gt;
      )}
    &lt;/div&gt;
  );
};

export default SearchBar;
</code></pre>



<h2 class="wp-block-heading">客製化 tailwind.config.js 樣式</h2>



<pre class="wp-block-code"><code>// frontend/tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: &#91;"./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {
      colors: {
        "rabbit-red": "#ea2e0e",
      },
    },
  },
  plugins: &#91;],
};
</code></pre>



<h2 class="wp-block-heading">製作購物車抽屜、購物車內容組件、手機版導覽</h2>



<pre class="wp-block-code"><code>// frontend/src/components/Layout/CartDrawer.jsx
import React, { useState } from "react";
import { IoMdClose } from "react-icons/io";
import CartContents from "../Cart/CartContents";

const CartDrawer = ({ drawerOpen, toggleCartDrawer }) =&gt; {
  return (
    &lt;div
      className={`fixed top-0 right-0 w-3/4 sm:w-1/2 md:w-&#91;30rem] h-full bg-white shadow-lg transform transition-transform duration-300 flex flex-col z-50 ${
        drawerOpen ? "translate-x-0" : "translate-x-full"
      }`}
    &gt;
      {/* Close Button - 關閉的按鈕 */}
      &lt;div className="flex justify-end p-4"&gt;
        &lt;button onClick={toggleCartDrawer}&gt;
          &lt;IoMdClose className="h-6 w-6 text-gray-600" /&gt;
        &lt;/button&gt;
      &lt;/div&gt;
      {/* Cart contents with scrollable area - 具有可滾動區域的購物車內容 */}
      &lt;div className="flex-grow p-4 overflow-y-auto"&gt;
        &lt;h2 className="text-xl font-semibold mb-4"&gt;Your Cart&lt;/h2&gt;
        {/* Component for Cart Contents - 購物車內容組件 */}
        &lt;CartContents /&gt;
      &lt;/div&gt;

      {/* Checkout button fixed at the bottom - 結帳按鈕固定在底部 */}
      &lt;div className="p-4 bg-white sticky bottom-0"&gt;
        &lt;button className="w-full bg-black text-white py-3 rounded-lg font-semibold hover:bg-gray-800 transition"&gt;
          Checkout
        &lt;/button&gt;
        &lt;p className="text-sm tracking-tighter text-gray-500 mt-2 text-center"&gt;
          Shipping, taxes, and discount codes calculated at checkout.
        &lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default CartDrawer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Common/Navbar.jsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import {
  HiOutlineUser,
  HiOutlineShoppingBag,
  HiBars3BottomRight,
} from "react-icons/hi2";
import SearchBar from "./SearchBar";
import CartDrawer from "../Layout/CartDrawer";
import { IoMdClose } from "react-icons/io";

const Navbar = () =&gt; {
  const &#91;drawerOpen, setDrawerOpen] = useState(false);
  const &#91;navDrawerOpen, setNavDrawerOpen] = useState(false);

  const toggleNavDrawer = () =&gt; {
    setNavDrawerOpen(!navDrawerOpen);
  };

  const toggleCartDrawer = () =&gt; {
    setDrawerOpen(!drawerOpen);
  };

  return (
    &lt;&gt;
      &lt;nav className="container mx-auto flex items-center justify-between py-4 px-6"&gt;
        {/* Left - Logo -&gt; 左側 - 商標、標誌 */}
        &lt;div&gt;
          &lt;Link to="/" className="text-2xl font-medium"&gt;
            Rabbit
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Center - Navigation Links -&gt; 中間 - 導覽連結 */}
        &lt;div className="hidden md:flex space-x-6"&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Men
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Women
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Top Wear
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Bottom Wear
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Right - Icons -&gt; 右側 - 圖示 */}
        &lt;div className="flex items-center space-x-4"&gt;
          &lt;Link to="/profile" className="hover:text-black"&gt;
            &lt;HiOutlineUser className="h-6 w-6 text-gray-700" /&gt;
          &lt;/Link&gt;
          &lt;button
            onClick={toggleCartDrawer}
            className="relative hover:text-black"
          &gt;
            &lt;HiOutlineShoppingBag className="h-6 w-6 text-gray-700" /&gt;
            &lt;span className="absolute -top-1 bg-rabbit-red text-white text-xs rounded-full px-2 py-0.5"&gt;
              4
            &lt;/span&gt;
          &lt;/button&gt;
          {/* Search - 搜尋 */}
          &lt;div className="overflow-hidden"&gt;
            &lt;SearchBar /&gt;
          &lt;/div&gt;

          &lt;button onClick={toggleNavDrawer} className="md:hidden"&gt;
            &lt;HiBars3BottomRight className="h-6 w-6 text-gray-700" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/nav&gt;
      &lt;CartDrawer drawerOpen={drawerOpen} toggleCartDrawer={toggleCartDrawer} /&gt;

      {/* Mobile Navigation - 手機版導覽 */}
      &lt;div
        className={`fixed top-0 left-0 w-3/4 sm:w-1/2 md:w-1/3 h-full bg-white shadow-lg transform transition-transform duration-300 z-50 ${
          navDrawerOpen ? "translate-x-0" : "-translate-x-full"
        }`}
      &gt;
        &lt;div className="flex justify-end p-4"&gt;
          &lt;button onClick={toggleNavDrawer}&gt;
            &lt;IoMdClose className="h-6 w-6 text-gray-600" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
        &lt;div className="p-4"&gt;
          &lt;h2 className="text-xl font-semibold mb-4"&gt;Menu&lt;/h2&gt;
          &lt;nav className="space-y-4"&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Men
            &lt;/Link&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Women
            &lt;/Link&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Top Wear
            &lt;/Link&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Bottom Wear
            &lt;/Link&gt;
          &lt;/nav&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default Navbar;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Cart/CartContents.jsx
import React from "react";
import { RiDeleteBin3Line } from "react-icons/ri";

const CartContents = () =&gt; {
  const cartProducts = &#91;
    {
      productId: 1,
      name: "T-shirt",
      size: "M",
      color: "Red",
      quantity: 1,
      price: 15,
      image: "https://picsum.photos/200?random=1",
    },
    {
      productId: 2,
      name: "Jeans",
      size: "L",
      color: "Blue",
      quantity: 1,
      price: 25,
      image: "https://picsum.photos/200?random=2",
    },
  ];

  return (
    &lt;div&gt;
      {cartProducts.map((product, index) =&gt; (
        &lt;div
          key={index}
          className="flex items-start justify-between py-4 border-b"
        &gt;
          &lt;div className="flex items-start"&gt;
            &lt;img
              src={product.image}
              alt={product.name}
              className="w-20 h-24 object-cover mr-4 rounded"
            /&gt;
            &lt;div&gt;
              &lt;h3&gt;{product.name}&lt;/h3&gt;
              &lt;p className="text-sm text-gray-500"&gt;
                size: {product.size} | color: {product.color}
              &lt;/p&gt;
              &lt;div className="flex items-center mt-2"&gt;
                &lt;button className="border rounded px-2 py-1 text-xl font-medium"&gt;
                  -
                &lt;/button&gt;
                &lt;span className="mx-4"&gt;{product.quantity}&lt;/span&gt;
                &lt;button className="border rounded px-2 py-1 text-xl font-medium"&gt;
                  +
                &lt;/button&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div&gt;
            &lt;p&gt;$ {product.price.toLocaleString()}&lt;/p&gt;
            &lt;button&gt;
              &lt;RiDeleteBin3Line className="h-6 w-6 mt-2 text-red-600" /&gt;
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      ))}
    &lt;/div&gt;
  );
};

export default CartContents;
</code></pre>



<ul class="wp-block-list">
<li><a href="https://picsum.photos/" target="_blank" rel="noreferrer noopener">Lorem Picsum</a></li>
</ul>



<h2 class="wp-block-heading">製作 Footer Section (頁尾區域)</h2>



<pre class="wp-block-code"><code>// frontend/src/components/Common/Footer.jsx
import React from "react";
import { Link } from "react-router-dom";
import { TbBrandMeta } from "react-icons/tb";
import { IoLogoInstagram } from "react-icons/io";
import { RiTwitterXLine } from "react-icons/ri";
import { FiPhoneCall } from "react-icons/fi";

const Footer = () =&gt; {
  return (
    &lt;footer className="border-t py-12"&gt;
      &lt;div className="container mx-auto grid grid-cols-1 md:grid-cols-4 gap-8 px-4 lg:px-0"&gt;
        &lt;div&gt;
          &lt;h3 className="text-lg text-gray-800 mb-4"&gt;Newsletter&lt;/h3&gt;
          &lt;p className="text-gray-500 mb-4"&gt;
            Be the first to hear about new products, exclusive events, and
            online offers.
          &lt;/p&gt;
          &lt;p className="font-medium text-sm text-gray-600 mb-6"&gt;
            Sign up and get 10% off your first order.
          &lt;/p&gt;

          {/* Newsletter form */}
          &lt;form className="flex"&gt;
            &lt;input
              type="email"
              placeholder="Enter your email"
              className="p-3 w-full text-sm border-t border-l border-b border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-gray-500 transition-all"
              required
            /&gt;
            &lt;button
              type="submit"
              className="bg-black text-white px-6 py-3 text-sm rounded-r-md hover:bg-gray-800 transition-all"
            &gt;
              Subscribe
            &lt;/button&gt;
          &lt;/form&gt;
        &lt;/div&gt;

        {/* Shop links - 購物連結 */}
        &lt;div&gt;
          &lt;h3 className="text-lg text-gray-800 mb-4"&gt;Shop&lt;/h3&gt;
          &lt;ul className="space-y-2 text-gray-600"&gt;
            &lt;li&gt;
              &lt;Link to="#" className="hover:text-gray-600 transition-colors"&gt;
                Men's Top Wear
              &lt;/Link&gt;
            &lt;/li&gt;
            &lt;li&gt;
              &lt;Link to="#" className="hover:text-gray-600 transition-colors"&gt;
                Women's Top Wear
              &lt;/Link&gt;
            &lt;/li&gt;
            &lt;li&gt;
              &lt;Link to="#" className="hover:text-gray-600 transition-colors"&gt;
                Men's Bottom Wear
              &lt;/Link&gt;
            &lt;/li&gt;
            &lt;li&gt;
              &lt;Link to="#" className="hover:text-gray-600 transition-colors"&gt;
                Women's Bottom Wear
              &lt;/Link&gt;
            &lt;/li&gt;
          &lt;/ul&gt;
        &lt;/div&gt;
        {/* Support Links - 支援連結 */}
        &lt;div&gt;
          &lt;h3 className="text-lg text-gray-800 mb-4"&gt;Support&lt;/h3&gt;
          &lt;ul className="space-y-2 text-gray-600"&gt;
            &lt;li&gt;
              &lt;Link to="#" className="hover:text-gray-600 transition-colors"&gt;
                Contact Us
              &lt;/Link&gt;
            &lt;/li&gt;
            &lt;li&gt;
              &lt;Link to="#" className="hover:text-gray-600 transition-colors"&gt;
                About Us
              &lt;/Link&gt;
            &lt;/li&gt;
            &lt;li&gt;
              &lt;Link to="#" className="hover:text-gray-600 transition-colors"&gt;
                FAQs
              &lt;/Link&gt;
            &lt;/li&gt;
            &lt;li&gt;
              &lt;Link to="#" className="hover:text-gray-600 transition-colors"&gt;
                Features
              &lt;/Link&gt;
            &lt;/li&gt;
          &lt;/ul&gt;
        &lt;/div&gt;
        {/* Follow us - 追蹤我們 */}
        &lt;div&gt;
          &lt;h3 className="text-lg text-gray-800 mb-4"&gt;Follow Us&lt;/h3&gt;
          &lt;div className="flex items-center space-x-4 mb-6"&gt;
            &lt;a
              href="https://www.facebook.com/"
              target="_blank"
              rel="noopener noreferrer"
              className="hover:text-gray-500"
            &gt;
              &lt;TbBrandMeta className="h-5 w-5" /&gt;
            &lt;/a&gt;
            &lt;a
              href="https://www.instagram.com/"
              target="_blank"
              rel="noopener noreferrer"
              className="hover:text-gray-500"
            &gt;
              &lt;IoLogoInstagram className="h-5 w-5" /&gt;
            &lt;/a&gt;
            &lt;a
              href="https://x.com/"
              target="_blank"
              rel="noopener noreferrer"
              className="hover:text-gray-500"
            &gt;
              &lt;RiTwitterXLine className="h-4 w-4" /&gt;
            &lt;/a&gt;
          &lt;/div&gt;
          &lt;p className="text-gray-500"&gt;Call Us&lt;/p&gt;
          &lt;p&gt;
            &lt;FiPhoneCall className="inline-block mr-2" /&gt;
            0123-456-789
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      {/* Footer Bottom - 頁尾底部  */}
      &lt;div className="container mx-auto mt-12 px-4 lg:px-0 border-t border-gray-200 pt-6"&gt;
        &lt;p className="text-gray-500 text-sm tracking-tighter text-center"&gt;
          &amp;copy; 2025, Learning From CompileTab. All Rights Reserved.
        &lt;/p&gt;
      &lt;/div&gt;
    &lt;/footer&gt;
  );
};

export default Footer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Layout/UserLayout.jsx
import React from "react";
import Header from "../Common/Header";
import Footer from "../Common/Footer";

const UserLayout = () =&gt; {
  return (
    &lt;&gt;
      {/* Header */}
      &lt;Header /&gt;
      {/* Main content */}
      {/* Footer */}
      &lt;Footer /&gt;
    &lt;/&gt;
  );
};

export default UserLayout;
</code></pre>



<h2 class="wp-block-heading">製作 Main Content (主要內容)</h2>



<pre class="wp-block-code"><code>// frontend/src/pages/Home.jsx
import React from "react";
import Hero from "../components/Layout/Hero";
import GenderCollectionSection from "../components/Products/GenderCollectionSection";
import NewArrivals from "../components/Products/NewArrivals";
import ProductDetails from "../components/Products/ProductDetails";
import ProductGrid from "../components/Products/ProductGrid";
import FeaturedCollection from "../components/Products/FeaturedCollection";
import FeaturesSection from "../components/Products/FeaturesSection";

const placeholderProducts = &#91;
  {
    _id: 1,
    name: "Product 1",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=3",
      },
    ],
  },
  {
    _id: 2,
    name: "Product 2",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=4",
      },
    ],
  },
  {
    _id: 3,
    name: "Product 3",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=5",
      },
    ],
  },
  {
    _id: 4,
    name: "Product 4",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=6",
      },
    ],
  },
  {
    _id: 5,
    name: "Product 5",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=7",
      },
    ],
  },
  {
    _id: 6,
    name: "Product 6",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=8",
      },
    ],
  },
  {
    _id: 7,
    name: "Product 7",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=9",
      },
    ],
  },
  {
    _id: 8,
    name: "Product 8",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=10",
      },
    ],
  },
];

const Home = () =&gt; {
  return (
    &lt;div&gt;
      &lt;Hero /&gt;
      &lt;GenderCollectionSection /&gt;
      &lt;NewArrivals /&gt;

      {/* Best Seller - 暢銷商品  */}
      &lt;h2 className="text-3xl text-center font-bold mb-4"&gt;Best Seller&lt;/h2&gt;
      &lt;ProductDetails /&gt;

      &lt;div className="container mx-auto"&gt;
        &lt;h2 className="text-3xl text-center font-bold mb-4"&gt;
          Top Wears for Women
        &lt;/h2&gt;
        &lt;ProductGrid products={placeholderProducts} /&gt;
      &lt;/div&gt;

      &lt;FeaturedCollection /&gt;
      &lt;FeaturesSection /&gt;
    &lt;/div&gt;
  );
};

export default Home;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Layout/Hero.jsx
import React from "react";
import heroImg from "../../assets/rabbit-hero.webp";
import { Link } from "react-router-dom";

const Hero = () =&gt; {
  return (
    &lt;section className="relative"&gt;
      &lt;img
        src={heroImg}
        alt="Rabbit"
        className="w-full h-&#91;400px] md:h-&#91;600px] lg:h-&#91;750px] object-cover"
      /&gt;
      &lt;div className="absolute inset-0 bg-black bg-opacity-5 flex items-center justify-center"&gt;
        &lt;div className="text-center text-white p-6"&gt;
          &lt;h1 className="text-4xl md:text-9xl font-bold tracking-tighter uppercase mb-4"&gt;
            Vacation &lt;br /&gt; Ready
          &lt;/h1&gt;
          &lt;p className="text-sm tracking-tighter md:text-lg mb-6"&gt;
            Explore our vaction-ready outfits with fast worldwide shipping.
          &lt;/p&gt;
          &lt;Link
            to="#"
            className="bg-white text-gray-950 px-6 py-3 rounded-sm text-lg"
          &gt;
            Shop Now
          &lt;/Link&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  );
};

export default Hero;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UserLayout from "./components/Layout/UserLayout";
import Home from "./pages/Home";
import { Toaster } from "sonner";

const App = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Toaster position="top-right" /&gt;
      &lt;Routes&gt;
        &lt;Route path="/" element={&lt;UserLayout /&gt;}&gt;
          {/* User Layout - 使用者佈局 */}
          &lt;Route index element={&lt;Home /&gt;} /&gt;
        &lt;/Route&gt;
        &lt;Route&gt;{/* Admin Layout - 管理員佈局 */}&lt;/Route&gt;
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default App;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Layout/UserLayout.jsx
import React from "react";
import Header from "../Common/Header";
import Footer from "../Common/Footer";
import { Outlet } from "react-router-dom";

const UserLayout = () =&gt; {
  return (
    &lt;&gt;
      {/* Header */}
      &lt;Header /&gt;
      {/* Main content */}
      &lt;main&gt;
        &lt;Outlet /&gt;
      &lt;/main&gt;
      {/* Footer */}
      &lt;Footer /&gt;
    &lt;/&gt;
  );
};

export default UserLayout;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Products/GenderCollectionSection.jsx
import React from "react";
import mensCollectionImage from "../../assets/mens-collection.webp";
import womenCollectionImage from "../../assets/womens-collection.webp";
import { Link } from "react-router-dom";

const GenderCollectionSection = () =&gt; {
  return (
    &lt;section className="py-16 px-4 lg:px-0"&gt;
      &lt;div className="container mx-auto flex flex-col md:flex-row gap-8"&gt;
        {/* Women's Collection - 女裝系列 */}
        &lt;div className="relative flex-1"&gt;
          &lt;img
            src={womenCollectionImage}
            alt="Women's Collection"
            className="w-full h-&#91;700px] object-cover"
          /&gt;
          &lt;div className="absolute bottom-8 left-8 bg-white bg-opacity-90 p-4"&gt;
            &lt;h2 className="text-2xl font-bold text-gray-900 mb-3"&gt;
              Women's Collection
            &lt;/h2&gt;
            &lt;Link
              to="/collections/all?gender=Women"
              className="text-gray-900 underline"
            &gt;
              Shop Now
            &lt;/Link&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        {/* Men's Collection - 男裝系列 */}
        &lt;div className="relative flex-1"&gt;
          &lt;img
            src={mensCollectionImage}
            alt="Men's Collection"
            className="w-full h-&#91;700px] object-cover"
          /&gt;
          &lt;div className="absolute bottom-8 left-8 bg-white bg-opacity-90 p-4"&gt;
            &lt;h2 className="text-2xl font-bold text-gray-900 mb-3"&gt;
              Men's Collection
            &lt;/h2&gt;
            &lt;Link
              to="/collections/all?gender=Men"
              className="text-gray-900 underline"
            &gt;
              Shop Now
            &lt;/Link&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  );
};

export default GenderCollectionSection;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Products/NewArrivals.jsx
import React, { useEffect, useRef, useState } from "react";
import { FiChevronLeft, FiChevronRight } from "react-icons/fi";
import { Link } from "react-router-dom";

const NewArrivals = () =&gt; {
  const scrollRef = useRef(null); // 用於引用滾動容器的 DOM 元素
  const &#91;isDragging, setIsDragging] = useState(false);
  const &#91;startX, setStartX] = useState(0);
  // const &#91;scrollLeft, setScrollLeft] = useState(false);
  // scrollLeft 是用來儲存滾動容器水平方向的偏移量，它應該是數字型態而非布林值。
  const &#91;scrollLeft, setScrollLeft] = useState(0);
  const &#91;canScrollLeft, setCanScrollLeft] = useState(false);
  const &#91;canScrollRight, setCanScrollRight] = useState(true);

  const newArrivals = &#91;
    {
      _id: "1",
      name: "Stylish Jacket",
      price: 120,
      images: &#91;
        {
          url: "https://picsum.photos/500/500?random=1",
          altText: "Stylish Jacket",
        },
      ],
    },
    {
      _id: "2",
      name: "Stylish Jacket",
      price: 120,
      images: &#91;
        {
          url: "https://picsum.photos/500/500?random=2",
          altText: "Stylish Jacket",
        },
      ],
    },
    {
      _id: "3",
      name: "Stylish Jacket",
      price: 120,
      images: &#91;
        {
          url: "https://picsum.photos/500/500?random=3",
          altText: "Stylish Jacket",
        },
      ],
    },
    {
      _id: "4",
      name: "Stylish Jacket",
      price: 120,
      images: &#91;
        {
          url: "https://picsum.photos/500/500?random=4",
          altText: "Stylish Jacket",
        },
      ],
    },
    {
      _id: "5",
      name: "Stylish Jacket",
      price: 120,
      images: &#91;
        {
          url: "https://picsum.photos/500/500?random=5",
          altText: "Stylish Jacket",
        },
      ],
    },
    {
      _id: "6",
      name: "Stylish Jacket",
      price: 120,
      images: &#91;
        {
          url: "https://picsum.photos/500/500?random=6",
          altText: "Stylish Jacket",
        },
      ],
    },
    {
      _id: "7",
      name: "Stylish Jacket",
      price: 120,
      images: &#91;
        {
          url: "https://picsum.photos/500/500?random=7",
          altText: "Stylish Jacket",
        },
      ],
    },
    {
      _id: "8",
      name: "Stylish Jacket",
      price: 120,
      images: &#91;
        {
          url: "https://picsum.photos/500/500?random=8",
          altText: "Stylish Jacket",
        },
      ],
    },
  ];

  const handleMouseDown = (e) =&gt; {
    setIsDragging(true); // 開始拖曳
    setStartX(e.pageX - scrollRef.current.offsetLeft); // 記錄滑鼠按下時的 X 座標，減去容器的偏移量
    setScrollLeft(scrollRef.current.scrollLeft); // 記錄滾動容器當前的 scrollLeft (滾動位置)
  };

  const handleMouseMove = (e) =&gt; {
    if (!isDragging) return; // 如果沒有正在拖曳，則不進行後續操作
    const x = e.pageX - scrollRef.current.offsetLeft; // 計算滑鼠相對於容器的 X 座標
    const walk = x - startX; // 計算滑鼠移動的距離
    scrollRef.current.scrollLeft = scrollLeft - walk; // 根據滑鼠移動的距離更新滾動位置
  };

  const handleMouseUpOrLeave = () =&gt; {
    setIsDragging(false); // 停止拖曳，將 isDraggin 設為 false
  };

  const scroll = (direction) =&gt; {
    const scrollAmount = direction === "left" ? -300 : 300; // 根據方向決定滾動的距離
    scrollRef.current.scrollBy({ left: scrollAmount, behaviour: "smooth" }); // 進行平滑滾動
  };

  // Update Scroll Buttons - 更新滾動的按鈕
  const updateScrollButtons = () =&gt; {
    const container = scrollRef.current; // 取得滾動容器的引用

    if (container) {
      const leftScroll = container.scrollLeft; // 取得當前滾動容器的滾動位置 (距離左邊的偏移量)
      const rightScrollable =
        container.scrollWidth &gt; leftScroll + container.clientWidth; // 檢查是否還有內容可以向右滾動

      setCanScrollLeft(leftScroll &gt; 0); // 如果滾動位置大於 0，則可以向左滾動
      setCanScrollRight(rightScrollable); // 如果總容器寬度大於當前滾動位置加上可視範圍，則可以向右滾動
    }

    // 用來調試，輸出滾動容器的一些狀態信息
    // console.log({
    //   scrollLeft: container.scrollLeft, // 當前滾動位置
    //   clientWidth: container.clientWidth, // 容器的可見寬度
    //   containerScrollWidth: container.scrollWidth, // 容器內容的總寬度
    //   offsetLeft: scrollRef.current.offsetLeft, // 滾動容器相對於頁面左邊的偏移量
    // });
  };

  useEffect(() =&gt; {
    const container = scrollRef.current; // 取得滾動容器的 DOM 元素
    if (container) {
      // 如果容器存在，則添加滾動事件監聽器
      container.addEventListener("scroll", updateScrollButtons);

      // 呼叫一次 updateScrollButtons 來初始化滾動按鈕狀態
      updateScrollButtons();

      // 返回清理函數，在組件卸載時移除滾動事件監聽器
      return () =&gt; container.removeEventListener("scroll", updateScrollButtons);
    }
  }, &#91;]);

  return (
    &lt;section className="py-16 px-4 lg:px-0"&gt;
      &lt;div className="container mx-auto text-center mb-10 relative"&gt;
        &lt;h2 className="text-3xl font-bold mb-4"&gt;Explore New Arrivals&lt;/h2&gt;
        &lt;p className="text-lg text-gray-600 mb-8"&gt;
          Discover the latest styles straight off the runway, freshly added to
          keep your wardrobe on the cutting edge of fashion.
        &lt;/p&gt;

        {/* Scroll Buttons - 滾動按鈕 */}
        &lt;div className="absolute right-0 bottom-&#91;-30px] flex space-x-2"&gt;
          &lt;button
            onClick={() =&gt; scroll("left")}
            disabled={!canScrollLeft}
            className={`p-2 rounded border ${
              canScrollLeft
                ? "bg-white text-black"
                : "bg-gray-200 text-gray-400 cursor-not-allowed"
            }`}
          &gt;
            &lt;FiChevronLeft className="text-2xl" /&gt;
          &lt;/button&gt;
          &lt;button
            onClick={() =&gt; scroll("right")}
            className={`p-2 rounded border ${
              canScrollRight
                ? "bg-white text-black"
                : "bg-gray-200 text-gray-400 cursor-not-allowed"
            }`}
          &gt;
            &lt;FiChevronRight className="text-2xl" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      {/* Scrollable Content - 可滾動內容 */}
      &lt;div
        ref={scrollRef}
        className={`container mx-auto overflow-x-scroll flex space-x-6 relative ${
          isDragging ? "cursor-grabbing" : "cursor-grab"
        }`}
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUpOrLeave}
        onMouseLeave={handleMouseUpOrLeave}
      &gt;
        {newArrivals.map((product) =&gt; (
          &lt;div
            key={product._id}
            className="min-w-&#91;100%] sm:min-w-&#91;50%] lg:min-w-&#91;30%] relative"
          &gt;
            &lt;img
              src={product.images&#91;0]?.url}
              alt={product.images&#91;0]?.altText || product.name}
              className="w-full h-&#91;500px] object-cover rounded-lg"
              draggable="false"
            /&gt;
            &lt;div className="absolute bottom-0 left-0 right-0 bg-opacity-50 backdrop-blur-md text-white p-4 rounded-b-lg"&gt;
              &lt;Link to={`/product/${product._id}`} className="block"&gt;
                &lt;h4 className="font-medium"&gt;{product.name}&lt;/h4&gt;
                &lt;p className="mt-1"&gt;${product.price}&lt;/p&gt;
              &lt;/Link&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;
    &lt;/section&gt;
  );
};

export default NewArrivals;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Products/ProductDetails.jsx
import React, { useEffect, useState } from "react";
import { toast } from "sonner";
import ProductGrid from "./ProductGrid";

const selectedProduct = {
  name: "Stylish Jacket",
  price: 120,
  originalPrice: 150,
  description: "This is a stylish Jacket perfect for any occasion",
  brand: "FashionBrand",
  material: "Leather",
  sizes: &#91;"S", "M", "L", "XL"],
  colors: &#91;"Red", "Black"],
  images: &#91;
    {
      url: "https://picsum.photos/500/500?random=1",
      altText: "Stylish Jacket 1",
    },
    {
      url: "https://picsum.photos/500/500?random=2",
      altText: "Stylish Jacket 2",
    },
  ],
};

const similarProducts = &#91;
  {
    _id: 1,
    name: "Product 1",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=3",
      },
    ],
  },
  {
    _id: 2,
    name: "Product 2",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=4",
      },
    ],
  },
  {
    _id: 3,
    name: "Product 3",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=5",
      },
    ],
  },
  {
    _id: 4,
    name: "Product 4",
    price: 100,
    images: &#91;
      {
        url: "https://picsum.photos/500/500?random=6",
      },
    ],
  },
];

const ProductDetails = () =&gt; {
  const &#91;mainImage, setMainImage] = useState(""); // 用來存儲主圖像的狀態，初始值為空字符串
  const &#91;selectedSize, setSelectedSize] = useState(""); // 用來存儲選擇的尺碼的狀態，初始值為空字符串
  const &#91;selectedColor, setSelectedColor] = useState(""); // 用來存儲選擇的顏色的狀態，初始值為空字符串
  const &#91;quantity, setQuantity] = useState(1); // 用來存儲選擇的購買數量的狀態，初始值為 1
  const &#91;isButtonDisabled, setIsButtonDisabled] = useState(false); // 用來控制按鈕是否禁用的狀態，初始值為 false (按鈕可用)

  // 使用 useEffect 當 selectedProduct 變化時更新主圖片
  useEffect(() =&gt; {
    // 檢查 selectedProduct 是否有圖片，並且圖片陣列長度大於 0
    if (selectedProduct?.images?.length &gt; 0) {
      // 更新主圖片為第一張圖片的 URL
      setMainImage(selectedProduct.images&#91;0].url);
    }
  }, &#91;selectedProduct]); // 當 selectedProduct 變化時執行這個效果

  // 定義一個函數來處理購買數量的增減操作
  const handleQuantityChange = (action) =&gt; {
    // 如果 action 是 "plus"，則增加數量
    if (action === "plus") setQuantity((prev) =&gt; prev + 1);
    // 如果 action 是 "minus" 且數量大於 1，則減少數量
    if (action === "minus" &amp;&amp; quantity &gt; 1) setQuantity((prev) =&gt; prev - 1);
  };

  // 定義函數處理將商品加入購物車的操作
  const handleAddToCart = () =&gt; {
    // 檢查用戶是否選擇了尺碼和顏色
    if (!selectedSize || !selectedColor) {
      // 如果沒有選擇尺碼或顏色，顯示錯誤通知
      toast.error("Please select a size and color before adding to cart.", {
        duration: 1000, // 設置錯誤通知顯示 1 秒
      });
      return; // 退出函數，不執行後續操作
    }

    // 禁用按鈕，防止重複點擊
    setIsButtonDisabled(true);

    // 設置 500 毫秒延時，模擬加入購物車過程
    setTimeout(() =&gt; {
      // 顯示成功通知，告訴用戶商品已添加到購物車
      toast.success("Product added to cart!", {
        duration: 1000, // 成功通知顯示 1 秒
      });

      // 重新啟用按鈕
      setIsButtonDisabled(false);
    }, 500); // 延時 500 毫秒
  };

  return (
    &lt;div className="p-6"&gt;
      &lt;div className="max-w-6xl mx-auto bg-white p-8 rounded-lg"&gt;
        &lt;div className="flex flex-col md:flex-row"&gt;
          {/* Left Thumbnails - 左側縮圖 */}
          &lt;div className="hidden md:flex flex-col space-y-4 mr-6"&gt;
            {selectedProduct.images.map((image, index) =&gt; (
              &lt;img
                key={index}
                src={image.url}
                alt={image.altText || `Thumbnail ${index}`}
                className={`w-20 h-20 object-cover rounded-lg cursor-pointer border ${
                  mainImage === image.url ? "border-black" : "border-gray-300"
                }`}
                onClick={() =&gt; setMainImage(image.url)}
              /&gt;
            ))}
          &lt;/div&gt;
          {/* Main Image - 主要的圖片 */}
          &lt;div className="md:w-1/2"&gt;
            &lt;div className="mb-4"&gt;
              {/* 僅當 mainImage 不是空字符串時，才渲染圖片 */}
              {mainImage &amp;&amp; (
                &lt;img
                  src={mainImage}
                  alt="Main Product"
                  className="w-full h-auto object-cover rounded-lg"
                /&gt;
              )}
            &lt;/div&gt;
          &lt;/div&gt;
          {/* Mobile Thumbnail - 手機板型縮圖 */}
          &lt;div className="md:hidden flex overflow-x-scroll space-x-4 mb-4"&gt;
            {selectedProduct.images.map((image, index) =&gt; (
              &lt;img
                key={index}
                src={image.url}
                alt={image.altText || `Thumbnail ${index}`}
                className={`w-20 h-20 object-cover rounded-lg cursor-pointer border ${
                  mainImage === image.url ? "border-black" : "border-gray-300"
                }`}
                onClick={() =&gt; setMainImage(image.url)}
              /&gt;
            ))}
          &lt;/div&gt;

          {/* Right Side - 右側 */}
          &lt;div className="md:w-1/2 md:ml-10"&gt;
            &lt;h1 className="text-2xl md:text-3xl font-semibold mb-2"&gt;
              {selectedProduct.name}
            &lt;/h1&gt;

            &lt;p className="text-lg text-gray-600 mb-1 line-through"&gt;
              {selectedProduct.originalPrice &amp;&amp;
                `${selectedProduct.originalPrice}`}
            &lt;/p&gt;
            &lt;p className="text-xl text-gray-500 mb-2"&gt;
              $ {selectedProduct.price}
            &lt;/p&gt;
            &lt;p className="text-gray-600 mb-4"&gt;{selectedProduct.description}&lt;/p&gt;

            &lt;div className="mb-4"&gt;
              &lt;p className="text-gray-700"&gt;Color:&lt;/p&gt;
              &lt;div className="flex gap-2 mt-2"&gt;
                {selectedProduct.colors.map((color) =&gt; (
                  &lt;button
                    key={color}
                    onClick={() =&gt; setSelectedColor(color)}
                    className={`w-8 h-8 rounded-full border ${
                      selectedColor === color
                        ? "border-4 border-black"
                        : "border-gray-300"
                    }`}
                    style={{
                      backgroundColor: color.toLocaleLowerCase(),
                      filter: "brightness(0.5)",
                    }}
                  &gt;&lt;/button&gt;
                ))}
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;p className="text-gray-700"&gt;Size:&lt;/p&gt;
              &lt;div className="flex gap-2 mt-2"&gt;
                {selectedProduct.sizes.map((size) =&gt; (
                  &lt;button
                    key={size}
                    onClick={() =&gt; setSelectedSize(size)}
                    className={`px-4 py-2 rounded border ${
                      selectedSize === size ? "bg-black text-white" : ""
                    }`}
                  &gt;
                    {size}
                  &lt;/button&gt;
                ))}
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;div className="mb-6"&gt;
              &lt;p className="text-gray-700"&gt;Quantity:&lt;/p&gt;
              &lt;div className="flex items-center space-x-4 mt-2"&gt;
                &lt;button
                  onClick={() =&gt; handleQuantityChange("minus")}
                  className="px-2 py-1 bg-gray-200 rounded text-lg"
                &gt;
                  -
                &lt;/button&gt;
                &lt;span className="text-lg"&gt;{quantity}&lt;/span&gt;
                &lt;button
                  onClick={() =&gt; handleQuantityChange("plus")}
                  className="px-2 py-1 bg-gray-200 rounded text-lg"
                &gt;
                  +
                &lt;/button&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;button
              onClick={handleAddToCart}
              disabled={isButtonDisabled}
              className={`bg-black text-white py-2 px-6 rounded w-full mb-4 ${
                isButtonDisabled
                  ? "cursor-not-allowed opacity-50"
                  : "hover:bg-gray-900"
              }`}
            &gt;
              {isButtonDisabled ? "Adding..." : "ADD TO CART"}
            &lt;/button&gt;

            &lt;div className="mt-10 text-gray-700"&gt;
              &lt;h3 className="text-xl font-bold mb-4"&gt;Characteristics:&lt;/h3&gt;
              &lt;table className="w-full text-left text-sm text-gray-600"&gt;
                &lt;tbody&gt;
                  &lt;tr&gt;
                    &lt;td className="py-1"&gt;Brand&lt;/td&gt;
                    &lt;td className="py-1"&gt;{selectedProduct.brand}&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                    &lt;td className="py-1"&gt;Material&lt;/td&gt;
                    &lt;td className="py-1"&gt;{selectedProduct.material}&lt;/td&gt;
                  &lt;/tr&gt;
                &lt;/tbody&gt;
              &lt;/table&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div className="mt-20"&gt;
          &lt;h2 className="text-2xl text-center font-medium mb-4"&gt;
            You May Also Like
          &lt;/h2&gt;
          &lt;ProductGrid products={similarProducts} /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default ProductDetails;
</code></pre>



<h3 class="wp-block-heading">使用 Sonner 套件</h3>



<p>An opinionated toast component for React.Render a toast</p>



<ul class="wp-block-list">
<li><a href="https://sonner.emilkowal.ski/" target="_blank" rel="noreferrer noopener">Sonner</a></li>
</ul>



<pre class="wp-block-code"><code>// frontend/src/components/Products/ProductGrid.jsx
import React from "react";
import { Link } from "react-router-dom";

const ProductGrid = ({ products }) =&gt; {
  return (
    &lt;div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"&gt;
      {products.map((product, index) =&gt; (
        &lt;Link key={index} to={`/product/${product._id}`} className="block"&gt;
          &lt;div className="bg-white p-4 rounded-lg"&gt;
            &lt;div className="w-full h-96 mb-4"&gt;
              &lt;img
                src={product.images&#91;0].url}
                alt={product.images&#91;0].altText || product.name}
                className="w-full h-full object-cover rounded-lg"
              /&gt;
            &lt;/div&gt;
            &lt;h3 className="text-sm mb-2"&gt;{product.name}&lt;/h3&gt;
            &lt;p className="text-gray-500 font-medium text-sm tracking-tighter"&gt;
              $ {product.price}
            &lt;/p&gt;
          &lt;/div&gt;
        &lt;/Link&gt;
      ))}
    &lt;/div&gt;
  );
};

export default ProductGrid;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Products/FeaturedCollection.jsx
import React from "react";
import { Link } from "react-router-dom";
import featured from "../../assets/featured.webp";

const FeaturedCollection = () =&gt; {
  return (
    &lt;section className="py-16 px-4 lg:px-0"&gt;
      &lt;div className="container mx-auto flex flex-col-reverse lg:flex-row items-center bg-green-50 rounded-3xl"&gt;
        {/* Left Content - 左邊內容 */}
        &lt;div className="lg:w-1/2 p-8 text-center lg:text-left"&gt;
          &lt;h2 className="text-lg font-semibold text-gray-700 mb-2"&gt;
            Comfort and Style
          &lt;/h2&gt;
          &lt;h2 className="text-4xl lg:text-5xl font-bold mb-6"&gt;
            Apparel made for your everyday life
          &lt;/h2&gt;
          &lt;p className="text-lg text-gray-600 mb-6"&gt;
            Discover high-quality, comfortable clothing that effortlessly blends
            fashion and function. Designed to make you look and feel great every
            day.
          &lt;/p&gt;
          &lt;Link
            to="/collection/all"
            className="bg-black text-white px-6 py-3 rounded-lg text-lg hover:bg-gray-800"
          &gt;
            Shop Now
          &lt;/Link&gt;
        &lt;/div&gt;

        {/* Right Content - 右邊內容 */}
        &lt;div className="lg:w-1/2"&gt;
          &lt;img
            src={featured}
            alt="Featured Collection"
            className="w-full h-full object-cover lg:rounded-tr-3xl lg:rounded-br-3xl"
          /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  );
};

export default FeaturedCollection;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Products/FeaturesSection.jsx
import React from "react";
import {
  HiArrowPathRoundedSquare,
  HiOutlineCreditCard,
  HiShoppingBag,
} from "react-icons/hi2";

const FeaturesSection = () =&gt; {
  return (
    &lt;section className="py-16 px-4 bg-white"&gt;
      &lt;div className="container mx-auto grid grid-cols-1 md:grid-cols-3 gap-8 text-center"&gt;
        {/* Feature 1 - 特色 1 */}
        &lt;div className="flex flex-col items-center"&gt;
          &lt;div className="p-4 rounded-full mb-4"&gt;
            &lt;HiShoppingBag className="text-xl" /&gt;
          &lt;/div&gt;
          &lt;h4 className="tracking-tighter mb-2"&gt;FREE INTERNATIONAL SHIPPING&lt;/h4&gt;
          &lt;p className="text-gray-600 text-sm tracking-tighter"&gt;
            On all orders over $100.00
          &lt;/p&gt;
        &lt;/div&gt;

        {/* Feature 2 - 特色 2 */}
        &lt;div className="flex flex-col items-center"&gt;
          &lt;div className="p-4 rounded-full mb-4"&gt;
            &lt;HiArrowPathRoundedSquare className="text-xl" /&gt;
          &lt;/div&gt;
          &lt;h4 className="tracking-tighter mb-2"&gt;45 DAYS RETURN&lt;/h4&gt;
          &lt;p className="text-gray-600 text-sm tracking-tighter"&gt;
            Money back guarantee
          &lt;/p&gt;
        &lt;/div&gt;

        {/* Feature 3 - 特色 3 */}
        &lt;div className="flex flex-col items-center"&gt;
          &lt;div className="p-4 rounded-full mb-4"&gt;
            &lt;HiOutlineCreditCard className="text-xl" /&gt;
          &lt;/div&gt;
          &lt;h4 className="tracking-tighter mb-2"&gt;SECURE CHECKOUT&lt;/h4&gt;
          &lt;p className="text-gray-600 text-sm tracking-tighter"&gt;
            100% secured checkout process
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  );
};

export default FeaturesSection;
</code></pre>



<h2 class="wp-block-heading">製作登入頁面、註冊頁面</h2>



<pre class="wp-block-code"><code>// frontend/src/pages/Login.jsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import login from "../assets/login.webp";

const Login = () =&gt; {
  const &#91;email, setEmail] = useState("");
  const &#91;password, setPassword] = useState("");

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    console.log("User Login:", { email, password });
  };

  return (
    &lt;div className="flex"&gt;
      &lt;div className="w-full md:w-1/2 flex flex-col justify-center items-center p-8 md:p-12"&gt;
        &lt;form
          onSubmit={handleSubmit}
          className="w-full max-w-md bg-white p-8 rounded-lg border shadow-sm"
        &gt;
          &lt;div className="flex justify-center mb-6"&gt;
            &lt;h2 className="text-xl font-medium"&gt;Rabbit&lt;/h2&gt;
          &lt;/div&gt;
          &lt;h2 className="text-2xl font-bold text-center mb-6"&gt;Hey there! &lt;/h2&gt;
          &lt;p className="text-center mb-6"&gt;
            Enter your username and password to Login.
          &lt;/p&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Email&lt;/label&gt;
            &lt;input
              type="email"
              value={email}
              onChange={(e) =&gt; setEmail(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your email address"
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Password&lt;/label&gt;
            &lt;input
              type="password"
              value={password}
              onChange={(e) =&gt; setPassword(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your password"
            /&gt;
          &lt;/div&gt;
          &lt;button
            type="submit"
            className="w-full bg-black text-white p-2 rounded-lg font-semibold hover:bg-gray-800 transition"
          &gt;
            Sign In
          &lt;/button&gt;
          &lt;p className="mt-6 text-center text-sm"&gt;
            Don't have an account?{" "}
            &lt;Link to="/register" className="text-blue-500"&gt;
              Register
            &lt;/Link&gt;
          &lt;/p&gt;
        &lt;/form&gt;
      &lt;/div&gt;

      &lt;div className="hidden md:block w-1/2 bg-gray-800"&gt;
        &lt;div className="h-full flex flex-col justify-center items-center"&gt;
          &lt;img
            src={login}
            alt="Login to Account"
            className="h-&#91;750px] w-full object-cover"
          /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Login;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UserLayout from "./components/Layout/UserLayout";
import Home from "./pages/Home";
import { Toaster } from "sonner";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Profile from "./pages/Profile";

const App = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Toaster position="top-right" /&gt;
      &lt;Routes&gt;
        &lt;Route path="/" element={&lt;UserLayout /&gt;}&gt;
          {/* User Layout - 使用者佈局 */}
          &lt;Route index element={&lt;Home /&gt;} /&gt;
          &lt;Route path="login" element={&lt;Login /&gt;} /&gt;
          &lt;Route path="register" element={&lt;Register /&gt;} /&gt;
          &lt;Route path="profile" element={&lt;Profile /&gt;} /&gt;
        &lt;/Route&gt;
        &lt;Route&gt;{/* Admin Layout - 管理員佈局 */}&lt;/Route&gt;
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default App;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Register.jsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import register from "../assets/register.webp";

const Register = () =&gt; {
  const &#91;name, setName] = useState("");
  const &#91;email, setEmail] = useState("");
  const &#91;password, setPassword] = useState("");

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    console.log("User Registered:", { name, email, password });
  };

  return (
    &lt;div className="flex"&gt;
      &lt;div className="w-full md:w-1/2 flex flex-col justify-center items-center p-8 md:p-12"&gt;
        &lt;form
          onSubmit={handleSubmit}
          className="w-full max-w-md bg-white p-8 rounded-lg border shadow-sm"
        &gt;
          &lt;div className="flex justify-center mb-6"&gt;
            &lt;h2 className="text-xl font-medium"&gt;Rabbit&lt;/h2&gt;
          &lt;/div&gt;
          &lt;h2 className="text-2xl font-bold text-center mb-6"&gt;Hey there! &lt;/h2&gt;
          &lt;p className="text-center mb-6"&gt;
            Enter your username and password to Login.
          &lt;/p&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Name&lt;/label&gt;
            &lt;input
              type="text"
              value={name}
              onChange={(e) =&gt; setName(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your name"
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Email&lt;/label&gt;
            &lt;input
              type="email"
              value={email}
              onChange={(e) =&gt; setEmail(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your email address"
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-sm font-semibold mb-2"&gt;Password&lt;/label&gt;
            &lt;input
              type="password"
              value={password}
              onChange={(e) =&gt; setPassword(e.target.value)}
              className="w-full p-2 border rounded"
              placeholder="Enter your password"
            /&gt;
          &lt;/div&gt;
          &lt;button
            type="submit"
            className="w-full bg-black text-white p-2 rounded-lg font-semibold hover:bg-gray-800 transition"
          &gt;
            Sign Up
          &lt;/button&gt;
          &lt;p className="mt-6 text-center text-sm"&gt;
            Don't have an account?{" "}
            &lt;Link to="/login" className="text-blue-500"&gt;
              Login
            &lt;/Link&gt;
          &lt;/p&gt;
        &lt;/form&gt;
      &lt;/div&gt;

      &lt;div className="hidden md:block w-1/2 bg-gray-800"&gt;
        &lt;div className="h-full flex flex-col justify-center items-center"&gt;
          &lt;img
            src={register}
            alt="Login to Account"
            className="h-&#91;750px] w-full object-cover"
          /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Register;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/Profile.jsx
import React from "react";
import MyOrdersPage from "./MyOrdersPage";

const Profile = () =&gt; {
  return (
    &lt;div className="min-h-screen flex flex-col"&gt;
      &lt;div className="flex-grow container mx-auto p-4 md:p-6"&gt;
        &lt;div className="flex flex-col md:flex-row md:space-x-6 space-y-6 md:space-y-0"&gt;
          {/* Left Section - 左側區域 */}
          &lt;div className="w-full md:w-1/3 lg:w-1/4 shadow-md rounded-lg p-6"&gt;
            &lt;h1 className="text-2xl md:text-3xl font-bold mb-4"&gt;John Doe&lt;/h1&gt;
            &lt;p className="text-lg text-gray-600 mb-4"&gt;John@example.com&lt;/p&gt;
            &lt;button className="w-full bg-red-500 text-white py-2 px-4 rounded hover:bg-red-600"&gt;
              Logout
            &lt;/button&gt;
          &lt;/div&gt;
          {/* Right Section: Orders table - 右側區域: 訂單表格 */}
          &lt;div className="w-full md:w-2/3 lg:w-3/4"&gt;
            &lt;MyOrdersPage /&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Profile;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/MyOrdersPage.jsx
import React, { useEffect, useState } from "react";

const MyOrdersPage = () =&gt; {
  const &#91;orders, setOrders] = useState(&#91;]);

  useEffect(() =&gt; {
    // Simulate fetching orders - 模擬獲取訂單
    setTimeout(() =&gt; {
      const mockOrders = &#91;
        {
          _id: "12345",
          createdAt: new Date(),
          shippingAddress: { city: "New York", country: "USA" },
          orderItems: &#91;
            {
              name: "Product 1",
              image: "https://picsum.photos/500/500?random=1",
            },
          ],
          totalPrice: 100,
          isPaid: true,
        },
        {
          _id: "34567",
          createdAt: new Date(),
          shippingAddress: { city: "New York", country: "USA" },
          orderItems: &#91;
            {
              name: "Product 2",
              image: "https://picsum.photos/500/500?random=2",
            },
          ],
          totalPrice: 100,
          isPaid: true,
        },
      ];

      setOrders(mockOrders);
    }, 1000);
  }, &#91;]);

  return (
    &lt;div className="max-w-7xl mx-auto p-4 sm:p-6"&gt;
      &lt;h2 className="text-xl sm:text-2xl font-bold mb-6"&gt;My Orders&lt;/h2&gt;
      &lt;div className="relative shadow-md sm:rounded-lg overflow-hidden"&gt;
        &lt;table className="min-w-full text-left text-gray-500"&gt;
          &lt;thead className="bg-gray-100 text-xs uppercase text-gray-700"&gt;
            &lt;tr&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Image&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Order ID&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Created&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Shipping Address&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Items&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Price&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Status&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            {orders.length &gt; 0 ? (
              orders.map((order) =&gt; (
                &lt;tr
                  key={order._id}
                  className="border-b hover:border-gray-50 cursor-pointer"
                &gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    &lt;img
                      src={order.orderItems&#91;0].image}
                      alt={order.orderItems&#91;0].name}
                      className="w-10 h-10 sm:w-12 sm:h-12 object-cover rounded-lg"
                    /&gt;
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4 font-medium text-gray-900 whitespace-nowrap"&gt;
                    #{order._id}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    {new Date(order.createdAt).toLocaleDateString()}{" "}
                    {new Date(order.createdAt).toLocaleTimeString()}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    {order.shippingAddress
                      ? `${order.shippingAddress.city}, ${order.shippingAddress.country}`
                      : "N/A"}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    {order.orderItems.length}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    ${order.totalPrice}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    &lt;span
                      className={`${
                        order.isPaid
                          ? "bg-green-100 text-green-700"
                          : "bg-red-100 text-red-700"
                      } px-2 py-1 rounded-full text-xs sm:text-sm font-medium`}
                    &gt;
                      {order.isPaid ? "Paid" : "Pending"}
                    &lt;/span&gt;
                  &lt;/td&gt;
                &lt;/tr&gt;
              ))
            ) : (
              &lt;tr&gt;
                &lt;td colSpan={7} className="py-4 px-4 text-center text-gray-500"&gt;
                  You have no orders
                &lt;/td&gt;
              &lt;/tr&gt;
            )}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default MyOrdersPage;
</code></pre>



<h2 class="wp-block-heading">製作 Collection Section (系列區域)</h2>



<pre class="wp-block-code"><code>// frontend/src/pages/CollectionPage.jsx
import React, { useEffect, useRef, useState } from "react";
import { FaFilter } from "react-icons/fa";
import FilterSidebar from "../components/Products/FilterSidebar";
import SortOptions from "../components/Products/SortOptions";
import ProductGrid from "../components/Products/ProductGrid";

const CollectionPage = () =&gt; {
  const &#91;products, setProducts] = useState(&#91;]);
  const sidebarRef = useRef(null);
  const &#91;isSidebarOpen, setIsSidebarOpen] = useState(false);

  const toggleSidebar = () =&gt; {
    setIsSidebarOpen(!isSidebarOpen);
  };

  const handleClickOutside = (e) =&gt; {
    // Close sidebar if clicked outside - 如果點擊在外部則關閉側邊欄
    if (sidebarRef.current &amp;&amp; !sidebarRef.current.contains(e.target)) {
      setIsSidebarOpen(false);
    }
  };

  useEffect(() =&gt; {
    // Add event listner for clicks - 為點擊事件添加事件監聽器
    document.addEventListener("mousedown", handleClickOutside);
    // clean event listener - 清除事件監聽器
    return () =&gt; {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, &#91;]);

  useEffect(() =&gt; {
    setTimeout(() =&gt; {
      const fetchedProducts = &#91;
        {
          _id: 1,
          name: "Product 1",
          price: 100,
          images: &#91;
            {
              url: "https://picsum.photos/500/500?random=3",
            },
          ],
        },
        {
          _id: 2,
          name: "Product 2",
          price: 100,
          images: &#91;
            {
              url: "https://picsum.photos/500/500?random=4",
            },
          ],
        },
        {
          _id: 3,
          name: "Product 3",
          price: 100,
          images: &#91;
            {
              url: "https://picsum.photos/500/500?random=5",
            },
          ],
        },
        {
          _id: 4,
          name: "Product 4",
          price: 100,
          images: &#91;
            {
              url: "https://picsum.photos/500/500?random=6",
            },
          ],
        },
        {
          _id: 5,
          name: "Product 5",
          price: 100,
          images: &#91;
            {
              url: "https://picsum.photos/500/500?random=7",
            },
          ],
        },
        {
          _id: 6,
          name: "Product 6",
          price: 100,
          images: &#91;
            {
              url: "https://picsum.photos/500/500?random=8",
            },
          ],
        },
        {
          _id: 7,
          name: "Product 7",
          price: 100,
          images: &#91;
            {
              url: "https://picsum.photos/500/500?random=9",
            },
          ],
        },
        {
          _id: 8,
          name: "Product 8",
          price: 100,
          images: &#91;
            {
              url: "https://picsum.photos/500/500?random=10",
            },
          ],
        },
      ];
      setProducts(fetchedProducts);
    }, 1000);
  }, &#91;]);
  return (
    &lt;div className="flex flex-col lg:flex-row"&gt;
      {/* Mobile Filter button - 手機篩選按鈕  */}
      &lt;button
        onClick={toggleSidebar}
        className="lg:hidden border p-2 flex justify-center items-center"
      &gt;
        &lt;FaFilter className="mr-2" /&gt; Filters
      &lt;/button&gt;

      {/* Filter Sidebar - 篩選側邊欄 */}
      &lt;div
        ref={sidebarRef}
        className={`${
          isSidebarOpen ? "translate-x-0" : "-translate-x-full"
        } fixed inset-y-0 z-50 left-0 w-64 bg-white overflow-y-auto transition-transform duration-300 lg:static lg:translate-x-0`}
      &gt;
        &lt;FilterSidebar /&gt;
      &lt;/div&gt;
      &lt;div className="flex-grow p-4"&gt;
        &lt;h2 className="text-2xl uppercase mb-4"&gt;All Collection&lt;/h2&gt;

        {/* Sort Options - 排序選項 */}
        &lt;SortOptions /&gt;

        {/* Product Grid - 產品網格  */}
        &lt;ProductGrid products={products} /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default CollectionPage;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UserLayout from "./components/Layout/UserLayout";
import Home from "./pages/Home";
import { Toaster } from "sonner";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Profile from "./pages/Profile";
import CollectionPage from "./pages/CollectionPage";

const App = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Toaster position="top-right" /&gt;
      &lt;Routes&gt;
        &lt;Route path="/" element={&lt;UserLayout /&gt;}&gt;
          {/* User Layout - 使用者佈局 */}
          &lt;Route index element={&lt;Home /&gt;} /&gt;
          &lt;Route path="login" element={&lt;Login /&gt;} /&gt;
          &lt;Route path="register" element={&lt;Register /&gt;} /&gt;
          &lt;Route path="profile" element={&lt;Profile /&gt;} /&gt;
          &lt;Route path="collections/:collection" element={&lt;CollectionPage /&gt;} /&gt;
        &lt;/Route&gt;
        &lt;Route&gt;{/* Admin Layout - 管理員佈局 */}&lt;/Route&gt;
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default App;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Common/Navbar.jsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import {
  HiOutlineUser,
  HiOutlineShoppingBag,
  HiBars3BottomRight,
} from "react-icons/hi2";
import SearchBar from "./SearchBar";
import CartDrawer from "../Layout/CartDrawer";
import { IoMdClose } from "react-icons/io";

const Navbar = () =&gt; {
  const &#91;drawerOpen, setDrawerOpen] = useState(false);
  const &#91;navDrawerOpen, setNavDrawerOpen] = useState(false);

  const toggleNavDrawer = () =&gt; {
    setNavDrawerOpen(!navDrawerOpen);
  };

  const toggleCartDrawer = () =&gt; {
    setDrawerOpen(!drawerOpen);
  };

  return (
    &lt;&gt;
      &lt;nav className="container mx-auto flex items-center justify-between py-4 px-6"&gt;
        {/* Left - Logo -&gt; 左側 - 商標、標誌 */}
        &lt;div&gt;
          &lt;Link to="/" className="text-2xl font-medium"&gt;
            Rabbit
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Center - Navigation Links -&gt; 中間 - 導覽連結 */}
        &lt;div className="hidden md:flex space-x-6"&gt;
          &lt;Link
            to="/collections/all"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Men
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Women
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Top Wear
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Bottom Wear
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Right - Icons -&gt; 右側 - 圖示 */}
        &lt;div className="flex items-center space-x-4"&gt;
          &lt;Link to="/profile" className="hover:text-black"&gt;
            &lt;HiOutlineUser className="h-6 w-6 text-gray-700" /&gt;
          &lt;/Link&gt;
          &lt;button
            onClick={toggleCartDrawer}
            className="relative hover:text-black"
          &gt;
            &lt;HiOutlineShoppingBag className="h-6 w-6 text-gray-700" /&gt;
            &lt;span className="absolute -top-1 bg-rabbit-red text-white text-xs rounded-full px-2 py-0.5"&gt;
              4
            &lt;/span&gt;
          &lt;/button&gt;
          {/* Search - 搜尋 */}
          &lt;div className="overflow-hidden"&gt;
            &lt;SearchBar /&gt;
          &lt;/div&gt;

          &lt;button onClick={toggleNavDrawer} className="md:hidden"&gt;
            &lt;HiBars3BottomRight className="h-6 w-6 text-gray-700" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/nav&gt;
      &lt;CartDrawer drawerOpen={drawerOpen} toggleCartDrawer={toggleCartDrawer} /&gt;

      {/* Mobile Navigation - 手機版導覽 */}
      &lt;div
        className={`fixed top-0 left-0 w-3/4 sm:w-1/2 md:w-1/3 h-full bg-white shadow-lg transform transition-transform duration-300 z-50 ${
          navDrawerOpen ? "translate-x-0" : "-translate-x-full"
        }`}
      &gt;
        &lt;div className="flex justify-end p-4"&gt;
          &lt;button onClick={toggleNavDrawer}&gt;
            &lt;IoMdClose className="h-6 w-6 text-gray-600" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
        &lt;div className="p-4"&gt;
          &lt;h2 className="text-xl font-semibold mb-4"&gt;Menu&lt;/h2&gt;
          &lt;nav className="space-y-4"&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Men
            &lt;/Link&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Women
            &lt;/Link&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Top Wear
            &lt;/Link&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Bottom Wear
            &lt;/Link&gt;
          &lt;/nav&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default Navbar;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Products/FilterSidebar.jsx
import React, { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";

const FilterSidebar = () =&gt; {
  // x.com/?a=1&amp;b=2
  const &#91;searchParams, setSearchParams] = useSearchParams();
  const navigate = useNavigate();
  const &#91;filters, setFilters] = useState({
    category: "",
    gender: "",
    color: "",
    size: &#91;],
    material: &#91;],
    brand: &#91;],
    minPrice: 0,
    maxPrice: 100,
  });

  const &#91;priceRange, setPriceRange] = useState(&#91;0, 100]);

  const categories = &#91;"Top Wear", "Bottom Wear"];

  const colors = &#91;
    "Red",
    "Blue",
    "Black",
    "Green",
    "Yellow",
    "Gray",
    "White",
    "Pink",
    "Beige",
    "Navy",
  ];

  const sizes = &#91;"XS", "S", "M", "L", "XL", "XXL"];

  const materials = &#91;
    "Cotton",
    "Wool",
    "Denim",
    "Polyester",
    "Silk",
    "Linen",
    "Viscose",
    "Fleece",
  ];

  const brands = &#91;
    "Urban Threads",
    "Modern Fit",
    "Street Style",
    "Beach Breeze",
    "Fashionista",
    "ChicStyle",
  ];

  const genders = &#91;"Men", "Women"];

  useEffect(() =&gt; {
    // {category: 'Top Wear', maxPrice: 100} =&gt; params.category
    const params = Object.fromEntries(&#91;...searchParams]);

    setFilters({
      category: params.category || "",
      gender: params.gender || "",
      color: params.color || "",
      size: params.size ? params.size.split(",") : &#91;],
      material: params.material ? params.material.split(",") : &#91;],
      brand: params.brand ? params.brand.split(",") : &#91;],
      minPrice: params.minPrice || 0,
      maxPrice: params.maxPrice || 100,
    });
    setPriceRange(&#91;0, params.maxPrice || 100]);
  }, &#91;searchParams]);

  const handleFilterChange = (e) =&gt; {
    const { name, value, checked, type } = e.target;
    // console.log({ name, value, checked, type });
    let newFilters = { ...filters };

    if (type === "checkbox") {
      if (checked) {
        newFilters&#91;name] = &#91;...(newFilters&#91;name] || &#91;]), value]; // &#91;"XS", "S"]
      } else {
        newFilters&#91;name] = newFilters&#91;name].filter((item) =&gt; item !== value);
      }
    } else {
      newFilters&#91;name] = value;
    }
    setFilters(newFilters);
    // console.log(newFilters);
    updateURLParams(newFilters);
  };

  const updateURLParams = (newFilters) =&gt; {
    const params = new URLSearchParams();
    // {category: "Top Wear", size: &#91;"XS", "S"]}
    Object.keys(newFilters).forEach((key) =&gt; {
      if (Array.isArray(newFilters&#91;key]) &amp;&amp; newFilters&#91;key].length &gt; 0) {
        params.append(key, newFilters&#91;key].join(",")); // "XS,S"
      } else if (newFilters&#91;key]) {
        params.append(key, newFilters&#91;key]);
      }
    });
    setSearchParams(params);
    navigate(`?${params.toString()}`); // ?category=Bottom+Wear&amp;size=XS%2CS
  };

  const handlePriceChange = (e) =&gt; {
    const newPrice = e.target.value;
    setPriceRange(&#91;0, newPrice]);
    const newFilters = { ...filters, minPrice: 0, maxPrice: newPrice };
    setFilters(filters);
    updateURLParams(newFilters);
  };

  return (
    &lt;div className="p-4"&gt;
      &lt;h3 className="text-xl font-medium text-gray-800 mb-4"&gt;Filter&lt;/h3&gt;

      {/* Category Filter - 分類篩選 */}
      &lt;div className="mb-6"&gt;
        &lt;label className="block text-gray-600 font-medium mb-2"&gt;Category&lt;/label&gt;
        {categories.map((category) =&gt; (
          &lt;div key={category} className="flex items-center mb-1"&gt;
            &lt;input
              type="radio"
              name="category"
              value={category}
              onChange={handleFilterChange}
              checked={filters.category === category}
              className="mr-2 h-4 w-4 text-blue-500 focus:ring-blue-400 border-gray-300"
            /&gt;
            &lt;span className="text-gray-700"&gt;{category}&lt;/span&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;

      {/* Gender Filter - 性別篩選 */}
      &lt;div className="mb-6"&gt;
        &lt;label className="block text-gray-600 font-medium mb-2"&gt;Gender&lt;/label&gt;
        {genders.map((gender) =&gt; (
          &lt;div key={gender} className="flex items-center mb-1"&gt;
            &lt;input
              type="radio"
              name="gender"
              value={gender}
              onChange={handleFilterChange}
              checked={filters.gender === gender}
              className="mr-2 h-4 w-4 text-blue-500 focus:ring-blue-400 border-gray-300"
            /&gt;
            &lt;span className="text-gray-700"&gt;{gender}&lt;/span&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;

      {/* Color Filter - 顏色篩選 */}
      &lt;div className="mb-6"&gt;
        &lt;label className="block text-gray-600 font-medium mb-2"&gt;Color&lt;/label&gt;
        &lt;div className="flex flex-wrap gap-2"&gt;
          {colors.map((color) =&gt; (
            &lt;button
              key={color}
              name="color"
              value={color}
              onClick={handleFilterChange}
              className={`w-8 h-8 rounded-full border border-gray-300 cursor-pointer transition hover:scale-105 ${
                filters.color === color ? "ring-2 ring-blue-500" : ""
              }`}
              style={{ backgroundColor: color.toLowerCase() }}
            &gt;&lt;/button&gt;
          ))}
        &lt;/div&gt;
      &lt;/div&gt;

      {/* Size Filter - 尺寸篩選 */}
      &lt;div className="mb-6"&gt;
        &lt;label className="block text-gray-600 font-medium mb-2"&gt;Size&lt;/label&gt;
        {sizes.map((size) =&gt; (
          &lt;div key={size} className="flex items-center mb-1"&gt;
            &lt;input
              type="checkbox"
              name="size"
              value={size}
              onChange={handleFilterChange}
              checked={filters.size.includes(size)}
              className="mr-2 h-4 w-4 text-blue-red focus:ring-blue-400 border-gray-300"
            /&gt;
            &lt;span className="text-gray-700"&gt;{size}&lt;/span&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;

      {/* Material Filter - 材質篩選 */}
      &lt;div className="mb-6"&gt;
        &lt;label className="block text-gray-600 font-medium mb-2"&gt;Material&lt;/label&gt;
        {materials.map((material) =&gt; (
          &lt;div key={material} className="flex items-center mb-1"&gt;
            &lt;input
              type="checkbox"
              name="material"
              value={material}
              onChange={handleFilterChange}
              checked={filters.material.includes(material)}
              className="mr-2 h-4 w-4 text-blue-red focus:ring-blue-400 border-gray-300"
            /&gt;
            &lt;span className="text-gray-700"&gt;{material}&lt;/span&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;

      {/* Brand Filter - 品牌篩選 */}
      &lt;div className="mb-6"&gt;
        &lt;label className="block text-gray-600 font-medium mb-2"&gt;Brand&lt;/label&gt;
        {brands.map((brand) =&gt; (
          &lt;div key={brand} className="flex items-center mb-1"&gt;
            &lt;input
              type="checkbox"
              name="brand"
              value={brand}
              onChange={handleFilterChange}
              checked={filters.brand.includes(brand)}
              className="mr-2 h-4 w-4 text-blue-red focus:ring-blue-400 border-gray-300"
            /&gt;
            &lt;span className="text-gray-700"&gt;{brand}&lt;/span&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;

      {/* Price Range Filter - 價格區間篩選 */}
      &lt;div className="mb-8"&gt;
        &lt;label className="block text-gray-600 font-medium mb-2"&gt;
          Price Range
        &lt;/label&gt;
        &lt;input
          type="range"
          name="priceRange"
          min={0}
          max={100}
          value={priceRange&#91;1]}
          onChange={handlePriceChange}
          className="w-full h-2 bg-gray-300 rounded-lg appearance-none cursor-pointer"
        /&gt;
        &lt;div className="flex justify-between text-gray-600 mt-2"&gt;
          &lt;span&gt;$0&lt;/span&gt;
          &lt;span&gt;${priceRange&#91;1]}&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default FilterSidebar;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Products/SortOptions.jsx
import React from "react";
import { useSearchParams } from "react-router-dom";

const SortOptions = () =&gt; {
  const &#91;searchParams, setSearchParams] = useSearchParams();

  const handleSortChange = (e) =&gt; {
    const sortBy = e.target.value;
    searchParams.set("sortBy", sortBy);
    setSearchParams(searchParams);
  };

  return (
    &lt;div className="mb-4 flex items-center justify-end"&gt;
      &lt;select
        onChange={handleSortChange}
        value={searchParams.get("sortBy") || ""}
        id="sort"
        className="border p-2 rounded-md focus:outline-none"
      &gt;
        &lt;option value=""&gt;Default&lt;/option&gt;
        &lt;option value="priceAsc"&gt;Price: Low to High&lt;/option&gt;
        &lt;option value="priceDesc"&gt;Price: High to Low&lt;/option&gt;
        &lt;option value="popularity"&gt;Popularity&lt;/option&gt;
      &lt;/select&gt;
    &lt;/div&gt;
  );
};

export default SortOptions;
</code></pre>



<h2 class="wp-block-heading">增加產品細節的路由 (added the route to our product details)</h2>



<pre class="wp-block-code"><code>// frontend/src/App.jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UserLayout from "./components/Layout/UserLayout";
import Home from "./pages/Home";
import { Toaster } from "sonner";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Profile from "./pages/Profile";
import CollectionPage from "./pages/CollectionPage";
import ProductDetails from "./components/Products/ProductDetails";

const App = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Toaster position="top-right" /&gt;
      &lt;Routes&gt;
        &lt;Route path="/" element={&lt;UserLayout /&gt;}&gt;
          {/* User Layout - 使用者佈局 */}
          &lt;Route index element={&lt;Home /&gt;} /&gt;
          &lt;Route path="login" element={&lt;Login /&gt;} /&gt;
          &lt;Route path="register" element={&lt;Register /&gt;} /&gt;
          &lt;Route path="profile" element={&lt;Profile /&gt;} /&gt;
          &lt;Route path="collections/:collection" element={&lt;CollectionPage /&gt;} /&gt;
          &lt;Route path="product/:id" element={&lt;ProductDetails /&gt;} /&gt;
        &lt;/Route&gt;
        &lt;Route&gt;{/* Admin Layout - 管理員佈局 */}&lt;/Route&gt;
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default App;
</code></pre>



<h2 class="wp-block-heading">製作結帳頁面</h2>



<pre class="wp-block-code"><code>// frontend/src/components/Cart/Checkout.jsx
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import PayPalButton from "./PayPalButton";

const cart = {
  products: &#91;
    {
      name: "Stylish Jacket",
      size: "M",
      color: "Black",
      price: 120,
      image: "https://picsum.photos/150?random=1",
    },
    {
      name: "Casual Sneakers",
      size: "42",
      color: "White",
      price: 75,
      image: "https://picsum.photos/150?random=2",
    },
  ],
  totalPrice: 195,
};

const Checkout = () =&gt; {
  const navigate = useNavigate();
  const &#91;checkoutId, setCheckoutId] = useState(null);
  const &#91;shippingAddress, setShippingAddress] = useState({
    firstName: "",
    lastName: "",
    address: "",
    city: "",
    postalCode: "",
    country: "",
    phone: "",
  });

  const handleCreateCheckout = (e) =&gt; {
    e.preventDefault();
    setCheckoutId(123);
  };

  const handlePaymentSuccess = (details) =&gt; {
    console.log("Payment Successful", details);
    navigate("/order-confirmation");
  };

  return (
    &lt;div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-7xl mx-auto py-10 px-6 tracking-tighter"&gt;
      {/* Left Section - 左側區域 */}
      &lt;div className="bg-white rounded-lg p-6"&gt;
        &lt;h2 className="text-2xl uppercase mb-6"&gt;Checkout&lt;/h2&gt;
        &lt;form onSubmit={handleCreateCheckout}&gt;
          &lt;h3 className="text-lg mb-4"&gt;Contact Details&lt;/h3&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Email&lt;/label&gt;
            &lt;input
              type="email"
              value="user@example.com"
              className="w-full p-2 border rounded"
              disabled
            /&gt;
          &lt;/div&gt;
          &lt;h3 className="text-lg mb-4"&gt;Delivery&lt;/h3&gt;
          &lt;div className="mb-4 grid grid-cols-2 gap-4"&gt;
            &lt;div&gt;
              &lt;label className="block text-gray-700"&gt;First Name&lt;/label&gt;
              &lt;input
                type="text"
                value={shippingAddress.firstName}
                onChange={(e) =&gt;
                  setShippingAddress({
                    ...shippingAddress,
                    firstName: e.target.value,
                  })
                }
                className="w-full p-2 border rounded"
                required
              /&gt;
            &lt;/div&gt;
            &lt;div&gt;
              &lt;label className="block text-gray-700"&gt;Last Name&lt;/label&gt;
              &lt;input
                type="text"
                value={shippingAddress.lastName}
                onChange={(e) =&gt;
                  setShippingAddress({
                    ...shippingAddress,
                    lastName: e.target.value,
                  })
                }
                className="w-full p-2 border rounded"
                required
              /&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Address&lt;/label&gt;
            &lt;input
              type="text"
              value={shippingAddress.address}
              onChange={(e) =&gt;
                setShippingAddress({
                  ...shippingAddress,
                  address: e.target.value,
                })
              }
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4 grid grid-cols-2 gap-4"&gt;
            &lt;div&gt;
              &lt;label className="block text-gray-700"&gt;City&lt;/label&gt;
              &lt;input
                type="text"
                value={shippingAddress.city}
                onChange={(e) =&gt;
                  setShippingAddress({
                    ...shippingAddress,
                    city: e.target.value,
                  })
                }
                className="w-full p-2 border rounded"
                required
              /&gt;
            &lt;/div&gt;
            &lt;div&gt;
              &lt;label className="block text-gray-700"&gt;Postal Code&lt;/label&gt;
              &lt;input
                type="text"
                value={shippingAddress.postalCode}
                onChange={(e) =&gt;
                  setShippingAddress({
                    ...shippingAddress,
                    postalCode: e.target.value,
                  })
                }
                className="w-full p-2 border rounded"
                required
              /&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Country&lt;/label&gt;
            &lt;input
              type="text"
              value={shippingAddress.country}
              onChange={(e) =&gt;
                setShippingAddress({
                  ...shippingAddress,
                  country: e.target.value,
                })
              }
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Phone&lt;/label&gt;
            &lt;input
              type="tel"
              value={shippingAddress.phone}
              onChange={(e) =&gt;
                setShippingAddress({
                  ...shippingAddress,
                  phone: e.target.value,
                })
              }
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mt-6"&gt;
            {!checkoutId ? (
              &lt;button
                type="submit"
                className="w-full bg-black text-white py-3 rounded"
              &gt;
                Continue to Payment
              &lt;/button&gt;
            ) : (
              &lt;div&gt;
                &lt;h3 className="text-lg mb-4"&gt;Pay with Paypal&lt;/h3&gt;
                {/* Paypal Component */}
                &lt;PayPalButton
                  amount={100}
                  onSuccess={handlePaymentSuccess}
                  onError={(err) =&gt; alert("Payment failed. Try again.")}
                /&gt;
              &lt;/div&gt;
            )}
          &lt;/div&gt;
        &lt;/form&gt;
      &lt;/div&gt;
      {/* Right Section - 右側區域 */}
      &lt;div className="bg-gray-50 p-6 rounded-lg"&gt;
        &lt;h3 className="text-lg mb-4"&gt;Order Summary&lt;/h3&gt;
        &lt;div className="border-t py-4 mb-4"&gt;
          {cart.products.map((product, index) =&gt; (
            &lt;div
              key={index}
              className="flex items-start justify-between py-2 border-b"
            &gt;
              &lt;div className="flex items-start"&gt;
                &lt;img
                  src={product.image}
                  alt={product.name}
                  className="w-20 h-24 object-cover mr-4"
                /&gt;
                &lt;div&gt;
                  &lt;h3 className="text-md"&gt;{product.name}&lt;/h3&gt;
                  &lt;p className="text-gray-500"&gt;Size: {product.size}&lt;/p&gt;
                  &lt;p className="text-gray-500"&gt;Color: {product.color}&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;p className="text-xl"&gt;${product.price?.toLocaleString()}&lt;/p&gt;
            &lt;/div&gt;
          ))}
        &lt;/div&gt;
        &lt;div className="flex justify-between items-center text-lg mb-4"&gt;
          &lt;p&gt;Subtotal&lt;/p&gt;
          &lt;p&gt;${cart.totalPrice?.toLocaleString()}&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="flex justify-between items-center text-lg"&gt;
          &lt;p&gt;Shipping&lt;/p&gt;
          &lt;p&gt;Free&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="flex justify-between items-center text-lg mt-4 border-t pt-4"&gt;
          &lt;p&gt;Total&lt;/p&gt;
          &lt;p&gt;${cart.totalPrice?.toLocaleString()}&lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default Checkout;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Layout/CartDrawer.jsx
import React, { useState } from "react";
import { IoMdClose } from "react-icons/io";
import CartContents from "../Cart/CartContents";
import { useNavigate } from "react-router-dom";

const CartDrawer = ({ drawerOpen, toggleCartDrawer }) =&gt; {
  const navigate = useNavigate();
  const handleCheckout = () =&gt; {
    navigate("/checkout");
  };
  return (
    &lt;div
      className={`fixed top-0 right-0 w-3/4 sm:w-1/2 md:w-&#91;30rem] h-full bg-white shadow-lg transform transition-transform duration-300 flex flex-col z-50 ${
        drawerOpen ? "translate-x-0" : "translate-x-full"
      }`}
    &gt;
      {/* Close Button - 關閉的按鈕 */}
      &lt;div className="flex justify-end p-4"&gt;
        &lt;button onClick={toggleCartDrawer}&gt;
          &lt;IoMdClose className="h-6 w-6 text-gray-600" /&gt;
        &lt;/button&gt;
      &lt;/div&gt;
      {/* Cart contents with scrollable area - 具有可滾動區域的購物車內容 */}
      &lt;div className="flex-grow p-4 overflow-y-auto"&gt;
        &lt;h2 className="text-xl font-semibold mb-4"&gt;Your Cart&lt;/h2&gt;
        {/* Component for Cart Contents - 購物車內容組件 */}
        &lt;CartContents /&gt;
      &lt;/div&gt;

      {/* Checkout button fixed at the bottom - 結帳按鈕固定在底部 */}
      &lt;div className="p-4 bg-white sticky bottom-0"&gt;
        &lt;button
          onClick={handleCheckout}
          className="w-full bg-black text-white py-3 rounded-lg font-semibold hover:bg-gray-800 transition"
        &gt;
          Checkout
        &lt;/button&gt;
        &lt;p className="text-sm tracking-tighter text-gray-500 mt-2 text-center"&gt;
          Shipping, taxes, and discount codes calculated at checkout.
        &lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default CartDrawer;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UserLayout from "./components/Layout/UserLayout";
import Home from "./pages/Home";
import { Toaster } from "sonner";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Profile from "./pages/Profile";
import CollectionPage from "./pages/CollectionPage";
import ProductDetails from "./components/Products/ProductDetails";
import Checkout from "./components/Cart/Checkout";

const App = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Toaster position="top-right" /&gt;
      &lt;Routes&gt;
        &lt;Route path="/" element={&lt;UserLayout /&gt;}&gt;
          {/* User Layout - 使用者佈局 */}
          &lt;Route index element={&lt;Home /&gt;} /&gt;
          &lt;Route path="login" element={&lt;Login /&gt;} /&gt;
          &lt;Route path="register" element={&lt;Register /&gt;} /&gt;
          &lt;Route path="profile" element={&lt;Profile /&gt;} /&gt;
          &lt;Route path="collections/:collection" element={&lt;CollectionPage /&gt;} /&gt;
          &lt;Route path="product/:id" element={&lt;ProductDetails /&gt;} /&gt;
          &lt;Route path="checkout" element={&lt;Checkout /&gt;} /&gt;
        &lt;/Route&gt;
        &lt;Route&gt;{/* Admin Layout - 管理員佈局 */}&lt;/Route&gt;
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default App;
</code></pre>



<h2 class="wp-block-heading">使用 Paypal Sandbox 開發金流串接</h2>



<pre class="wp-block-code"><code>// frontend/src/components/Cart/PayPalButton.jsx
import React from "react";
import { PayPalButtons, PayPalScriptProvider } from "@paypal/react-paypal-js";

const PayPalButton = ({ amount, onSuccess, onError }) =&gt; {
  return (
    &lt;PayPalScriptProvider
      options={{
        "client-id":
          "使用自己的Client Id",
      }}
    &gt;
      &lt;PayPalButtons
        style={{ layout: "vertical" }}
        createOrder={(data, actions) =&gt; {
          return actions.order.create({
            purchase_units: &#91;{ amount: { value: amount } }],
          });
        }}
        onApprove={(data, actions) =&gt; {
          return actions.order.capture().then(onSuccess);
        }}
        onError={onError}
      /&gt;
    &lt;/PayPalScriptProvider&gt;
  );
};

export default PayPalButton;
</code></pre>



<ul class="wp-block-list">
<li><a href="https://www.npmjs.com/package/@paypal/react-paypal-js" target="_blank" rel="noreferrer noopener">react-paypal-js</a> – 安裝套件</li>
</ul>



<h2 class="wp-block-heading">製作訂單確認頁面 (orders confirmation)</h2>



<pre class="wp-block-code"><code>// frontend/src/pages/OrderConfirmationPage.jsx
import React from "react";

const checkout = {
  _id: "12323",
  createdAt: new Date(),
  checkoutItems: &#91;
    {
      productId: "1",
      name: "Jacket",
      color: "black",
      size: "M",
      price: 150,
      quantity: 1,
      image: "https://picsum.photos/150?random=1",
    },
    {
      productId: "2",
      name: "T-shirt",
      color: "black",
      size: "M",
      price: 120,
      quantity: 2,
      image: "https://picsum.photos/150?random=2",
    },
  ],
  shippingAddress: {
    address: "123 Fashion Street",
    city: "New York",
    country: "USA",
  },
};

const OrderConfirmationPage = () =&gt; {
  const calculateEstimatedDelivery = (createdAt) =&gt; {
    const orderDate = new Date(createdAt);
    orderDate.setDate(orderDate.getDate() + 10); // Add 10 days to the order date - 訂單日期增加10天
    return orderDate.toLocaleDateString();
  };

  return (
    &lt;div className="max-w-4xl mx-auto p-6 bg-white"&gt;
      &lt;h1 className="text-4xl font-bold text-center text-emerald-700 mb-8"&gt;
        Thank You for Your Order!
      &lt;/h1&gt;

      {checkout &amp;&amp; (
        &lt;div className="p-6 rounded-lg border"&gt;
          &lt;div className="flex justify-between mb-20"&gt;
            {/* Order Id and Date - 訂單編號和日期 */}
            &lt;div&gt;
              &lt;h2 className="text-xl font-semibold"&gt;
                Order ID: {checkout._id}
              &lt;/h2&gt;
              &lt;p className="text-gray-500"&gt;
                Order date: {new Date(checkout.createdAt).toLocaleDateString()}
              &lt;/p&gt;
            &lt;/div&gt;
            {/* Estimated Delivery- 預計送達 */}
            &lt;div&gt;
              &lt;p className="text-emerald-700 text-sm"&gt;
                Estimated Delivery:{" "}
                {calculateEstimatedDelivery(checkout.createdAt)}
              &lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* Ordered Items - 訂單項目 */}
          &lt;div className="mb-20"&gt;
            {checkout.checkoutItems.map((item) =&gt; (
              &lt;div key={item.productId} className="flex items-center mb-4"&gt;
                &lt;img
                  src={item.image}
                  alt={item.name}
                  className="w-16 h-16 object-cover rounded-md mr-4"
                /&gt;
                &lt;div&gt;
                  &lt;h4 className="text-md font-semibold"&gt;{item.name}&lt;/h4&gt;
                  &lt;p className="text-sm text-gray-500"&gt;
                    {item.color} | {item.size}
                  &lt;/p&gt;
                &lt;/div&gt;
                &lt;div className="ml-auto text-right"&gt;
                  &lt;p className="text-md"&gt;${item.price}&lt;/p&gt;
                  &lt;p className="text-sm text-gray-500"&gt;Qty: {item.quantity}&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            ))}
          &lt;/div&gt;
          {/* Payment and Delivery Info - 付款與送貨資訊 */}
          &lt;div className="grid grid-cols-2 gap-8"&gt;
            {/* Payment Info - 付款資訊 */}
            &lt;div&gt;
              &lt;h4 className="text-lg font-semibold mb-2"&gt;Payment&lt;/h4&gt;
              &lt;p className="text-gray-600"&gt;PayPal&lt;/p&gt;
            &lt;/div&gt;

            {/* Delivery Info - 送貨資訊 */}
            &lt;div&gt;
              &lt;h4 className="text-lg font-semibold mb-2"&gt;Delivery&lt;/h4&gt;
              &lt;p className="text-gray-600"&gt;
                {checkout.shippingAddress.address}
              &lt;/p&gt;
              &lt;p className="text-gray-600"&gt;
                {checkout.shippingAddress.city},{" "}
                {checkout.shippingAddress.country}
              &lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  );
};

export default OrderConfirmationPage;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UserLayout from "./components/Layout/UserLayout";
import Home from "./pages/Home";
import { Toaster } from "sonner";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Profile from "./pages/Profile";
import CollectionPage from "./pages/CollectionPage";
import ProductDetails from "./components/Products/ProductDetails";
import Checkout from "./components/Cart/Checkout";
import OrderConfirmationPage from "./pages/OrderConfirmationPage";

const App = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Toaster position="top-right" /&gt;
      &lt;Routes&gt;
        &lt;Route path="/" element={&lt;UserLayout /&gt;}&gt;
          {/* User Layout - 使用者佈局 */}
          &lt;Route index element={&lt;Home /&gt;} /&gt;
          &lt;Route path="login" element={&lt;Login /&gt;} /&gt;
          &lt;Route path="register" element={&lt;Register /&gt;} /&gt;
          &lt;Route path="profile" element={&lt;Profile /&gt;} /&gt;
          &lt;Route path="collections/:collection" element={&lt;CollectionPage /&gt;} /&gt;
          &lt;Route path="product/:id" element={&lt;ProductDetails /&gt;} /&gt;
          &lt;Route path="checkout" element={&lt;Checkout /&gt;} /&gt;
          &lt;Route
            path="order-confirmation"
            element={&lt;OrderConfirmationPage /&gt;}
          /&gt;
        &lt;/Route&gt;
        &lt;Route&gt;{/* Admin Layout - 管理員佈局 */}&lt;/Route&gt;
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default App;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Layout/CartDrawer.jsx
import React, { useState } from "react";
import { IoMdClose } from "react-icons/io";
import CartContents from "../Cart/CartContents";
import { useNavigate } from "react-router-dom";

const CartDrawer = ({ drawerOpen, toggleCartDrawer }) =&gt; {
  const navigate = useNavigate();
  const handleCheckout = () =&gt; {
    toggleCartDrawer();
    navigate("/checkout");
  };
  return (
    &lt;div
      className={`fixed top-0 right-0 w-3/4 sm:w-1/2 md:w-&#91;30rem] h-full bg-white shadow-lg transform transition-transform duration-300 flex flex-col z-50 ${
        drawerOpen ? "translate-x-0" : "translate-x-full"
      }`}
    &gt;
      {/* Close Button - 關閉的按鈕 */}
      &lt;div className="flex justify-end p-4"&gt;
        &lt;button onClick={toggleCartDrawer}&gt;
          &lt;IoMdClose className="h-6 w-6 text-gray-600" /&gt;
        &lt;/button&gt;
      &lt;/div&gt;
      {/* Cart contents with scrollable area - 具有可滾動區域的購物車內容 */}
      &lt;div className="flex-grow p-4 overflow-y-auto"&gt;
        &lt;h2 className="text-xl font-semibold mb-4"&gt;Your Cart&lt;/h2&gt;
        {/* Component for Cart Contents - 購物車內容組件 */}
        &lt;CartContents /&gt;
      &lt;/div&gt;

      {/* Checkout button fixed at the bottom - 結帳按鈕固定在底部 */}
      &lt;div className="p-4 bg-white sticky bottom-0"&gt;
        &lt;button
          onClick={handleCheckout}
          className="w-full bg-black text-white py-3 rounded-lg font-semibold hover:bg-gray-800 transition"
        &gt;
          Checkout
        &lt;/button&gt;
        &lt;p className="text-sm tracking-tighter text-gray-500 mt-2 text-center"&gt;
          Shipping, taxes, and discount codes calculated at checkout.
        &lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default CartDrawer;
</code></pre>



<h2 class="wp-block-heading">製作訂單細節頁面</h2>



<pre class="wp-block-code"><code>// frontend/src/pages/OrderDetailsPage.jsx
import React, { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";

const OrderDetailsPage = () =&gt; {
  const { id } = useParams();
  const &#91;orderDetails, setOrderDetails] = useState(null);

  useEffect(() =&gt; {
    const mockOrderDetails = {
      _id: id,
      createdAt: new Date(),
      isPaid: true,
      isDelivered: false,
      paymentMethod: "PayPal",
      shippingMethod: "Standard",
      shippingAddress: { city: "New York", country: "USA" },
      orderItems: &#91;
        {
          productId: "1",
          name: "Jacket",
          price: 120,
          quantity: 1,
          image: "https://picsum.photos/150?random=1",
        },
        {
          productId: "2",
          name: "Shirt",
          price: 150,
          quantity: 2,
          image: "https://picsum.photos/150?random=2",
        },
      ],
    };
    setOrderDetails(mockOrderDetails);
  }, &#91;id]);

  return (
    &lt;div className="max-w-7xl mx-auto p-4 sm:p-6"&gt;
      &lt;h2 className="text-2xl md:text-3xl font-bold mb-6"&gt;Order Details&lt;/h2&gt;
      {!orderDetails ? (
        &lt;p&gt;No Order details found&lt;/p&gt;
      ) : (
        &lt;div className="p-4 sm:p-6 rounded-lg border"&gt;
          {/* Order Info - 訂單資訊 */}
          &lt;div className="flex flex-col sm:flex-row justify-between mb-8"&gt;
            &lt;div&gt;
              &lt;h3 className="text-lg md:text-xl font-semibold"&gt;
                Order ID: #{orderDetails._id}
              &lt;/h3&gt;
              &lt;p className="text-gray-600"&gt;
                {new Date(orderDetails.createdAt).toLocaleDateString()}
              &lt;/p&gt;
            &lt;/div&gt;
            &lt;div className="flex flex-col items-start sm:items-end mt-4 sm:mt-0"&gt;
              &lt;span
                className={`${
                  orderDetails.isPaid
                    ? "bg-green-100 text-green-700"
                    : "bg-red-100 text-red-700"
                } px-3 py-1 rounded-full text-sm font-medium mb-2`}
              &gt;
                {orderDetails.isPaid ? "Approved" : "Pending"}
              &lt;/span&gt;
              &lt;span
                className={`${
                  orderDetails.isDelivered
                    ? "bg-green-100 text-green-700"
                    : "bg-yellow-100 text-yellow-700"
                } px-3 py-1 rounded-full text-sm font-medium mb-2`}
              &gt;
                {orderDetails.isDelivered ? "Delivered" : "Pending"}
              &lt;/span&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* Customer, Payment, Shipping Info - 顧客, 付款, 送貨資訊 */}
          &lt;div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 mb-8"&gt;
            &lt;div&gt;
              &lt;h4 className="text-lg font-semibold mb-2"&gt;Payment Info&lt;/h4&gt;
              &lt;p&gt;Payment Method: {orderDetails.paymentMethod}&lt;/p&gt;
              &lt;p&gt;Status: {orderDetails.isPaid ? "Paid" : "Unpaid"}&lt;/p&gt;
            &lt;/div&gt;
            &lt;div&gt;
              &lt;h4 className="text-lg font-semibold mb-2"&gt;Shipping Info&lt;/h4&gt;
              &lt;p&gt;Shipping Method: {orderDetails.shippingMethod}&lt;/p&gt;
              &lt;p&gt;
                Address:{" "}
                {`${orderDetails.shippingAddress.city}, ${orderDetails.shippingAddress.country}`}
              &lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* Product list - 產品清單 */}
          &lt;div className="overflow-x-auto"&gt;
            &lt;h4 className="text-lg font-semibold mb-4"&gt;Products&lt;/h4&gt;
            &lt;table className="min-w-full text-gray-600 mb-4"&gt;
              &lt;thead className="bg-gray-100"&gt;
                &lt;tr&gt;
                  &lt;th className="py-2 px-4"&gt;Name&lt;/th&gt;
                  &lt;th className="py-2 px-4"&gt;Unit Price&lt;/th&gt;
                  &lt;th className="py-2 px-4"&gt;Quantity&lt;/th&gt;
                  &lt;th className="py-2 px-4"&gt;Total&lt;/th&gt;
                &lt;/tr&gt;
              &lt;/thead&gt;
              &lt;tbody&gt;
                {orderDetails.orderItems.map((item) =&gt; (
                  &lt;tr key={item.productId} className="border-b"&gt;
                    &lt;td className="py-2 px-4 flex items-center"&gt;
                      &lt;img
                        src={item.image}
                        alt={item.name}
                        className="w-12 h-12 object-cover rounded-lg mr-4"
                      /&gt;
                      &lt;Link
                        to={`/product/${item.productId}`}
                        className="text-blue-500 hover:underline"
                      &gt;
                        {item.name}
                      &lt;/Link&gt;
                    &lt;/td&gt;
                    &lt;td className="py-2 px-4"&gt;${item.price}&lt;/td&gt;
                    &lt;td className="py-2 px-4"&gt;${item.quantity}&lt;/td&gt;
                    &lt;td className="py-2 px-4"&gt;${item.price * item.quantity}&lt;/td&gt;
                  &lt;/tr&gt;
                ))}
              &lt;/tbody&gt;
            &lt;/table&gt;
          &lt;/div&gt;

          {/* Back to Orders Link - 回到訂單頁面連結 */}
          &lt;Link to="/my-orders" className="text-blue-500 hover:underline"&gt;
            Back to My Orders
          &lt;/Link&gt;
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  );
};

export default OrderDetailsPage;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UserLayout from "./components/Layout/UserLayout";
import Home from "./pages/Home";
import { Toaster } from "sonner";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Profile from "./pages/Profile";
import CollectionPage from "./pages/CollectionPage";
import ProductDetails from "./components/Products/ProductDetails";
import Checkout from "./components/Cart/Checkout";
import OrderConfirmationPage from "./pages/OrderConfirmationPage";
import OrderDetailsPage from "./pages/OrderDetailsPage";
import MyOrdersPage from "./pages/MyOrdersPage";

const App = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Toaster position="top-right" /&gt;
      &lt;Routes&gt;
        &lt;Route path="/" element={&lt;UserLayout /&gt;}&gt;
          {/* User Layout - 使用者佈局 */}
          &lt;Route index element={&lt;Home /&gt;} /&gt;
          &lt;Route path="login" element={&lt;Login /&gt;} /&gt;
          &lt;Route path="register" element={&lt;Register /&gt;} /&gt;
          &lt;Route path="profile" element={&lt;Profile /&gt;} /&gt;
          &lt;Route path="collections/:collection" element={&lt;CollectionPage /&gt;} /&gt;
          &lt;Route path="product/:id" element={&lt;ProductDetails /&gt;} /&gt;
          &lt;Route path="checkout" element={&lt;Checkout /&gt;} /&gt;
          &lt;Route
            path="order-confirmation"
            element={&lt;OrderConfirmationPage /&gt;}
          /&gt;
          &lt;Route path="order/:id" element={&lt;OrderDetailsPage /&gt;} /&gt;
          &lt;Route path="my-orders" element={&lt;MyOrdersPage /&gt;} /&gt;
        &lt;/Route&gt;
        &lt;Route&gt;{/* Admin Layout - 管理員佈局 */}&lt;/Route&gt;
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default App;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/MyOrdersPage.jsx
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";

const MyOrdersPage = () =&gt; {
  const &#91;orders, setOrders] = useState(&#91;]);
  const navigate = useNavigate();

  useEffect(() =&gt; {
    // Simulate fetching orders - 模擬獲取訂單
    setTimeout(() =&gt; {
      const mockOrders = &#91;
        {
          _id: "12345",
          createdAt: new Date(),
          shippingAddress: { city: "New York", country: "USA" },
          orderItems: &#91;
            {
              name: "Product 1",
              image: "https://picsum.photos/500/500?random=1",
            },
          ],
          totalPrice: 100,
          isPaid: true,
        },
        {
          _id: "34567",
          createdAt: new Date(),
          shippingAddress: { city: "New York", country: "USA" },
          orderItems: &#91;
            {
              name: "Product 2",
              image: "https://picsum.photos/500/500?random=2",
            },
          ],
          totalPrice: 100,
          isPaid: true,
        },
      ];

      setOrders(mockOrders);
    }, 1000);
  }, &#91;]);

  const handleRowClick = (orderId) =&gt; {
    navigate(`/order/${orderId}`);
  };

  return (
    &lt;div className="max-w-7xl mx-auto p-4 sm:p-6"&gt;
      &lt;h2 className="text-xl sm:text-2xl font-bold mb-6"&gt;My Orders&lt;/h2&gt;
      &lt;div className="relative shadow-md sm:rounded-lg overflow-hidden"&gt;
        &lt;table className="min-w-full text-left text-gray-500"&gt;
          &lt;thead className="bg-gray-100 text-xs uppercase text-gray-700"&gt;
            &lt;tr&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Image&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Order ID&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Created&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Shipping Address&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Items&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Price&lt;/th&gt;
              &lt;th className="py-2 px-4 sm:py-3"&gt;Status&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            {orders.length &gt; 0 ? (
              orders.map((order) =&gt; (
                &lt;tr
                  key={order._id}
                  onClick={() =&gt; handleRowClick(order._id)}
                  className="border-b hover:border-gray-50 cursor-pointer"
                &gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    &lt;img
                      src={order.orderItems&#91;0].image}
                      alt={order.orderItems&#91;0].name}
                      className="w-10 h-10 sm:w-12 sm:h-12 object-cover rounded-lg"
                    /&gt;
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4 font-medium text-gray-900 whitespace-nowrap"&gt;
                    #{order._id}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    {new Date(order.createdAt).toLocaleDateString()}{" "}
                    {new Date(order.createdAt).toLocaleTimeString()}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    {order.shippingAddress
                      ? `${order.shippingAddress.city}, ${order.shippingAddress.country}`
                      : "N/A"}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    {order.orderItems.length}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    ${order.totalPrice}
                  &lt;/td&gt;
                  &lt;td className="py-2 px-2 sm:py-4 sm:px-4"&gt;
                    &lt;span
                      className={`${
                        order.isPaid
                          ? "bg-green-100 text-green-700"
                          : "bg-red-100 text-red-700"
                      } px-2 py-1 rounded-full text-xs sm:text-sm font-medium`}
                    &gt;
                      {order.isPaid ? "Paid" : "Pending"}
                    &lt;/span&gt;
                  &lt;/td&gt;
                &lt;/tr&gt;
              ))
            ) : (
              &lt;tr&gt;
                &lt;td colSpan={7} className="py-4 px-4 text-center text-gray-500"&gt;
                  You have no orders
                &lt;/td&gt;
              &lt;/tr&gt;
            )}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default MyOrdersPage;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Cart/PayPalButton.jsx
import React from "react";
import { PayPalButtons, PayPalScriptProvider } from "@paypal/react-paypal-js";

const PayPalButton = ({ amount, onSuccess, onError }) =&gt; {
  return (
    &lt;PayPalScriptProvider
      options={{
        "client-id": import.meta.env.VITE_PAYPAL_CLIENT_ID,
      }}
    &gt;
      &lt;PayPalButtons
        style={{ layout: "vertical" }}
        createOrder={(data, actions) =&gt; {
          return actions.order.create({
            purchase_units: &#91;{ amount: { value: amount } }],
          });
        }}
        onApprove={(data, actions) =&gt; {
          return actions.order.capture().then(onSuccess);
        }}
        onError={onError}
      /&gt;
    &lt;/PayPalScriptProvider&gt;
  );
};

export default PayPalButton;
</code></pre>



<pre class="wp-block-code"><code>// frontend/.env
VITE_PAYPAL_CLIENT_ID=你的client-id
</code></pre>



<h2 class="wp-block-heading">製作管理員頁面</h2>



<pre class="wp-block-code"><code>// frontend/src/components/Admin/AdminLayout.jsx
import React, { useState } from "react";
import { FaBars } from "react-icons/fa";
import AdminSidebar from "./AdminSidebar";
import { Outlet } from "react-router-dom";

const AdminLayout = () =&gt; {
  const &#91;isSidebarOpen, setIsSidebarOpen] = useState(false);

  const toggleSidebar = () =&gt; {
    setIsSidebarOpen(!isSidebarOpen);
  };

  return (
    &lt;div className="min-h-screen flex flex-col md:flex-row relative"&gt;
      {/* Mobile Toggle Button - 手機切換按鈕 */}
      &lt;div className="flex md:hidden p-4 bg-gray-900 text-white z-20"&gt;
        &lt;button onClick={toggleSidebar}&gt;
          &lt;FaBars size={24} /&gt;
        &lt;/button&gt;
        &lt;h1 className="ml-4 text-xl font-medium"&gt;Admin Dashboard&lt;/h1&gt;
      &lt;/div&gt;

      {/* Overlay for mobile sidebar - 手機側邊欄的覆蓋 */}
      {isSidebarOpen &amp;&amp; (
        &lt;div
          className="fixed inset-0 z-10 bg-black bg-opacity-50 md:hidden"
          onClick={toggleSidebar}
        &gt;&lt;/div&gt;
      )}

      {/* sidebar - 側邊欄 */}
      &lt;div
        className={`bg-gray-900 w-64 min-h-screen text-white absolute md:relative transform ${
          isSidebarOpen ? "translate-x-0" : "-translate-x-full"
        } transition-transform duration-300 md:translate-x-0 md:static md:block z-20`}
      &gt;
        {/* Sidebar - 側邊欄 */}
        &lt;AdminSidebar /&gt;
      &lt;/div&gt;

      {/* Main Content - 主要內容 */}
      &lt;div className="flex-grow p-6 overflow-auto"&gt;
        &lt;Outlet /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default AdminLayout;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/App.jsx
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import UserLayout from "./components/Layout/UserLayout";
import Home from "./pages/Home";
import { Toaster } from "sonner";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Profile from "./pages/Profile";
import CollectionPage from "./pages/CollectionPage";
import ProductDetails from "./components/Products/ProductDetails";
import Checkout from "./components/Cart/Checkout";
import OrderConfirmationPage from "./pages/OrderConfirmationPage";
import OrderDetailsPage from "./pages/OrderDetailsPage";
import MyOrdersPage from "./pages/MyOrdersPage";
import AdminLayout from "./components/Admin/AdminLayout";
import AdminHomePage from "./pages/AdminHomePage";
import UserManagement from "./components/Admin/UserManagement";
import ProductManagement from "./components/Admin/ProductManagement";
import EditProductPage from "./components/Admin/EditProductPage";
import OrderManagement from "./components/Admin/OrderManagement";

const App = () =&gt; {
  return (
    &lt;BrowserRouter&gt;
      &lt;Toaster position="top-right" /&gt;
      &lt;Routes&gt;
        &lt;Route path="/" element={&lt;UserLayout /&gt;}&gt;
          {/* User Layout - 使用者佈局 */}
          &lt;Route index element={&lt;Home /&gt;} /&gt;
          &lt;Route path="login" element={&lt;Login /&gt;} /&gt;
          &lt;Route path="register" element={&lt;Register /&gt;} /&gt;
          &lt;Route path="profile" element={&lt;Profile /&gt;} /&gt;
          &lt;Route path="collections/:collection" element={&lt;CollectionPage /&gt;} /&gt;
          &lt;Route path="product/:id" element={&lt;ProductDetails /&gt;} /&gt;
          &lt;Route path="checkout" element={&lt;Checkout /&gt;} /&gt;
          &lt;Route
            path="order-confirmation"
            element={&lt;OrderConfirmationPage /&gt;}
          /&gt;
          &lt;Route path="order/:id" element={&lt;OrderDetailsPage /&gt;} /&gt;
          &lt;Route path="my-orders" element={&lt;MyOrdersPage /&gt;} /&gt;
        &lt;/Route&gt;
        &lt;Route path="/admin" element={&lt;AdminLayout /&gt;}&gt;
          {/* Admin Layout - 管理員佈局 */}
          &lt;Route index element={&lt;AdminHomePage /&gt;} /&gt;
          &lt;Route path="users" element={&lt;UserManagement /&gt;} /&gt;
          &lt;Route path="products" element={&lt;ProductManagement /&gt;} /&gt;
          &lt;Route path="products/:id/edit" element={&lt;EditProductPage /&gt;} /&gt;
          &lt;Route path="orders" element={&lt;OrderManagement /&gt;} /&gt;
        &lt;/Route&gt;
      &lt;/Routes&gt;
    &lt;/BrowserRouter&gt;
  );
};

export default App;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Common/Navbar.jsx
import React, { useState } from "react";
import { Link } from "react-router-dom";
import {
  HiOutlineUser,
  HiOutlineShoppingBag,
  HiBars3BottomRight,
} from "react-icons/hi2";
import SearchBar from "./SearchBar";
import CartDrawer from "../Layout/CartDrawer";
import { IoMdClose } from "react-icons/io";

const Navbar = () =&gt; {
  const &#91;drawerOpen, setDrawerOpen] = useState(false);
  const &#91;navDrawerOpen, setNavDrawerOpen] = useState(false);

  const toggleNavDrawer = () =&gt; {
    setNavDrawerOpen(!navDrawerOpen);
  };

  const toggleCartDrawer = () =&gt; {
    setDrawerOpen(!drawerOpen);
  };

  return (
    &lt;&gt;
      &lt;nav className="container mx-auto flex items-center justify-between py-4 px-6"&gt;
        {/* Left - Logo -&gt; 左側 - 商標、標誌 */}
        &lt;div&gt;
          &lt;Link to="/" className="text-2xl font-medium"&gt;
            Rabbit
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Center - Navigation Links -&gt; 中間 - 導覽連結 */}
        &lt;div className="hidden md:flex space-x-6"&gt;
          &lt;Link
            to="/collections/all"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Men
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Women
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Top Wear
          &lt;/Link&gt;
          &lt;Link
            to="#"
            className="text-gray-700 hover:text-black text-sm font-medium uppercase"
          &gt;
            Bottom Wear
          &lt;/Link&gt;
        &lt;/div&gt;
        {/* Right - Icons -&gt; 右側 - 圖示 */}
        &lt;div className="flex items-center space-x-4"&gt;
          &lt;Link
            to="/admin"
            className="block bg-black px-2 rounded text-sm text-white"
          &gt;
            Admin
          &lt;/Link&gt;
          &lt;Link to="/profile" className="hover:text-black"&gt;
            &lt;HiOutlineUser className="h-6 w-6 text-gray-700" /&gt;
          &lt;/Link&gt;
          &lt;button
            onClick={toggleCartDrawer}
            className="relative hover:text-black"
          &gt;
            &lt;HiOutlineShoppingBag className="h-6 w-6 text-gray-700" /&gt;
            &lt;span className="absolute -top-1 bg-rabbit-red text-white text-xs rounded-full px-2 py-0.5"&gt;
              4
            &lt;/span&gt;
          &lt;/button&gt;
          {/* Search - 搜尋 */}
          &lt;div className="overflow-hidden"&gt;
            &lt;SearchBar /&gt;
          &lt;/div&gt;

          &lt;button onClick={toggleNavDrawer} className="md:hidden"&gt;
            &lt;HiBars3BottomRight className="h-6 w-6 text-gray-700" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/nav&gt;
      &lt;CartDrawer drawerOpen={drawerOpen} toggleCartDrawer={toggleCartDrawer} /&gt;

      {/* Mobile Navigation - 手機版導覽 */}
      &lt;div
        className={`fixed top-0 left-0 w-3/4 sm:w-1/2 md:w-1/3 h-full bg-white shadow-lg transform transition-transform duration-300 z-50 ${
          navDrawerOpen ? "translate-x-0" : "-translate-x-full"
        }`}
      &gt;
        &lt;div className="flex justify-end p-4"&gt;
          &lt;button onClick={toggleNavDrawer}&gt;
            &lt;IoMdClose className="h-6 w-6 text-gray-600" /&gt;
          &lt;/button&gt;
        &lt;/div&gt;
        &lt;div className="p-4"&gt;
          &lt;h2 className="text-xl font-semibold mb-4"&gt;Menu&lt;/h2&gt;
          &lt;nav className="space-y-4"&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Men
            &lt;/Link&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Women
            &lt;/Link&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Top Wear
            &lt;/Link&gt;
            &lt;Link
              to="#"
              onClick={toggleNavDrawer}
              className="block text-gray-600 hover:text-black"
            &gt;
              Bottom Wear
            &lt;/Link&gt;
          &lt;/nav&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};

export default Navbar;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Admin/AdminSidebar.jsx
import React from "react";
import {
  FaBoxOpen,
  FaClipboardList,
  FaSignOutAlt,
  FaStore,
  FaUser,
} from "react-icons/fa";
import { Link, NavLink, useNavigate } from "react-router-dom";

const AdminSidebar = () =&gt; {
  const navigate = useNavigate();
  const handleLogout = () =&gt; {
    navigate("/");
  };

  return (
    &lt;div className="p-6"&gt;
      &lt;div className="mb-6"&gt;
        &lt;Link to="/admin" className="text-2xl font-medium"&gt;
          Rabbit
        &lt;/Link&gt;
      &lt;/div&gt;
      &lt;h2 className="text-xl font-medium mb-6 text-center"&gt;Admin Dashboard&lt;/h2&gt;

      &lt;nav className="flex flex-col space-y-2"&gt;
        &lt;NavLink
          to="/admin/users"
          className={({ isActive }) =&gt;
            isActive
              ? "bg-gray-700 text-white py-3 px-4 rounded flex items-center space-x-2"
              : "text-gray-300 hover:bg-gray-700 hover:text-white py-3 px-4 rounded flex items-center space-x-2"
          }
        &gt;
          &lt;FaUser /&gt;
          &lt;span&gt;Users&lt;/span&gt;
        &lt;/NavLink&gt;
        &lt;NavLink
          to="/admin/products"
          className={({ isActive }) =&gt;
            isActive
              ? "bg-gray-700 text-white py-3 px-4 rounded flex items-center space-x-2"
              : "text-gray-300 hover:bg-gray-700 hover:text-white py-3 px-4 rounded flex items-center space-x-2"
          }
        &gt;
          &lt;FaBoxOpen /&gt;
          &lt;span&gt;Products&lt;/span&gt;
        &lt;/NavLink&gt;
        &lt;NavLink
          to="/admin/orders"
          className={({ isActive }) =&gt;
            isActive
              ? "bg-gray-700 text-white py-3 px-4 rounded flex items-center space-x-2"
              : "text-gray-300 hover:bg-gray-700 hover:text-white py-3 px-4 rounded flex items-center space-x-2"
          }
        &gt;
          &lt;FaClipboardList /&gt;
          &lt;span&gt;Orders&lt;/span&gt;
        &lt;/NavLink&gt;
        &lt;NavLink
          to="/"
          className={({ isActive }) =&gt;
            isActive
              ? "bg-gray-700 text-white py-3 px-4 rounded flex items-center space-x-2"
              : "text-gray-300 hover:bg-gray-700 hover:text-white py-3 px-4 rounded flex items-center space-x-2"
          }
        &gt;
          &lt;FaStore /&gt;
          &lt;span&gt;Shop&lt;/span&gt;
        &lt;/NavLink&gt;
      &lt;/nav&gt;
      &lt;div className="mt-6"&gt;
        &lt;button
          onClick={handleLogout}
          className="w-full bg-red-500 hover:bg-red-600 text-white py-2 px-4 rounded flex items-center justify-center space-x-2"
        &gt;
          &lt;FaSignOutAlt /&gt;
          &lt;span&gt;Logout&lt;/span&gt;
        &lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default AdminSidebar;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/pages/AdminHomePage.jsx
import React from "react";
import { Link } from "react-router-dom";

const AdminHomePage = () =&gt; {
  const orders = &#91;
    {
      _id: 123123,
      user: {
        name: "John Doe",
      },
      totalPrice: 110,
      status: "Processing",
    },
  ];

  return (
    &lt;div className="max-w-7xl mx-auto p-6"&gt;
      &lt;h1 className="text-3xl font-bold mb-6"&gt;Admin Dashboard&lt;/h1&gt;
      &lt;div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"&gt;
        &lt;div className="p-4 shadow-md rounded-lg"&gt;
          &lt;h2 className="text-xl font-semibold"&gt;Revenue&lt;/h2&gt;
          &lt;p className="text-2xl"&gt;$10000&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="p-4 shadow-md rounded-lg"&gt;
          &lt;h2 className="text-xl font-semibold"&gt;Total Order&lt;/h2&gt;
          &lt;p className="text-2xl"&gt;200&lt;/p&gt;
          &lt;Link to="/amin/orders" className="text-blue-500 hover:underline"&gt;
            Manage Orders
          &lt;/Link&gt;
        &lt;/div&gt;
        &lt;div className="p-4 shadow-md rounded-lg"&gt;
          &lt;h2 className="text-xl font-semibold"&gt;Total Products&lt;/h2&gt;
          &lt;p className="text-2xl"&gt;100&lt;/p&gt;
          &lt;Link to="/amin/products" className="text-blue-500 hover:underline"&gt;
            Manage Products
          &lt;/Link&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div className="mt-6"&gt;
        &lt;h2 className="text-2xl font-bold mb-4"&gt;Recent Orders&lt;/h2&gt;
        &lt;div className="overflow-x-auto"&gt;
          &lt;table className="min-w-full text-left text-gray-500"&gt;
            &lt;thead className="bg-gray-100 text-xs uppercase text-gray-700"&gt;
              &lt;tr&gt;
                &lt;th className="py-3 px-4"&gt;Order ID&lt;/th&gt;
                &lt;th className="py-3 px-4"&gt;User&lt;/th&gt;
                &lt;th className="py-3 px-4"&gt;Total Price&lt;/th&gt;
                &lt;th className="py-3 px-4"&gt;Status&lt;/th&gt;
              &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
              {orders.length &gt; 0 ? (
                orders.map((order) =&gt; (
                  &lt;tr
                    key={order._id}
                    className="border-b hover:bg-gray-50 cursor-pointer"
                  &gt;
                    &lt;td className="p-4"&gt;{order._id}&lt;/td&gt;
                    &lt;td className="p-4"&gt;{order.user.name}&lt;/td&gt;
                    &lt;td className="p-4"&gt;{order.totalPrice}&lt;/td&gt;
                    &lt;td className="p-4"&gt;{order.status}&lt;/td&gt;
                  &lt;/tr&gt;
                ))
              ) : (
                &lt;tr&gt;
                  &lt;td colSpan={4} className="p-4 text-center text-gray-500"&gt;
                    No recent orders found.
                  &lt;/td&gt;
                &lt;/tr&gt;
              )}
            &lt;/tbody&gt;
          &lt;/table&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default AdminHomePage;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Admin/UserManagement.jsx
import React, { useState } from "react";

const UserManagement = () =&gt; {
  const users = &#91;
    {
      _id: 123213,
      name: "John Doe",
      email: "john@example.com",
      role: "admin",
    },
  ];

  const &#91;formData, setFormData] = useState({
    name: "",
    email: "",
    password: "",
    role: "customer", // Default role - 預設角色
  });

  const handleChange = (e) =&gt; {
    setFormData({
      ...formData,
      &#91;e.target.name]: e.target.value,
    });
  };

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    console.log(formData);

    // Reset the form after Submission - 提交後重置表單
    setFormData({
      name: "",
      email: "",
      password: "",
      role: "customer",
    });
  };

  const handleRoleChange = (userId, newRole) =&gt; {
    console.log({ id: userId, role: newRole });
  };

  const handleDeleteUser = (userId) =&gt; {
    if (window.confirm("Are you sure you want to delete this user?")) {
      console.log("deleting user with ID", userId);
    }
  };

  return (
    &lt;div className="max-w-7xl mx-auto p-6"&gt;
      &lt;h2 className="text-2xl font-bold mb-6"&gt;User Management&lt;/h2&gt;
      {/* Add New User Form - 新增使用者表單 */}
      &lt;div className="p-6 rounded-lg mb-6"&gt;
        &lt;h3 className="text-lg font-bold mb-4"&gt;Add New User&lt;/h3&gt;
        &lt;form onSubmit={handleSubmit}&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Name&lt;/label&gt;
            &lt;input
              type="text"
              name="name"
              value={formData.name}
              onChange={handleChange}
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Email&lt;/label&gt;
            &lt;input
              type="email"
              name="email"
              value={formData.email}
              onChange={handleChange}
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Password&lt;/label&gt;
            &lt;input
              type="password"
              name="password"
              value={formData.password}
              onChange={handleChange}
              className="w-full p-2 border rounded"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div className="mb-4"&gt;
            &lt;label className="block text-gray-700"&gt;Role&lt;/label&gt;
            &lt;select
              name="role"
              value={formData.role}
              onChange={handleChange}
              className="w-full p-2 border rounded"
            &gt;
              &lt;option value="customer"&gt;Customer&lt;/option&gt;
              &lt;option value="admin"&gt;Admin&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;
          &lt;button
            type="submit"
            className="bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600"
          &gt;
            Add User
          &lt;/button&gt;
        &lt;/form&gt;
      &lt;/div&gt;

      {/* User List Management - 用戶列表管理 */}
      &lt;div className="overflow-x-auto shadow-md sm:rounded-lg"&gt;
        &lt;table className="min-w-full text-left text-gray-500"&gt;
          &lt;thead className="bg-gray-100 text-xs uppercase text-gray-700"&gt;
            &lt;tr&gt;
              &lt;th className="py-3 px-4"&gt;Name&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Email&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Role&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Actions&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            {users.map((user) =&gt; (
              &lt;tr key={user._id} className="border-b hover:bg-gray-50"&gt;
                &lt;td className="p-4 font-medium text-gray-900 whitespace-nowrap"&gt;
                  {user.name}
                &lt;/td&gt;
                &lt;td className="p-4"&gt;{user.email}&lt;/td&gt;
                &lt;td className="p-4"&gt;
                  &lt;select
                    value={user.role}
                    onChange={(e) =&gt; handleRoleChange(user._id, e.target.value)}
                    className="p-2 border rounded"
                  &gt;
                    &lt;option value="customer"&gt;Customer&lt;/option&gt;
                    &lt;option value="admin"&gt;Admin&lt;/option&gt;
                  &lt;/select&gt;
                &lt;/td&gt;
                &lt;td className="p-4"&gt;
                  &lt;button
                    onClick={() =&gt; handleDeleteUser(user._id)}
                    className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
                  &gt;
                    Delete
                  &lt;/button&gt;
                &lt;/td&gt;
              &lt;/tr&gt;
            ))}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default UserManagement;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Admin/ProductManagement.jsx
import React from "react";
import { Link } from "react-router-dom";

const ProductManagement = () =&gt; {
  const products = &#91;
    {
      _id: 123123,
      name: "Shirt",
      price: 110,
      sku: "123123213",
    },
  ];

  const handleDelete = (id) =&gt; {
    if (window.confirm("Are you sure you want to delete the Product?")) {
      console.log("Delete Product with id:", id);
    }
  };

  return (
    &lt;div className="max-w-7xl mx-auto p-6"&gt;
      &lt;h2 className="text-2xl font-bold mb-6"&gt;Product Management&lt;/h2&gt;
      &lt;div className="overflow-x-auto shadow-md sm:rounded-lg"&gt;
        &lt;table className="min-w-full text-left text-gray-500"&gt;
          &lt;thead className="bg-gray-100 text-xs uppercase text-gray-700"&gt;
            &lt;tr&gt;
              &lt;th className="py-3 px-4"&gt;Name&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Price&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;SKU&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Actions&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            {products.length &gt; 0 ? (
              products.map((product) =&gt; (
                &lt;tr
                  key={product._id}
                  className="border-b hover:bg-gray-50 cursor-pointer"
                &gt;
                  &lt;td className="p-4 font-medium text-gray-900 whitespace-nowrap"&gt;
                    {product.name}
                  &lt;/td&gt;
                  &lt;td className="p-4"&gt;${product.price}&lt;/td&gt;
                  &lt;td className="p-4"&gt;{product.sku}&lt;/td&gt;
                  &lt;td className="p-4"&gt;
                    &lt;Link
                      to={`/admin/products/${product._id}/edit`}
                      className="bg-yellow-500 text-white px-2 py-1 rounded mr-2 hover:bg-yellow-600"
                    &gt;
                      Edit
                    &lt;/Link&gt;
                    &lt;button
                      onClick={() =&gt; handleDelete(product._id)}
                      className="bg-red-500 text-white px-2 py-1 rounded hover:bg-red-600"
                    &gt;
                      Delete
                    &lt;/button&gt;
                  &lt;/td&gt;
                &lt;/tr&gt;
              ))
            ) : (
              &lt;tr&gt;
                &lt;td colSpan={4} className="p-4 text-center text-gray-500"&gt;
                  No Products found.
                &lt;/td&gt;
              &lt;/tr&gt;
            )}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default ProductManagement;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Admin/EditProductPage.jsx
import React, { useState } from "react";

const EditProductPage = () =&gt; {
  const &#91;productData, setProductData] = useState({
    name: "",
    description: "",
    price: 0,
    countInStock: 0,
    sku: "",
    category: "",
    brand: "",
    sizes: &#91;],
    colors: &#91;],
    collections: "",
    material: "",
    gender: "",
    images: &#91;
      {
        url: "https://picsum.photos/150?random=1",
      },
      {
        url: "https://picsum.photos/150?random=2",
      },
    ],
  });

  const handleChange = (e) =&gt; {
    const { name, value } = e.target;
    setProductData((prevData) =&gt; ({ ...prevData, &#91;name]: value }));
  };

  const handleImageUpload = async (e) =&gt; {
    const file = e.target.files&#91;0];
    console.log(file);
  };

  const handleSubmit = (e) =&gt; {
    e.preventDefault();
    console.log(productData);
  };

  return (
    &lt;div className="max-w-5xl mx-auto p-6 shadow-md rounded-md"&gt;
      &lt;h2 className="text-3xl font-bold mb-6"&gt;Edit Product&lt;/h2&gt;
      &lt;form onSubmit={handleSubmit}&gt;
        {/* Name - 名稱 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;Product Name&lt;/label&gt;
          &lt;input
            type="text"
            name="name"
            value={productData.name}
            onChange={handleChange}
            className="w-full border border-gray-300 rounded-md p-2"
            required
          /&gt;
        &lt;/div&gt;

        {/* Description - 描述 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;Description&lt;/label&gt;
          &lt;textarea
            name="description"
            value={productData.description}
            onChange={handleChange}
            className="w-full border border-gray-300 rounded-md p-2"
            rows={4}
            required
          /&gt;
        &lt;/div&gt;

        {/* Price - 價格 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;Price&lt;/label&gt;
          &lt;input
            type="number"
            name="price"
            value={productData.price}
            onChange={handleChange}
            className="w-full border border-gray-300 rounded-md p-2"
          /&gt;
        &lt;/div&gt;

        {/* Count In stock - 庫存數量 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;Count in Stock&lt;/label&gt;
          &lt;input
            type="number"
            name="countInStock"
            value={productData.countInStock}
            onChange={handleChange}
            className="w-full border border-gray-300 rounded-md p-2"
          /&gt;
        &lt;/div&gt;

        {/* SKU - 庫存單位 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;SKU&lt;/label&gt;
          &lt;input
            type="text"
            name="sku"
            value={productData.sku}
            onChange={handleChange}
            className="w-full border border-gray-300 rounded-md p-2"
          /&gt;
        &lt;/div&gt;

        {/* Sizes - 尺寸 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;
            Sizes (comma-separated)
          &lt;/label&gt;
          &lt;input
            type="text"
            name="sizes"
            value={productData.sizes.join(", ")}
            onChange={(e) =&gt;
              setProductData({
                ...productData,
                sizes: e.target.value.split(",").map((size) =&gt; size.trim()),
              })
            }
            className="w-full border border-gray-300 rounded-md p-2"
          /&gt;
        &lt;/div&gt;

        {/* Colors - 顏色 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;
            Colors (comma-separated)
          &lt;/label&gt;
          &lt;input
            type="text"
            name="colors"
            value={productData.colors.join(", ")}
            onChange={(e) =&gt;
              setProductData({
                ...productData,
                colors: e.target.value.split(",").map((color) =&gt; color.trim()),
              })
            }
            className="w-full border border-gray-300 rounded-md p-2"
          /&gt;
        &lt;/div&gt;

        {/* Image Upload - 圖片上傳 */}
        &lt;div className="mb-6"&gt;
          &lt;label className="block font-semibold mb-2"&gt;Upload Image&lt;/label&gt;
          &lt;input type="file" onChange={handleImageUpload} /&gt;
          &lt;div className="flex gap-4 mt-4"&gt;
            {productData.images.map((image, index) =&gt; (
              &lt;div key={index}&gt;
                &lt;img
                  src={image.url}
                  alt={image.altText || "Product Image"}
                  className="w-20 h-20 object-cover rounded-md shadow-md"
                /&gt;
              &lt;/div&gt;
            ))}
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;button
          type="submit"
          className="w-full bg-green-500 text-white py-2 rounded-md hover:bg-green-600 transition-colors"
        &gt;
          Update Product
        &lt;/button&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
};

export default EditProductPage;
</code></pre>



<pre class="wp-block-code"><code>// frontend/src/components/Admin/OrderManagement.jsx
import React from "react";

const OrderManagement = () =&gt; {
  const orders = &#91;
    {
      _id: 12312321,
      user: {
        name: "John Doe",
      },
      totalPrice: 110,
      status: "Processing",
    },
  ];

  const handleStatusChange = (orderId, status) =&gt; {
    console.log({ id: orderId, status });
  };

  return (
    &lt;div className="max-w-7xl mx-auto p-6"&gt;
      &lt;h2 className="text-2xl font-bold mb-6"&gt;Order Management&lt;/h2&gt;

      &lt;div className="overflow-x-auto shadow-md sm:rounded-lg"&gt;
        &lt;table className="min-w-full text-left text-gray-500"&gt;
          &lt;thead className="bg-gray-100 text-xs uppercase text-gray-700"&gt;
            &lt;tr&gt;
              &lt;th className="py-3 px-4"&gt;Order ID&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Customer&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Total Price&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Status&lt;/th&gt;
              &lt;th className="py-3 px-4"&gt;Actions&lt;/th&gt;
            &lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            {orders.length &gt; 0 ? (
              orders.map((order) =&gt; (
                &lt;tr
                  key={order._id}
                  className="border-b hover:bg-gray-50 cursor-pointer"
                &gt;
                  &lt;td className="py-4 px-4 font-medium text-gray-900 whitespace-nowrap"&gt;
                    #{order._id}
                  &lt;/td&gt;
                  &lt;td className="p-4"&gt;{order.user.name}&lt;/td&gt;
                  &lt;td className="p-4"&gt;${order.totalPrice}&lt;/td&gt;
                  &lt;td className="p-4"&gt;
                    &lt;select
                      value={order.status}
                      onChange={(e) =&gt;
                        handleStatusChange(order._id, e.target.value)
                      }
                      className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5"
                    &gt;
                      &lt;option value="Processing"&gt;Processing&lt;/option&gt;
                      &lt;option value="Shipped"&gt;Shipped&lt;/option&gt;
                      &lt;option value="Delivered"&gt;Delivered&lt;/option&gt;
                      &lt;option value="Cancelled"&gt;Cancelled&lt;/option&gt;
                    &lt;/select&gt;
                  &lt;/td&gt;
                  &lt;td className="p-4"&gt;
                    &lt;button
                      onClick={() =&gt; handleStatusChange(order._id, "Delivered")}
                      className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
                    &gt;
                      Mark as Delivered
                    &lt;/button&gt;
                  &lt;/td&gt;
                &lt;/tr&gt;
              ))
            ) : (
              &lt;tr&gt;
                &lt;td colSpan={5} className="p-4 text-center text-gray-500"&gt;
                  No Orders found.
                &lt;/td&gt;
              &lt;/tr&gt;
            )}
          &lt;/tbody&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
};

export default OrderManagement;
</code></pre>



<p>以上完成這個專案前端的部分。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>React Real Estate App UI Design Tutorial for Beginners</title>
		<link>/wordpress_blog/react-real-estate-app-ui-design/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Sat, 14 Sep 2024 06:07:13 +0000</pubDate>
				<category><![CDATA[Youtube]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=876</guid>

					<description><![CDATA[Learning From Youtube Channel: L [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Learning From Youtube Channel: Lama Dev<br>Video: React React Estate App UI Design Tutorial for Beginners<br>Thank you.</p>



<p>00:00:00 – Introduction<br>00:02:17 – Installation<br>00:08:17 – Responsive Layout with CSS<br>00:15:36 – React Responsive Navbar Design<br>00:24:26 – React Responsive Hamburger Menu Design<br>00:33:09 – Real Estate App Homepage Design<br>00:34:28 – React.js VSCode Snippets<br>00:36:00 – React Responsive Hero Section Design<br>00:44:27 – Search Bar Design with React &amp; CSS<br>01:03:49 – React Router Dom Tutorial 2024<br>01:07:08 – React Router Dom Outlet Tutorial<br>01:12:25 – React Responsive List Page Design<br>01:39:30 – React Map Tutorial (Open Source Map Library)<br>01:40:41 – React Leaflet Map Tutorial<br>01:48:17 – React.js Responsive Single Page Design<br>01:58:23 – React.js Image Slider Tutorial From Scratch<br>02:12:41 – Property Features Design<br>02:30:00 – React.js Responsive Profile Page Design<br>02:45:03 – React.js Chat Component Design<br>02:50:17 – React Messenger Chat Window Design<br>03:00:55 – What’s Next?<br>03:01:25 – Outro</p>



<h2 class="wp-block-heading">Introduction</h2>



<h2 class="wp-block-heading">Installation</h2>



<ul class="wp-block-list">
<li>建立 estateui 資料夾</li>



<li><a href="https://github.com/safak/react-estate-ui" target="_blank" rel="noreferrer noopener">Source Code</a><br>可以使用 git clone 下載開始專案<br>git clone –single-branch -b starter web URL</li>



<li>建議別依賴單一技術，因為在工作上會被要求不同的技術</li>



<li>安裝 sass 套件 – npm i sass</li>



<li>在 src 資料夾裡面建立 index.scss 檔案</li>



<li>修改 main.jsx 檔案</li>



<li>修改 index.scss 檔案</li>



<li>修改 App.jsx 檔案</li>



<li>提到需要知道 CSS 相關知識</li>
</ul>



<pre class="wp-block-code"><code>// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.scss'

ReactDOM.createRoot(document.getElementById('root')).render(
  &lt;React.StrictMode&gt;
    &lt;App /&gt;
  &lt;/React.StrictMode&gt;,
)
</code></pre>



<pre class="wp-block-code"><code>// index.scss
* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

a {
  text-decoration: none;
  color: inherit;
}

body {
  font-family: 'Lato', sans-serif;
  overflow: hidden;
}
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
function App() {
  return (
    &lt;div&gt;&lt;a href="/"&gt;Hello&lt;/a&gt;&lt;/div&gt;
  )
}

export default App
</code></pre>



<h2 class="wp-block-heading">Responsive Layout with CSS</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>



<li>在 src 資料夾裡面建立 layout.scss 檔案</li>



<li>在 src 資料夾裡面建立 responsive.scss 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
import "./layout.scss"

function App() {
  return (
    &lt;div className="layout"&gt;
      Hello
    &lt;/div&gt;
  )
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// layout.scss
@import "./responsive.scss";

.layout {
  height: 100vh;
  max-width: 1366px;
  margin-left: auto;
  margin-right: auto;
  padding-left: 20px;
  padding-right: 20px;

  @include lg{
    background-color: rgb(247, 210, 196);
    max-width: 1280px;
  }

  @include md{
    background-color: rgb(186, 203, 234);
    max-width: 768px;
  }

  @include sm{
    background-color: rgb(239, 200, 200);
    max-width: 640px;
  }
}
</code></pre>



<pre class="wp-block-code"><code>// responsive.scss
@mixin sm {
  @media(max-width: 738px) {
    @content;
  }
}

@mixin md {
  @media(max-width: 1024px) {
    @content;
  }
}

@mixin lg {
  @media(max-width: 1366px) {
    @content;
  }
}
</code></pre>



<h2 class="wp-block-heading">React Responsive Navbar Design</h2>



<ul class="wp-block-list">
<li>在 src 資料夾裡面建立 components 資料夾</li>



<li>在 components 資料夾裡面建立 Navbar.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>



<li>在 components 資料夾裡面建立 navbar.scss 檔案</li>



<li>在 components 資料夾裡面建立 navbar 資料夾</li>



<li>把 Navbar.jsx、navbar.scss 檔案移動到 navbar 資料夾裡面</li>



<li>修改 App.jsx 檔案 Navbar 匯入路徑</li>



<li>修改 Navbar.jsx 檔案</li>



<li>修改 navbar.scss 檔案</li>
</ul>



<pre class="wp-block-code"><code>// Navbar.jsx
import "./navbar.scss"

function Navbar() {
  return (
    &lt;nav&gt;
      &lt;div className="left"&gt;
        &lt;a href="/" className="logo"&gt;
          &lt;img src="/logo.png" /&gt;
          &lt;span&gt;LamaEstate&lt;/span&gt;
        &lt;/a&gt;
        &lt;a href="/"&gt;Home&lt;/a&gt;
        &lt;a href="/"&gt;About&lt;/a&gt;
        &lt;a href="/"&gt;Contact&lt;/a&gt;
        &lt;a href="/"&gt;Agents&lt;/a&gt;
      &lt;/div&gt;
      &lt;div className="right"&gt;
        &lt;a href="/"&gt;Sign in&lt;/a&gt;
        &lt;a href="/" className="register"&gt;Sign up&lt;/a&gt;
      &lt;/div&gt;
    &lt;/nav&gt;
  )
}

export default Navbar;
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import Navbar from './components/navbar/Navbar'
import "./layout.scss"

function App() {
  return (
    &lt;div className="layout"&gt;
      &lt;Navbar /&gt;
    &lt;/div&gt;
  )
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// navbar.scss
nav {
  height: 100px;
  display: flex;
  justify-content: space-between;
  align-items: center;

  a {
    transition: all 0.4s ease;

    &amp;:hover {
      scale: 1.05;
    }
  }

  .left{
    flex: 3;
    display: flex;
    align-items: center;
    gap: 50px;

    .logo {
      font-weight: bold;
      font-size: 20px;
      display: flex;
      align-items: center;
      gap: 10px;

      img {
        width: 28px;
      }
    }
  }
  .right{
    flex: 2;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    background-color: #fcf5f3;
    height: 100%;

    a {
      padding: 12px 24px;
      margin: 20px;
    }

    .register {
      background-color: #fece51;
    }
  }
}
</code></pre>



<h2 class="wp-block-heading">React Responsive Hamburger Menu Design</h2>



<ul class="wp-block-list">
<li>修改 navbar.scss 檔案</li>



<li>修改 Navbar.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// navbar.scss
@import "../../responsive.scss";

nav {
  height: 100px;
  display: flex;
  justify-content: space-between;
  align-items: center;

  a {
    transition: all 0.4s ease;

    @include sm {
      display: none;
    }

    &amp;:hover {
      scale: 1.05;
    }
  }

  .left{
    flex: 3;
    display: flex;
    align-items: center;
    gap: 50px;

    .logo {
      font-weight: bold;
      font-size: 20px;
      display: flex;
      align-items: center;
      gap: 10px;

      img {
        width: 28px;
      }

      span {
        @include md {
          display: none;
        }

        @include sm {
          display: initial;
        }
      }
    }
  }
  .right{
    flex: 2;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    background-color: #fcf5f3;
    height: 100%;

    @include md {
      background-color: transparent;
    }

    a {
      padding: 12px 24px;
      margin: 20px;
    }

    .register {
      background-color: #fece51;
    }

    .menuIcon {
      display: none;
      z-index: 999;

      img {
        width: 36px;
        height: 36px;
        cursor: pointer;
      }

      @include sm {
        display: inline;
      }
    }

    .menu {
      position: absolute;
      top: 0;
      right: -50%;
      background-color: black;
      color: white;
      height: 100vh;
      width: 50%;
      transition: all 1s ease;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-size: 24px;

      &amp;.active {
        right: 0;
      }

      @include sm {
        a {
          display: initial;
        }
      }
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// Navbar.jsx
import { useState } from "react";
import "./navbar.scss"

function Navbar() {
  const &#91;open, setOpen] = useState(false);
  return (
    &lt;nav&gt;
      &lt;div className="left"&gt;
        &lt;a href="/" className="logo"&gt;
          &lt;img src="/logo.png" /&gt;
          &lt;span&gt;LamaEstate&lt;/span&gt;
        &lt;/a&gt;
        &lt;a href="/"&gt;Home&lt;/a&gt;
        &lt;a href="/"&gt;About&lt;/a&gt;
        &lt;a href="/"&gt;Contact&lt;/a&gt;
        &lt;a href="/"&gt;Agents&lt;/a&gt;
      &lt;/div&gt;
      &lt;div className="right"&gt;
        &lt;a href="/"&gt;Sign in&lt;/a&gt;
        &lt;a href="/" className="register"&gt;Sign up&lt;/a&gt;
        &lt;div className="menuIcon"&gt;
          &lt;img src="/menu.png" alt="" onClick={() =&gt; setOpen((prev) =&gt; !prev)} /&gt;
        &lt;/div&gt;
        &lt;div className={open ? "menu active" : "menu"}&gt;
          &lt;a href="/"&gt;Home&lt;/a&gt;
          &lt;a href="/"&gt;About&lt;/a&gt;
          &lt;a href="/"&gt;Contact&lt;/a&gt;
          &lt;a href="/"&gt;Agents&lt;/a&gt;
          &lt;a href="/"&gt;Sign in&lt;/a&gt;
          &lt;a href="/"&gt;Sign up&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/nav&gt;
  )
}

export default Navbar;
</code></pre>



<h2 class="wp-block-heading">Real Estate App Homepage Design</h2>



<ul class="wp-block-list">
<li>在 src 資料夾裡面建立 pages 資料夾或者 routes 資料夾<br>這裡選擇建立 routes 資料夾</li>



<li>在 routes 資料夾裡面建立 homePage 資料夾</li>



<li>在 homePage 資料夾裡面建立 homePage.jsx 檔案</li>



<li>在 homePage 資料夾裡面建立 homePage.scss 檔案</li>
</ul>



<h2 class="wp-block-heading">React.js VSCode Snippets</h2>



<ul class="wp-block-list">
<li>使用 fcs 片段快速建立程式碼<br>修改 homePage.jsx 檔案</li>



<li>介紹如何自訂片段程式碼設定 – <a href="https://github.com/safak/snippets" target="_blank" rel="noreferrer noopener">Snippets</a><br>修改 javascriptreact.json 檔案</li>



<li>Snippets: Configure User Snippets > javascriptreact.json</li>



<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// javascriptreact.json
{
    "fcs": {
        "prefix": "fcs",
        "body": &#91;
            "import './${TM_FILENAME_BASE/^(.)/${1:/downcase}/}.scss'"
            ""
            "function ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/capitalize}${2}/}}(){",
            "  return (",
            "    &lt;div className='${TM_FILENAME_BASE/^(.)/${1:/downcase}/}'&gt;${1:${TM_FILENAME_BASE/(.)(.*)/${1:/capitalize}${2}/}}&lt;/div&gt;",
            "  )",
            "}",
            "",
            "export default ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/capitalize}${2}/}}"
        ],
        "description": "Create a functional component with Sass"
    },
    "acs": {
	"prefix": "acs",
	"body": &#91;
            "import './${TM_FILENAME_BASE/^(.)/${1:/downcase}/}.scss'"
            ""
            "const ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/capitalize}${2}/}} = () =&gt; {",
            "  return (",
            "    &lt;div className='${TM_FILENAME_BASE/^(.)/${1:/downcase}/}'&gt;${1:${TM_FILENAME_BASE/(.)(.*)/${1:/capitalize}${2}/}}&lt;/div&gt;",
            "  )",
            "}",
            "",
            "export default ${1:${TM_FILENAME_BASE/(.)(.*)/${1:/capitalize}${2}/}}" 
	],
	"description": "Create an arrow component with Sass"
    }
}
</code></pre>



<pre class="wp-block-code"><code>// homePage.jsx
import './homePage.scss'

function HomePage(){
  return (
    &lt;div className='homePage'&gt;HomePage&lt;/div&gt;
  )
}

export default HomePage
</code></pre>



<h2 class="wp-block-heading">React Responsive Hero Section Design</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>



<li>修改 layout.scss 檔案<br>.content CSS 畫面高度滿版的方法有二<br>– 使用 height: calc(100vh – 100px);<br>– 使用 flex 方法 (這裡選擇這個方法)</li>



<li>修改 homePage.jsx 檔案</li>



<li>修改 homePage.scss 檔案</li>



<li>在 components 資料夾裡面建立 searchBar 資料夾</li>



<li>在 searchBar 資料夾裡面建立 SearchBar.jsx 檔案</li>



<li>使用 fcs 片段快速建立程式碼<br>修改 SearchBar.jsx 檔案</li>



<li>在 searchBar 資料夾裡面建立 searchBar.scss 檔案</li>



<li>修改 searchBar.scss 檔案</li>



<li>修改 homePage.jsx 檔案，加入 &lt;SearchBar />、匯入 SearchBar，增加 .boxes 內容</li>



<li>修改 homePage.scss 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
import Navbar from './components/navbar/Navbar'
import "./layout.scss"
import HomePage from './routes/homePage/homePage'

function App() {
  return (
    &lt;div className="layout"&gt;
      &lt;div className="navbar"&gt;
        &lt;Navbar /&gt;
      &lt;/div&gt;
      &lt;div className="content"&gt;
        &lt;HomePage /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// layout.scss
@import "./responsive.scss";

.layout {
  height: 100vh;
  max-width: 1366px;
  margin-left: auto;
  margin-right: auto;
  padding-left: 20px;
  padding-right: 20px;
  display: flex;
  flex-direction: column;

  @include lg{
    background-color: rgb(247, 210, 196);
    max-width: 1280px;
  }

  @include md{
    background-color: rgb(186, 203, 234);
    max-width: 768px;
  }

  @include sm{
    background-color: rgb(239, 200, 200);
    max-width: 640px;
  }

  .content {
    flex: 1;
  }
}
</code></pre>



<pre class="wp-block-code"><code>// homePage.jsx
import SearchBar from "../../components/searchBar/SearchBar";
import './homePage.scss';

function HomePage(){
  return (
    &lt;div className='homePage'&gt;
      &lt;div className="textContainer"&gt;
        &lt;div className="wrapper"&gt;
          &lt;h1 className='title'&gt;Find Real Estate &amp; Get Your Dream Place&lt;/h1&gt;
          &lt;p&gt;
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Eos
            explicabo suscipit cum eius, iure est nulla animi consequatur
            facilis id pariatur fugit quos laudantium temporibus dolor ea
            repellat provident impedit!
          &lt;/p&gt;
          &lt;SearchBar /&gt;
          &lt;div className="boxes"&gt;
            &lt;div className="box"&gt;
              &lt;h1&gt;16+&lt;/h1&gt;
              &lt;h2&gt;Years of Experience&lt;/h2&gt;
            &lt;/div&gt;
            &lt;div className="box"&gt;
              &lt;h1&gt;200&lt;/h1&gt;
              &lt;h2&gt;Award Gained&lt;/h2&gt;
            &lt;/div&gt;
            &lt;div className="box"&gt;
              &lt;h1&gt;1200+&lt;/h1&gt;
              &lt;h2&gt;Property Ready&lt;/h2&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div className="imgContainer"&gt;
        &lt;img src="/bg.png" alt="" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default HomePage
</code></pre>



<pre class="wp-block-code"><code>// homePage.scss
.homePage {
  display: flex;
  height: 100%;

  .textContainer {
    flex: 3;

    .wrapper {
      padding-right: 100px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      gap: 50px;
      height: 100%;

      .title {
        font-size: 64px;
      }

      .boxes {
        display: flex;
        justify-content: space-between;

        h1 {
          font-size: 36px;
        }

        h2 {
          font-size: 20px;
          font-weight: 300;
        }
      }
    }
  }

  .imgContainer {
    flex: 2;
    background-color: #fcf5f3;
    position: relative;
    display: flex;
    align-items: center;

    img {
      width: 115%;
      position: absolute;
      right: 0;
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// SearchBar.jsx
import './searchBar.scss'

function SearchBar(){
  return (
    &lt;div className='searchBar'&gt;SearchBar&lt;/div&gt;
  )
}

export default SearchBar
</code></pre>



<pre class="wp-block-code"><code>// searchBar.scss
.searchBar {
  
}
</code></pre>



<h2 class="wp-block-heading">Search Bar Design with React &amp; CSS</h2>



<ul class="wp-block-list">
<li>修改 SearchBar.jsx 檔案</li>



<li>修改 searchBar.scss 檔案</li>



<li>修改 homePage.scss 檔案</li>



<li>修改 layout.scss 檔案</li>
</ul>



<pre class="wp-block-code"><code>// SearchBar.jsx
import { useState } from 'react'
import './searchBar.scss'

const types = &#91;"buy", "rent"];

function SearchBar(){
  const &#91;query, setQuery] = useState({
    type: "buy",
    location: "",
    minPrice: 0,
    maxPrice: 0,
  });

  const switchType = (val) =&gt; {
    setQuery(prev =&gt; ({ ...prev, type: val }));
  };


  return (
    &lt;div className='searchBar'&gt;
      &lt;div className="type"&gt;
        {types.map((type) =&gt; (
          &lt;button key={type} onClick={() =&gt; switchType(type)} className={query.type === type ? "active" : ""}&gt;
            {type}
          &lt;/button&gt;
        ))}
      &lt;/div&gt;
      &lt;form&gt;
        &lt;input type="text" name='location' placeholder='City Location' /&gt;
        &lt;input
          type="number"
          name='minPrice'
          min={0}
          max={10000000}
          placeholder='Min Price'
        /&gt;
        &lt;input
          type="number"
          name='maxPrice'
          min={0}
          max={10000000}
          placeholder='Max Price'
        /&gt;
        &lt;button&gt;
          &lt;img src="/search.png" alt="" /&gt;
        &lt;/button&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  )
}

export default SearchBar
</code></pre>



<pre class="wp-block-code"><code>// searchBar.scss
@import "../../responsive.scss";

.searchBar {
  .type {


    button {
      padding: 16px 36px;
      border: 1px solid #999;
      border-bottom: none;
      cursor: pointer;
      background-color: white;
      text-transform: capitalize;

      &amp;.active {
        background-color: black;
        color: white;
      }

      &amp;:first-child {
        border-top-left-radius: 5px;
        border-right: none;
      }

      &amp;:last-child {
        border-top-right-radius: 5px;
        border-left: none;
      }
    }
  }

  form {
    border: 1px solid #999;
    display: flex;
    justify-content: space-between;
    height: 64px;
    gap: 5px;

    @include sm {
      flex-direction: column;
      border: none;
    }

    input {
      border: none;
      padding: 0px 10px;
      width: 200px;

      @include lg {
        padding: 0px 5px;

        &amp;:nth-child(2), &amp;:nth-child(3) {
          width: 140px;
        }
      }

      @include md {
        width: 200px;
        &amp;:nth-child(2),
        &amp;:nth-child(3) {
          width: 200px;
        }
      }

      @include sm {
        width: auto;
        &amp;:nth-child(2),
        &amp;:nth-child(3) {
          width: auto;
        }
        padding: 20px;
        border: 1px solid #999;
      }
    }

    button {
      border: none;
      cursor: pointer;
      background-color: #fece51;
      flex: 1;

      @include sm {
        padding: 10px;
      }

      img {
        width: 24px;
        height: 24px;
      }
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// homePage.scss
@import "../../responsive.scss";

.homePage {
  display: flex;
  height: 100%;

  .textContainer {
    flex: 3;

    .wrapper {
      padding-right: 100px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      gap: 50px;
      height: 100%;

      @include md {
        padding: 0;
      }

      @include sm {
        justify-content: flex-start;
      }

      .title {
        font-size: 64px;

        @include lg {
          font-size: 48px;
        }
      }

      .boxes {
        display: flex;
        justify-content: space-between;

        @include sm {
          display: none;
        }

        h1 {
          font-size: 36px;

          @include lg {
            font-size: 32px;
          }
        }

        h2 {
          font-size: 20px;
          font-weight: 300;
        }
      }
    }
  }

  .imgContainer {
    flex: 2;
    background-color: #fcf5f3;
    position: relative;
    display: flex;
    align-items: center;

    @include md {
      display: none;
    }

    img {
      width: 115%;
      position: absolute;
      right: 0;

      @include lg {
        width: 105%;
      }
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/layout.scss
@import "./responsive.scss";

.layout {
  height: 100vh;
  max-width: 1366px;
  margin-left: auto;
  margin-right: auto;
  padding-left: 20px;
  padding-right: 20px;
  display: flex;
  flex-direction: column;

  @include lg{
    // background-color: rgb(247, 210, 196);
    max-width: 1280px;
  }

  @include md{
    // background-color: rgb(186, 203, 234);
    max-width: 768px;
  }

  @include sm{
    // background-color: rgb(239, 200, 200);
    max-width: 640px;
  }

  .content {
    flex: 1;
  }
}
</code></pre>



<h2 class="wp-block-heading">React Router Dom Tutorial 2024</h2>



<ul class="wp-block-list">
<li>在 routes 資料夾裡面建立 listPage 資料夾</li>



<li>在 listPage 資料夾裡面建立 listPage.jsx 檔案</li>



<li>修改 listPage.jsx 檔案，使用程式碼片段 fcs 快速建立</li>



<li>在 listPage 資料夾裡面建立 listPage.scss 檔案</li>



<li>修改 listPage.scss 檔案</li>



<li>講解使用 React Router</li>



<li>安裝 React Router 套件 – npm i react-router-dom</li>



<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/routes/listPage/listPage.jsx
import './listPage.scss'

function ListPage(){
  return (
    &lt;div className='listPage'&gt;ListPage&lt;/div&gt;
  )
}

export default ListPage
</code></pre>



<pre class="wp-block-code"><code>// src/routes/listPage/listPage.scss
.listPage {

}
</code></pre>



<pre class="wp-block-code"><code>// src/App.jsx
import Navbar from './components/navbar/Navbar'
import "./layout.scss"
import HomePage from './routes/homePage/homePage'
import {
  createBrowserRouter,
  RouterProvider,
  Route,
  Link,
} from 'react-router-dom'
import ListPage from './routes/listPage/listPage'

function App() {

  const router = createBrowserRouter(&#91;
    {
      path: "/",
      element: &lt;HomePage /&gt;
    },
    {
      path: "/list",
      element: &lt;ListPage /&gt;
    },
  ]);

  return (
    // &lt;div className="layout"&gt;
    //   &lt;div className="navbar"&gt;
    //     &lt;Navbar /&gt;
    //   &lt;/div&gt;
    //   &lt;div className="content"&gt;
    //     &lt;HomePage /&gt;
    //   &lt;/div&gt;
    // &lt;/div&gt;
    &lt;RouterProvider router={router} /&gt;
  )
}

export default App
</code></pre>



<h2 class="wp-block-heading">React Router Dom Outlet Tutorial</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>



<li>在 routes 資料夾裡面建立 layout 資料夾</li>



<li>在 layout 資料夾裡面建立 layout.jsx 檔案</li>



<li>在 layout 資料夾裡面建立 layout.scss 檔案</li>



<li>修改 layout.scss 檔案</li>



<li>修改 layout.jsx 檔案，使用程式碼片段 fcs 快速建立程式碼</li>



<li>修改 App.jsx 檔案</li>



<li>修改 layout.jsx 檔案</li>



<li>到根目錄 layout.scss 檔案複製程式碼</li>



<li>修改 layout.scss 檔案，貼上程式碼</li>



<li>刪除根目錄 layout.scss 檔案</li>



<li>修改 App.jsx 檔案</li>



<li>在 routes 資料夾裡面建立 singlePage 資料夾</li>



<li>在 singlePage 資料夾裡面建立 singlePage.jsx 檔案</li>



<li>修改 singlePage.jsx 檔案，使用程式碼片段 fcs 快速建立</li>



<li>在 singlePage 資料夾裡面建立 singlePage.scss 檔案</li>



<li>在 routes 資料夾裡面建立 login 資料夾</li>



<li>在 login 資料夾裡面建立 login.jsx 檔案</li>



<li>在 login 資料夾裡面建立 login.scss 檔案</li>



<li>修改 login.jsx 檔案，使用程式碼片段 fcs 快速建立</li>



<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/App.jsx
import HomePage from './routes/homePage/homePage'
import {
  createBrowserRouter,
  RouterProvider,
} from 'react-router-dom'
import ListPage from './routes/listPage/listPage'
import Layout from './routes/layout/layout'
import SinglePage from './routes/singlePage/singlePage'

function App() {

  const router = createBrowserRouter(&#91;
    {
      path: "/",
      element: &lt;Layout /&gt;,
      children: &#91;
        {
          path: "/",
          element: &lt;HomePage /&gt;
        },
        {
          path: "/list",
          element: &lt;ListPage /&gt;
        },
        {
          path: "/:id",
          element: &lt;SinglePage /&gt;
        },
      ]
    },
  ]);

  return (
    &lt;RouterProvider router={router} /&gt;
  )
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// src/routes/layout/layout.jsx
import './layout.scss'
import Navbar from '../../components/navbar/Navbar'
import { Outlet } from 'react-router-dom'

function Layout(){
  return (
    &lt;div className="layout"&gt;
      &lt;div className="navbar"&gt;
        &lt;Navbar /&gt;
      &lt;/div&gt;
      &lt;div className="content"&gt;
        &lt;Outlet /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default Layout
</code></pre>



<pre class="wp-block-code"><code>// src/routes/layout/layout.scss
@import "../../responsive.scss";

.layout {
  height: 100vh;
  max-width: 1366px;
  margin-left: auto;
  margin-right: auto;
  padding-left: 20px;
  padding-right: 20px;
  display: flex;
  flex-direction: column;

  @include lg{
    // background-color: rgb(247, 210, 196);
    max-width: 1280px;
  }

  @include md{
    // background-color: rgb(186, 203, 234);
    max-width: 768px;
  }

  @include sm{
    // background-color: rgb(239, 200, 200);
    max-width: 640px;
  }

  .content {
    flex: 1;
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/routes/singlePage/singlePage.jsx
import './singlePage.scss'

function SinglePage(){
  return (
    &lt;div className='singlePage'&gt;SinglePage&lt;/div&gt;
  )
}

export default SinglePage
</code></pre>



<pre class="wp-block-code"><code>// src/routes/singlePage/singlePage.scss
.singlePage {

}
</code></pre>



<pre class="wp-block-code"><code>// src/routes/login/login.jsx
import './login.scss'

function Login(){
  return (
    &lt;div className='login'&gt;Login&lt;/div&gt;
  )
}

export default Login
</code></pre>



<pre class="wp-block-code"><code>// src/routes/login/login.scss
.login {

}
</code></pre>



<h2 class="wp-block-heading">React Responsive List Page Design</h2>



<ul class="wp-block-list">
<li>修改 listPage.jsx 檔案</li>



<li>修改 listPage.scss 檔案</li>



<li>在 src 資料夾裡面建立 lib 資料夾</li>



<li>在 lib 資料夾裡面建立 dummydata.js 檔案</li>



<li>修改 dummydata.js 檔案，複製程式碼貼上</li>



<li>修改 listPage.jsx 檔案</li>



<li>修改 listPage.scss 檔案</li>



<li>修改 listPage.jsx 檔案</li>



<li>修改 listPage.scss 檔案</li>



<li>在 components 資料夾裡面建立 filter 資料夾</li>



<li>在 filter 資料夾裡面建立 Filter.jsx 檔案</li>



<li>在 filter 資料夾裡面建立 filter.scss 檔案</li>



<li>修改 Filter.jsx 檔案，使用程式碼片段 fcs 快速建立</li>



<li>修改 filter.scss 檔案</li>



<li>修改 listPage.jsx 檔案</li>



<li>在 components 資料夾裡面建立 card 資料夾</li>



<li>在 card 資料夾裡面建立 Card.jsx 檔案</li>



<li>在 card 資料夾裡面建立 card.scss 檔案</li>



<li>修改 listPage.jsx 檔案</li>



<li>修改 Filter.jsx 檔案</li>



<li>修改 filter.scss 檔案</li>



<li>修改 listPage.scss 檔案</li>



<li>修改 Card.jsx 檔案</li>



<li>修改 Navbar.jsx 檔案<br>Change the &lt;a> tags in your Navbar component as well</li>



<li>修改 Card.jsx 檔案</li>



<li>修改 searchBar.scss 檔案，關於 scss 除錯</li>



<li>修改 Card.jsx 檔案</li>



<li>修改 card.scss 檔案</li>



<li>修改 Card.jsx 檔案</li>



<li>修改 dummydata.js 檔案</li>



<li>修改 card.scss 檔案</li>



<li>修改 listPage.scss 檔案</li>



<li>修改 layout.scss 檔案</li>



<li>修改 listPage.scss 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/routes/listPage/listPage.jsx
import { listData } from '../../lib/dummydata'
import './listPage.scss'
import Filter from '../../components/filter/Filter'
import Card from '../../components/card/Card'

function ListPage(){
  const data = listData;
  
  return (
    &lt;div className='listPage'&gt;
      &lt;div className="listContainer"&gt;
        &lt;div className="wrapper"&gt;
          &lt;Filter /&gt;
          { data.map(item =&gt; (
            &lt;Card key={item.id} item={item} /&gt;
          ))}
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div className="mapContainer"&gt;Map&lt;/div&gt;
    &lt;/div&gt;
  )
}

export default ListPage
</code></pre>



<pre class="wp-block-code"><code>// src/routes/listPage/listPage.scss
.listPage {
  display: flex;
  height: 100%;

  .listContainer {
    flex: 3;
    height: 100%;

    .wrapper {
      height: 100%;
      padding-right: 50px;
      display: flex;
      flex-direction: column;
      gap: 50px;
      overflow-y: scroll;
      padding-bottom: 50px;
    }
  }
  .mapContainer { flex: 2; background-color: #fcf5f3; }
}
</code></pre>



<pre class="wp-block-code"><code>// src/lib/dummydata.js
export const listData = &#91;
  {
    id: 1,
    title: "A Great Apartment Next to the Beach!",
    img: "https://images.pexels.com/photos/1918291/pexels-photo-1918291.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 2,
    bathroom: 1,
    price: 1000,
    address: "456 Park Avenue, London",
    latitude: 51.5074,
    longitude: -0.1278,
  },
  {
    id: 2,
    title: "An Awesome Apartment Near the Park! Almost too good to be true!",
    img: "https://images.pexels.com/photos/1428348/pexels-photo-1428348.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 3,
    bathroom: 2,
    price: 1500,
    address: "789 Oxford Street, London",
    latitude: 52.4862,
    longitude: -1.8904,
  },
  {
    id: 3,
    title: "A New Apartment in the City!",
    img: "https://images.pexels.com/photos/2062426/pexels-photo-2062426.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 1,
    bathroom: 1,
    price: 800,
    address: "101 Baker Street, London",
    latitude: 53.4808,
    longitude: -2.2426,
  },
  {
    id: 4,
    title: "Great Location! Great Price! Great Apartment!",
    img: "https://images.pexels.com/photos/2467285/pexels-photo-2467285.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 2,
    bathroom: 1,
    price: 1000,
    address: "234 Kingsway, London,",
    latitude: 53.8008,
    longitude: -1.5491,
  },
  {
    id: 5,
    title: "Apartment 5",
    img: "https://images.pexels.com/photos/276625/pexels-photo-276625.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 3,
    bathroom: 2,
    price: 1500,
    address: "567 Victoria Road, London",
    latitude: 53.4084,
    longitude: -2.9916,
  },
  {
    id: 6,
    title: "Apartment 6",
    img: "https://images.pexels.com/photos/271816/pexels-photo-271816.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 1,
    bathroom: 1,
    price: 800,
    address: "890 Regent Street, London",
    latitude: 54.9783,
    longitude: -1.6174,
  },
  {
    id: 7,
    title: "Apartment 7",
    img: "https://images.pexels.com/photos/2029667/pexels-photo-2029667.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 2,
    bathroom: 1,
    price: 1000,
    address: "112 Piccadilly, London",
    latitude: 53.3811,
    longitude: -1.4701,
  },
  {
    id: 8,
    title: "Apartment 8",
    img: "https://images.pexels.com/photos/276724/pexels-photo-276724.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 3,
    bathroom: 2,
    price: 1500,
    address: "8765 Main High Street, London",
    latitude: 51.4545,
    longitude: -2.5879,
  },
];

export const singlePostData = {
  id: 1,
  title: "Beautiful Apartment",
  price: 1200,
  images: &#91;
    "https://images.pexels.com/photos/1918291/pexels-photo-1918291.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    "https://images.pexels.com/photos/1428348/pexels-photo-1428348.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    "https://images.pexels.com/photos/2062426/pexels-photo-2062426.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    "https://images.pexels.com/photos/2467285/pexels-photo-2467285.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
  ],
  bedRooms: 2,
  bathroom: 1,
  size: 861,
  latitude: 51.5074,
  longitude: -0.1278,
  city: "London",
  address: "1234 Broadway St",
  school: "250m away",
  bus: "100m away",
  restaurant: "50m away",
  description:
    "Future alike hill pull picture swim magic chain seed engineer nest outer raise bound easy poetry gain loud weigh me recognize farmer bare danger. actually put square leg vessels earth engine matter key cup indeed body film century shut place environment were stage vertical roof bottom lady function breeze darkness beside tin view local breathe carbon swam declared magnet escape has from pile apart route coffee storm someone hold space use ahead sheep jungle closely natural attached part top grain your grade trade corn salmon trouble new bend most teacher range anybody every seat fifteen eventually",
};

export const userData = {
  id: 1,
  name: "John Doe",
  img: "https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
};
</code></pre>



<pre class="wp-block-code"><code>// src/components/filter/Filter.jsx
import './filter.scss'

function Filter(){
  return (
    &lt;div className='filter'&gt;
      &lt;h1&gt;Search results for &lt;b&gt;London&lt;/b&gt;&lt;/h1&gt;
      &lt;div className="top"&gt;
        &lt;div className="item"&gt;
          &lt;label htmlFor="city"&gt;Location&lt;/label&gt;
          &lt;input type="text" id='city' name='city' placeholder='City Location' /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div className="bottom"&gt;
      &lt;div className="item"&gt;
        &lt;label htmlFor="type"&gt;Type&lt;/label&gt;
        &lt;select name="type" id="type"&gt;
          &lt;option value=""&gt;any&lt;/option&gt;
          &lt;option value="buy"&gt;Buy&lt;/option&gt;
          &lt;option value="rent"&gt;Rent&lt;/option&gt;
        &lt;/select&gt;
      &lt;/div&gt;
      &lt;div className="item"&gt;
        &lt;label htmlFor="property"&gt;Property&lt;/label&gt;
        &lt;select name="property" id="property"&gt;
          &lt;option value=""&gt;any&lt;/option&gt;
          &lt;option value="apartment"&gt;Apartment&lt;/option&gt;
          &lt;option value="house"&gt;House&lt;/option&gt;
          &lt;option value="condo"&gt;Condo&lt;/option&gt;
          &lt;option value="land"&gt;Land&lt;/option&gt;
        &lt;/select&gt;
      &lt;/div&gt;
      &lt;div className="item"&gt;
        &lt;label htmlFor="minPrice"&gt;Min Price&lt;/label&gt;
        &lt;input type="number" id='minPrice' name='minPrice' placeholder='any' /&gt;
      &lt;/div&gt;
      &lt;div className="item"&gt;
        &lt;label htmlFor="maxPrice"&gt;Max Price&lt;/label&gt;
        &lt;input type="number" id='maxPrice' name='maxPrice' placeholder='any' /&gt;
      &lt;/div&gt;
      &lt;div className="item"&gt;
        &lt;label htmlFor="bedroom"&gt;Bedroom&lt;/label&gt;
        &lt;input type="text" id='bedroom' name='bedroom' placeholder='any' /&gt;
      &lt;/div&gt;
      &lt;button&gt;
        &lt;img src="/search.png" alt="" /&gt;
      &lt;/button&gt;

      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default Filter
</code></pre>



<pre class="wp-block-code"><code>// src/components/filter/filter.scss
.filter {
  display: flex;
  flex-direction: column;
  gap: 10px;

  h1 {
    font-weight: 300;
    font-size: 24px;
  }

  .item {
    display: flex;
    flex-direction: column;
    gap: 2px;

    label {
      font-size: 10px;
    }

    input,
    select {
      width: 100px;
      padding: 10px;
      border: 1px solid #e0e0e0;
      border-radius: 5px;
      font-size: 14px;
    }
  }

  .top {
    input {
      width: 100%;
    }
  }

  .bottom {
    display: flex;
    justify-content: space-between;
    gap: 20px;

    button {
      width: 100px;
      padding: 10px;
      border: none;
      cursor: pointer;
      background-color: #fece51;

      img {
        width: 24px;
        height: 24px;
      }
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/components/card/Card.jsx
import { Link } from 'react-router-dom'
import './card.scss'

function Card({ item }){
  return (
    &lt;div className='card'&gt;
      &lt;Link to={`/${item.id}`} className="imageContainer"&gt;
        &lt;img src={item.img} alt="" /&gt;
      &lt;/Link&gt;
      &lt;div className="textContainer"&gt;
        &lt;h2 className="title"&gt;
          &lt;Link to={`${item.id}`}&gt;{item.title}&lt;/Link&gt;
        &lt;/h2&gt;
        &lt;p className='address'&gt;
          &lt;img src="/pin.png" alt="" /&gt;
          &lt;span&gt;{item.address}&lt;/span&gt;
        &lt;/p&gt;
        &lt;p className='price'&gt;$ {item.price}&lt;/p&gt;
        &lt;div className="bottom"&gt;
          &lt;div className="features"&gt;
            &lt;div className="feature"&gt;
              &lt;img src="/bed.png" alt="" /&gt;
              &lt;span&gt;{item.bedroom} bedroom&lt;/span&gt;
            &lt;/div&gt;
            &lt;div className="feature"&gt;
              &lt;img src="/bath.png" alt="" /&gt;
              &lt;span&gt;{item.bathroom} bathroom&lt;/span&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;div className="icons"&gt;
            &lt;div className="icon"&gt;
              &lt;img src="/save.png" alt="" /&gt;
            &lt;/div&gt;
            &lt;div className="icon"&gt;
              &lt;img src="/chat.png" alt="" /&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default Card
</code></pre>



<pre class="wp-block-code"><code>// src/components/card/card.scss
.card {
  display: flex;
  gap: 20px;

  .imageContainer {
    flex: 2;
    height: 200px;

    img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      border-radius: 10px;
    }
  }

  .textContainer {
    flex: 3;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 10px;

    img {
      width: 16px;
      height: 16px;
    }

    .title {
      font-size: 20px;
      font-weight: 600;
      color: #444;
      transition: all 0.4s ease;

      &amp;:hover {
        color: #000;
        scale: 1.01;
      }
    }

    .address {
      font-size: 14px;
      display: flex;
      align-items: center;
      gap: 5px;
      color: #888;
    }

    .price {
      font-size: 20px;
      font-weight: 300;
      padding: 5px;
      border-radius: 5px;
      background-color: rgba(254, 205, 81, 0.438);
      width: max-content;
    }

    .bottom {
      display: flex;
      justify-content: space-between;
      gap: 10px;

      .features {
        display: flex;
        gap: 20px;
        font-size: 14px;

        .feature {
          display: flex;
          align-items: center;
          gap: 5px;
          background-color: whitesmoke;
          padding: 5px;
          border-radius: 5px;
        }
      }

      .icons {
        display: flex;
        gap: 20px;

        .icon {
          border: 1px solid #999;
          padding: 2px 5px;
          border-radius: 5px;
          cursor: pointer;
          display: flex;
          align-items: center;
          justify-content: center;

          &amp;:hover {
            background-color: lightgray;
          }
        }
      }
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/components/navbar/Navbar.jsx
import { Link } from "react-router-dom"
import { useState } from "react"
import "./navbar.scss"

function Navbar() {
  const &#91;open, setOpen] = useState(false);
  return (
    &lt;nav&gt;
      &lt;div className="left"&gt;
        &lt;Link to="/" className="logo"&gt;
          &lt;img src="/logo.png" /&gt;
          &lt;span&gt;LamaEstate&lt;/span&gt;
        &lt;/Link&gt;
        &lt;Link to="/"&gt;Home&lt;/Link&gt;
        &lt;Link to="/"&gt;About&lt;/Link&gt;
        &lt;Link to="/"&gt;Contact&lt;/Link&gt;
        &lt;Link to="/"&gt;Agents&lt;/Link&gt;
      &lt;/div&gt;
      &lt;div className="right"&gt;
        &lt;Link to="/"&gt;Sign in&lt;/Link&gt;
        &lt;Link to="/" className="register"&gt;Sign up&lt;/Link&gt;
        &lt;div className="menuIcon"&gt;
          &lt;img src="/menu.png" alt="" onClick={() =&gt; setOpen((prev) =&gt; !prev)} /&gt;
        &lt;/div&gt;
        &lt;div className={open ? "menu active" : "menu"}&gt;
          &lt;Link to="/"&gt;Home&lt;/Link&gt;
          &lt;Link to="/"&gt;About&lt;/Link&gt;
          &lt;Link to="/"&gt;Contact&lt;/Link&gt;
          &lt;Link to="/"&gt;Agents&lt;/Link&gt;
          &lt;Link to="/"&gt;Sign in&lt;/Link&gt;
          &lt;Link to="/"&gt;Sign up&lt;/Link&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/nav&gt;
  )
}

export default Navbar;
</code></pre>



<pre class="wp-block-code"><code>// src/components/searchBar/searchBar.scss
@import "../../responsive.scss";

.searchBar {
  .type {


    button {
      padding: 16px 36px;
      border: 1px solid #999;
      border-bottom: none;
      cursor: pointer;
      background-color: white;
      text-transform: capitalize;

      &amp;.active {
        background-color: black;
        color: white;
      }

      &amp;:first-child {
        border-top-left-radius: 5px;
        border-right: none;
      }

      &amp;:last-child {
        border-top-right-radius: 5px;
        border-left: none;
      }
    }
  }

  form {
    border: 1px solid #999;
    display: flex;
    justify-content: space-between;
    height: 64px;
    gap: 5px;

    @include sm {
      flex-direction: column;
      border: none;
    }

    input {
      border: none;
      padding: 0px 10px;
      width: 200px;

      @include lg {
        padding: 0px 5px;

        &amp;:nth-child(2), &amp;:nth-child(3) {
          width: 140px;
        }
      }

      @include md {
        width: 200px;
        &amp;:nth-child(2),
        &amp;:nth-child(3) {
          width: 200px;
        }
      }

      @include sm {
        width: auto;
        &amp;:nth-child(2),
        &amp;:nth-child(3) {
          width: auto;
        }

        &amp; {
          padding: 20px;
          border: 1px solid #999;
        }
      }
    }

    button {
      border: none;
      cursor: pointer;
      background-color: #fece51;
      flex: 1;

      @include sm {
        padding: 10px;
      }

      img {
        width: 24px;
        height: 24px;
      }
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/routes/layout/layout.scss
@import "../../responsive.scss";

.layout {
  height: 100vh;
  max-width: 1366px;
  margin-left: auto;
  margin-right: auto;
  padding-left: 20px;
  padding-right: 20px;
  display: flex;
  flex-direction: column;

  @include lg{
    // background-color: rgb(247, 210, 196);
    max-width: 1280px;
  }

  @include md{
    // background-color: rgb(186, 203, 234);
    max-width: 768px;
  }

  @include sm{
    // background-color: rgb(239, 200, 200);
    max-width: 640px;
  }

  .content {
    // flex: 1;
    height: calc(100vh - 100px);
  }
}
</code></pre>



<h2 class="wp-block-heading">React Map Tutorial (Open Source Map Library)</h2>



<ul class="wp-block-list">
<li>講解會使用 Open Source Map</li>



<li>在 components 資料夾裡面建立 map 資料夾</li>



<li>在 map 資料夾裡面建立 Map.jsx 檔案</li>



<li>在 map 資料夾裡面建立 map.scss 檔案</li>



<li>修改 Map.jsx 檔案，使用程式碼片段 fcs 快速建立</li>
</ul>



<pre class="wp-block-code"><code>// src/components/map/Map.jsx
import './map.scss'

function Map(){
  return (
    &lt;div className='map'&gt;Map&lt;/div&gt;
  )
}

export default Map
</code></pre>



<pre class="wp-block-code"><code>// src/components/map/map.scss
.map {
  
}
</code></pre>



<h2 class="wp-block-heading">React Leaflet Map Tutorial</h2>



<ul class="wp-block-list">
<li>使用 React Leaflet</li>



<li>安裝 React Leaflet 套件 – npm install react-leaflet leaflet</li>



<li>修改 Map.jsx 檔案</li>



<li>修改 map.scss 檔案</li>



<li>修改 listPage.jsx 檔案</li>



<li>修改 listPage.scss 檔案</li>



<li>修改 Map.jsx 檔案，匯入 leaflet.css 檔案</li>



<li>修改 listPage.jsx 檔案，items</li>



<li>修改 Map.jsx 檔案，items</li>



<li>在 components 資料夾裡面建立 pin 資料夾</li>



<li>在 pin 資料夾裡面建立 Pin.jsx 檔案</li>



<li>在 pin 資料夾裡面建立 pin.scss 檔案</li>



<li>修改 Pin.jsx 檔案，使用程式碼片段 fcs 快速建立</li>



<li>修改 pin.scss 檔案</li>



<li>修改 Map.jsx 檔案</li>



<li>修改 Pin.jsx 檔案</li>



<li>修改 Map.jsx 檔案</li>



<li>修改 pin.scss 檔案</li>



<li>修改 Pin.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/map/Map.jsx
import { MapContainer, TileLayer } from 'react-leaflet'
import './map.scss'
import 'leaflet/dist/leaflet.css'
import Pin from '../pin/Pin'

function Map({items}){
  return (
    &lt;MapContainer center={&#91;52.4797, -1.90269]} zoom={7} scrollWheelZoom={false} className='map'&gt;
      &lt;TileLayer
        attribution='&amp;copy; &lt;a href="https://www.openstreetmap.org/copyright"&gt;OpenStreetMap&lt;/a&gt; contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      /&gt;
      {items.map(item=&gt;(
        &lt;Pin item={item} key={item.id} /&gt;
      ))}
    &lt;/MapContainer&gt;
  )
}

export default Map
</code></pre>



<pre class="wp-block-code"><code>// src/components/map/map.scss
.map {
  width: 100%;
  height: 100%;
  border-radius: 20px;
}
</code></pre>



<pre class="wp-block-code"><code>// src/routes/listPage/listPage.jsx
import { listData } from '../../lib/dummydata'
import './listPage.scss'
import Filter from '../../components/filter/Filter'
import Card from '../../components/card/Card'
import Map from '../../components/map/Map';

function ListPage(){
  const data = listData;
  
  return (
    &lt;div className='listPage'&gt;
      &lt;div className="listContainer"&gt;
        &lt;div className="wrapper"&gt;
          &lt;Filter /&gt;
          { data.map(item =&gt; (
            &lt;Card key={item.id} item={item} /&gt;
          ))}
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div className="mapContainer"&gt;
        &lt;Map items={data} /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default ListPage
</code></pre>



<pre class="wp-block-code"><code>// src/routes/listPage/listPage.scss
.listPage {
  display: flex;
  height: 100%;

  .listContainer {
    flex: 3;
    height: 100%;

    .wrapper {
      height: 100%;
      padding-right: 50px;
      display: flex;
      flex-direction: column;
      gap: 50px;
      overflow-y: scroll;
      padding-bottom: 50px;
    }
  }
  .mapContainer {
    flex: 2;
    height: 100%;
    background-color: #fcf5f3;
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/components/pin/Pin.jsx
import { Marker, Popup } from 'react-leaflet'
import './pin.scss'
import { Link } from 'react-router-dom'

function Pin({ item }){
  return (
    &lt;Marker position={&#91;item.latitude, item.longitude]}&gt;
      &lt;Popup&gt;
        &lt;div className="popupContainer"&gt;
          &lt;img src={item.img} alt="" /&gt;
          &lt;div className="textContainer"&gt;
            &lt;Link to={`/${item.id}`}&gt;{item.title}&lt;/Link&gt;
            &lt;span&gt;{item.bedroom} bedroom&lt;/span&gt;
            &lt;b&gt;$ {item.price}&lt;/b&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/Popup&gt;
    &lt;/Marker&gt;
  )
}

export default Pin
</code></pre>



<pre class="wp-block-code"><code>// src/components/pin/pin.scss
.popupContainer {
  display: flex;
  gap: 20px;

  img {
    width: 64px;
    height: 48px;
    object-fit: cover;
    border-radius: 5px;
  }

  .textContainer {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }
}
</code></pre>



<h2 class="wp-block-heading">React.js Responsive Single Page Design</h2>



<ul class="wp-block-list">
<li>修改 singlePage.jsx 檔案</li>



<li>修改 singlePage.scss 檔案</li>



<li>在 components 資料夾裡面建立 slider 資料夾</li>



<li>在 slider 資料夾裡面建立 Slider.jsx 檔案</li>



<li>在 slider 資料夾裡面建立 slider.scss 檔案</li>



<li>修改 Slider.jsx 檔案，使用程式碼片段 fcs 快速建立</li>



<li>修改 singlePage.jsx 檔案</li>



<li>修改 dummydata.js 檔案，singlePostData、userData</li>



<li>修改 singlePage.jsx 檔案</li>



<li>修改 singlePage.scss 檔案</li>



<li>修改 singlePage.jsx 檔案</li>



<li>修改 singlePage.scss 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/routes/singlePage/singlePage.jsx
import './singlePage.scss'
import Slider from '../../components/slider/Slider'
import { singlePostData } from '../../lib/dummydata'
import { userData } from '../../lib/dummydata'

function SinglePage(){
  return (
    &lt;div className='singlePage'&gt;
      &lt;div className="details"&gt;
        &lt;div className="wrapper"&gt;
          &lt;Slider images={singlePostData.images} /&gt;
          &lt;div className="info"&gt;
            &lt;div className="top"&gt;
              &lt;div className="post"&gt;
                &lt;h1&gt;{singlePostData.title}&lt;/h1&gt;
                &lt;div className="address"&gt;
                  &lt;img src="/pin.png" alt="" /&gt;
                  &lt;span&gt;{singlePostData.address}&lt;/span&gt;
                &lt;/div&gt;
                &lt;div className="price"&gt;$ {singlePostData.price}&lt;/div&gt;
              &lt;/div&gt;
              &lt;div className="user"&gt;
                &lt;img src={userData.img} alt="" /&gt;
                &lt;span&gt;{userData.name}&lt;/span&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div className="bottom"&gt;
              {singlePostData.description}
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div className="features"&gt;
        &lt;div className="wrapper"&gt;&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default SinglePage
</code></pre>



<pre class="wp-block-code"><code>// src/routes/singlePage/singlePage.scss
.singlePage {
  display: flex;
  height: 100%;

  .details {
    flex: 3;

    .wrapper {
      padding-right: 50px;

      .info {
        .top {
          display: flex;
          justify-content: space-between;

          .post {
            display: flex;
            flex-direction: column;
            gap: 20px;
            h1 {
              font-weight: 400;
            }

            .address {
              display: flex;
              gap: 5px;
              align-items: center;
              color: #888;
              font-size: 14px;

              img {
                width: 16px;
                height: 16px;
              }
            }

            .price {
              padding: 5px;
              background-color: rgba(254, 205, 81, 0.438);
              border-radius: 5px;
              width: max-content;
              font-size: 20px;
              font-weight: 300;
            }
          }

          .user {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 20px;
            padding: 0px 50px;
            border-radius: 10px;
            background-color: rgba(254, 205, 81, 0.209);
            font-weight: 600;

            img {
              width: 50px;
              height: 50px;
              border-radius: 50%;
              object-fit: cover;
            }
          }
        }

        .bottom {
          margin-top: 50px;
          color: #555;
          line-height: 20px;
        }
      }
    }
  }

  .features {
    flex: 2;
    background-color: #fcf5f3;

    .wrapper {
      padding: 0px 20px;
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/components/slider/Slider.jsx
import './slider.scss'

function Slider(){
  return (
    &lt;div className='slider'&gt;Slider&lt;/div&gt;
  )
}

export default Slider
</code></pre>



<pre class="wp-block-code"><code>// src/components/slider/slider.scss
.slider {

}
</code></pre>



<pre class="wp-block-code"><code>// src/lib/dummydata.js
export const listData = &#91;
  {
    id: 1,
    title: "A Great Apartment Next to the Beach!",
    img: "https://images.pexels.com/photos/1918291/pexels-photo-1918291.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 2,
    bathroom: 1,
    price: 1000,
    address: "456 Park Avenue, London",
    latitude: 51.5074,
    longitude: -0.1278,
  },
  {
    id: 2,
    title: "An Awesome Apartment Near the Park! Almost too good to be true!",
    img: "https://images.pexels.com/photos/1428348/pexels-photo-1428348.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 3,
    bathroom: 2,
    price: 1500,
    address: "789 Oxford Street, London",
    latitude: 52.4862,
    longitude: -1.8904,
  },
  {
    id: 3,
    title: "A New Apartment in the City!",
    img: "https://images.pexels.com/photos/2062426/pexels-photo-2062426.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 1,
    bathroom: 1,
    price: 800,
    address: "101 Baker Street, London",
    latitude: 53.4808,
    longitude: -2.2426,
  },
  {
    id: 4,
    title: "Great Location! Great Price! Great Apartment!",
    img: "https://images.pexels.com/photos/2467285/pexels-photo-2467285.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 2,
    bathroom: 1,
    price: 1000,
    address: "234 Kingsway, London,",
    latitude: 53.8008,
    longitude: -1.5491,
  },
  {
    id: 5,
    title: "Apartment 5",
    img: "https://images.pexels.com/photos/276625/pexels-photo-276625.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 3,
    bathroom: 2,
    price: 1500,
    address: "567 Victoria Road, London",
    latitude: 53.4084,
    longitude: -2.9916,
  },
  {
    id: 6,
    title: "Apartment 6",
    img: "https://images.pexels.com/photos/271816/pexels-photo-271816.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 1,
    bathroom: 1,
    price: 800,
    address: "890 Regent Street, London",
    latitude: 54.9783,
    longitude: -1.6174,
  },
  {
    id: 7,
    title: "Apartment 7",
    img: "https://images.pexels.com/photos/2029667/pexels-photo-2029667.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 2,
    bathroom: 1,
    price: 1000,
    address: "112 Piccadilly, London",
    latitude: 53.3811,
    longitude: -1.4701,
  },
  {
    id: 8,
    title: "Apartment 8",
    img: "https://images.pexels.com/photos/276724/pexels-photo-276724.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    bedroom: 3,
    bathroom: 2,
    price: 1500,
    address: "8765 Main High Street, London",
    latitude: 51.4545,
    longitude: -2.5879,
  },
];

export const singlePostData = {
  id: 1,
  title: "Beautiful Apartment",
  price: 1200,
  images: &#91;
    "https://images.pexels.com/photos/1918291/pexels-photo-1918291.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    "https://images.pexels.com/photos/1428348/pexels-photo-1428348.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    "https://images.pexels.com/photos/2062426/pexels-photo-2062426.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
    "https://images.pexels.com/photos/2467285/pexels-photo-2467285.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
  ],
  bedRooms: 2,
  bathroom: 1,
  size: 861,
  latitude: 51.5074,
  longitude: -0.1278,
  city: "London",
  address: "1234 Broadway St",
  school: "250m away",
  bus: "100m away",
  restaurant: "50m away",
  description:
    "Future alike hill pull picture swim magic chain seed engineer nest outer raise bound easy poetry gain loud weigh me recognize farmer bare danger. actually put square leg vessels earth engine matter key cup indeed body film century shut place environment were stage vertical roof bottom lady function breeze darkness beside tin view local breathe carbon swam declared magnet escape has from pile apart route coffee storm someone hold space use ahead sheep jungle closely natural attached part top grain your grade trade corn salmon trouble new bend most teacher range anybody every seat fifteen eventually",
};

export const userData = {
  id: 1,
  name: "John Doe",
  img: "https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2",
};
</code></pre>



<h2 class="wp-block-heading">React.js Image Slider Tutorial From Scratch</h2>



<ul class="wp-block-list">
<li>修改 Slider.jsx 檔案</li>



<li>修改 slider.scss 檔案</li>



<li>修改 singlePage.scss 檔案</li>



<li>修改 Slider.jsx 檔案</li>



<li>修改 slider.scss 檔案</li>



<li>修改 Slider.jsx 檔案</li>



<li>修改 slider.scss 檔案</li>



<li>修改 Slider.jsx 檔案</li>



<li>修改 slider.scss 檔案</li>



<li>修改 Slider.jsx 檔案</li>



<li>修改 slider.scss 檔案</li>



<li>修改 Slider.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/slider/Slider.jsx
import { useState } from 'react'
import './slider.scss'

function Slider({ images }){
  const &#91;imageIndex, setImageIndex ] = useState(null);

  const changeSlide = (direction) =&gt; {
    if (direction==="left") {
      if (imageIndex === 0) {
        setImageIndex(images.length - 1);
      } else {
        setImageIndex(imageIndex - 1);
      }
    } else {
      if (imageIndex === images.length - 1) {
        setImageIndex(0);
      } else {
        setImageIndex(imageIndex + 1);
      }
    } 
  }

  return (
    &lt;div className='slider'&gt;
      { imageIndex !== null &amp;&amp; (
      &lt;div className="fullSlider"&gt;
        &lt;div className="arrow" onClick={() =&gt; changeSlide("left")}&gt;
          &lt;img src="/arrow.png" alt="" /&gt;
        &lt;/div&gt;
        &lt;div className="imgContainer"&gt;
          &lt;img src={images&#91;imageIndex]} alt="" /&gt;
        &lt;/div&gt;
        &lt;div className="arrow" onClick={() =&gt; changeSlide("right")}&gt;
          &lt;img src="/arrow.png" className='right' alt="" /&gt;
        &lt;/div&gt;
        &lt;div className="close" onClick={() =&gt; setImageIndex(null)}&gt;X&lt;/div&gt;
      &lt;/div&gt;
      )}
      &lt;div className="bigImage"&gt;
        &lt;img src={images&#91;0]} alt="" onClick={() =&gt; setImageIndex(0)} /&gt;
      &lt;/div&gt;
      &lt;div className="smallImage"&gt;
        {images.slice(1).map((image, index) =&gt; (
          &lt;img src={image} alt="" key={index} onClick={() =&gt; setImageIndex(index+1)}  /&gt;
        ))}
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default Slider
</code></pre>



<pre class="wp-block-code"><code>// src/components/slider/slider.scss
.slider {
  width: 100%;
  height: 350px;
  display: flex;
  gap: 20px;

  .fullSlider {
    position: absolute;
    width: 100vw;
    height: 100vh;
    top: 0;
    left: 0;
    background-color: black;
    display: flex;
    justify-content: space-between;
    align-items: center;

    .arrow {
      flex: 1;
      display: flex;
      align-items: center;
      justify-content: center;

      img {
        width: 50px;

        &amp;.right {
          transform: rotate(180deg);
        }
      }
    }

    .imgContainer {
      flex: 10;

      img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
    }

    .close {
      position: absolute;
      top: 0;
      right: 0;
      color: white;
      font-size: 36px;
      font-weight: bold;
      padding: 50px;
      cursor: pointer;
    }
  }

  img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 10px;
    cursor: pointer;
  }

  .bigImage {
    flex: 3;
  }

  .smallImage {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 20px;
    
    img {
      height: 100px;
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/components/singlePage.scss
.singlePage {
  display: flex;
  height: 100%;

  .details {
    flex: 3;

    .wrapper {
      padding-right: 50px;

      .info {
        margin-top: 50px;
        
        .top {
          display: flex;
          justify-content: space-between;

          .post {
            display: flex;
            flex-direction: column;
            gap: 20px;
            h1 {
              font-weight: 400;
            }

            .address {
              display: flex;
              gap: 5px;
              align-items: center;
              color: #888;
              font-size: 14px;

              img {
                width: 16px;
                height: 16px;
              }
            }

            .price {
              padding: 5px;
              background-color: rgba(254, 205, 81, 0.438);
              border-radius: 5px;
              width: max-content;
              font-size: 20px;
              font-weight: 300;
            }
          }

          .user {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 20px;
            padding: 0px 50px;
            border-radius: 10px;
            background-color: rgba(254, 205, 81, 0.209);
            font-weight: 600;

            img {
              width: 50px;
              height: 50px;
              border-radius: 50%;
              object-fit: cover;
            }
          }
        }

        .bottom {
          margin-top: 50px;
          color: #555;
          line-height: 20px;
        }
      }
    }
  }

  .features {
    flex: 2;
    background-color: #fcf5f3;

    .wrapper {
      padding: 0px 20px;
    }
  }
}
</code></pre>



<h2 class="wp-block-heading">Property Features Design</h2>



<ul class="wp-block-list">
<li>修改 singlePage.jsx 檔案</li>



<li>修改 singlePage.scss 檔案</li>



<li>修改 singlePage.jsx 檔案，修正程式碼</li>



<li>修改 singlePage.scss 檔案</li>



<li>修改 slider.scss 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/routes/singlePage/singlePage.jsx
import './singlePage.scss'
import Slider from '../../components/slider/Slider'
import Map from '../../components/map/Map'
import { singlePostData, userData } from '../../lib/dummydata'

function SinglePage(){
  return (
    &lt;div className='singlePage'&gt;
      &lt;div className="details"&gt;
        &lt;div className="wrapper"&gt;
          &lt;Slider images={singlePostData.images} /&gt;
          &lt;div className="info"&gt;
            &lt;div className="top"&gt;
              &lt;div className="post"&gt;
                &lt;h1&gt;{singlePostData.title}&lt;/h1&gt;
                &lt;div className="address"&gt;
                  &lt;img src="/pin.png" alt="" /&gt;
                  &lt;span&gt;{singlePostData.address}&lt;/span&gt;
                &lt;/div&gt;
                &lt;div className="price"&gt;$ {singlePostData.price}&lt;/div&gt;
              &lt;/div&gt;
              &lt;div className="user"&gt;
                &lt;img src={userData.img} alt="" /&gt;
                &lt;span&gt;{userData.name}&lt;/span&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div className="bottom"&gt;
              {singlePostData.description}
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div className="features"&gt;
        &lt;div className="wrapper"&gt;
          &lt;p className='title'&gt;General&lt;/p&gt;
          &lt;div className="listVertical"&gt;
            &lt;div className="feature"&gt;
              &lt;img src="/utility.png" alt="" /&gt;
              &lt;div className="featureText"&gt;
                &lt;span&gt;Utilities&lt;/span&gt;
                &lt;p&gt;Renter is responsible&lt;/p&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div className="feature"&gt;
              &lt;img src="/pet.png" alt="" /&gt;
              &lt;div className="featureText"&gt;
                &lt;span&gt;Pet Policy&lt;/span&gt;
                &lt;p&gt;Pets Allowed&lt;/p&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div className="feature"&gt;
              &lt;img src="/fee.png" alt="" /&gt;
              &lt;div className="featureText"&gt;
                &lt;span&gt;Property Fees&lt;/span&gt;
                &lt;p&gt;must have 3x the rent in total household income&lt;/p&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;p className='title'&gt;Room Sizes&lt;/p&gt;
          &lt;div className="sizes"&gt;
            &lt;div className="size"&gt;
              &lt;img src="/size.png" alt="" /&gt;
              &lt;span&gt;80 sqft&lt;/span&gt;
            &lt;/div&gt;
            &lt;div className="size"&gt;
              &lt;img src="/bed.png" alt="" /&gt;
              &lt;span&gt;2 beds&lt;/span&gt;
            &lt;/div&gt;
            &lt;div className="size"&gt;
              &lt;img src="/bath.png" alt="" /&gt;
              &lt;span&gt;1 bathroom&lt;/span&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;p className='title'&gt;Nearby Places&lt;/p&gt;
          &lt;div className="listHorizontal"&gt;
            &lt;div className="feature"&gt;
              &lt;img src="/school.png" alt="" /&gt;
              &lt;div className="featureText"&gt;
                &lt;span&gt;School&lt;/span&gt;
                &lt;p&gt;250m away&lt;/p&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div className="feature"&gt;
              &lt;img src="/pet.png" alt="" /&gt;
              &lt;div className="featureText"&gt;
                &lt;span&gt;Bus Stop&lt;/span&gt;
                &lt;p&gt;100m away&lt;/p&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div className="feature"&gt;
              &lt;img src="/fee.png" alt="" /&gt;
              &lt;div className="featureText"&gt;
                &lt;span&gt;Restaurant&lt;/span&gt;
                &lt;p&gt;200m away&lt;/p&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;p className='title'&gt;Location&lt;/p&gt;
          &lt;div className="mapContainer"&gt;
            &lt;Map items={&#91;singlePostData]} /&gt;
          &lt;/div&gt;
          &lt;div className="buttons"&gt;
            &lt;button&gt;
              &lt;img src="/chat.png" alt="" /&gt;
              Send a Message
            &lt;/button&gt;
            &lt;button&gt;
              &lt;img src="/save.png" alt="" /&gt;
              Save the Place
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default SinglePage
</code></pre>



<pre class="wp-block-code"><code>// src/routes/singlePage/singlePage.scss
@import '../../responsive.scss';

.singlePage {
  display: flex;
  height: 100%;

  @include md {
    flex-direction: column;
    overflow: scroll;
  }

  .details {
    flex: 3;
    height: 100%;
    overflow-y: scroll;

    @include md {
      flex: none;
      height: max-content;
      margin-bottom: 50px;
    }

    .wrapper {
      padding-right: 50px;

      @include lg {
        padding-right: 20px;
      }

      @include md {
        padding-right: 0px;
      }

      .info {
        margin-top: 50px;
        
        .top {
          display: flex;
          justify-content: space-between;

          .post {
            display: flex;
            flex-direction: column;
            gap: 20px;
            h1 {
              font-weight: 400;
            }

            .address {
              display: flex;
              gap: 5px;
              align-items: center;
              color: #888;
              font-size: 14px;

              img {
                width: 16px;
                height: 16px;
              }
            }

            .price {
              padding: 5px;
              background-color: rgba(254, 205, 81, 0.438);
              border-radius: 5px;
              width: max-content;
              font-size: 20px;
              font-weight: 300;
            }
          }

          .user {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            gap: 20px;
            padding: 0px 50px;
            border-radius: 10px;
            background-color: rgba(254, 205, 81, 0.209);
            font-weight: 600;

            img {
              width: 50px;
              height: 50px;
              border-radius: 50%;
              object-fit: cover;
            }
          }
        }

        .bottom {
          margin-top: 50px;
          color: #555;
          line-height: 20px;
        }
      }
    }
  }

  .features {
    flex: 2;
    background-color: #fcf5f3;
    height: 100%;
    overflow-y: scroll;

    @include md {
      flex: none;
      height: max-content;
      margin-bottom: 50px;
    }

    .wrapper {
      padding: 0px 20px;
      display: flex;
      flex-direction: column;
      gap: 20px;

      img {
        width: 24px;
        height: 24px;
      }

      .title {
        font-weight: bold;
        font-size: 18px;
        margin-bottom: 10px;
      }

      .feature {
        display: flex;
        align-items: center;
        gap: 10px;

        img {
          background-color: rgba(254, 205, 81, 0.209);
        }

        .featureText {
          span {
            font-weight: bold;
          }

          p {
            font-size: 14px;
          }
        }
      }

      .listVertical {
        display: flex;
        flex-direction: column;
        gap: 20px;
        padding: 20px;
        background-color: white;
        border-radius: 10px;
      }

      .listHorizontal {
        display: flex;
        justify-content: space-between;
        padding: 20px 10px;
        background-color: white;
        border-radius: 10px;
      }

      .sizes {
        display: flex;
        justify-content: space-between;

        @include lg {
          font-size: 12px;
        }

        .size {
          display: flex;
          align-items: center;
          gap: 10px;
          background-color: white;
          padding: 10px;
          border-radius: 5px;
        }
      }

      .mapContainer {
        width: 100%;
        height: 200px;
      }

      .buttons {
        display: flex;
        justify-content: space-between;

        button {
          padding: 20px;
          display: flex;
          align-items: center;
          gap: 5px;
          background-color: white;
          border: 1px solid #fece51;
          border-radius: 5px;
          cursor: pointer;

          img {
            width: 16px;
            height: 16px;
          }
        }
      }
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/components/slider/slider.scss
@import '../../responsive.scss';

.slider {
  width: 100%;
  height: 350px;
  display: flex;
  gap: 20px;

  .fullSlider {
    position: absolute;
    width: 100vw;
    height: 100vh;
    top: 0;
    left: 0;
    background-color: black;
    display: flex;
    justify-content: space-between;
    align-items: center;
    z-index: 9999;

    .arrow {
      flex: 1;
      display: flex;
      align-items: center;
      justify-content: center;

      img {
        width: 50px;

        @include md {
          width: 30px;
        }

        @include sm {
          width: 20px;
        }

        &amp;.right {
          transform: rotate(180deg);
        }
      }
    }

    .imgContainer {
      flex: 10;

      img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
    }

    .close {
      position: absolute;
      top: 0;
      right: 0;
      color: white;
      font-size: 36px;
      font-weight: bold;
      padding: 50px;
      cursor: pointer;
    }
  }

  img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 10px;
    cursor: pointer;
  }

  .bigImage {
    flex: 3;
  }

  .smallImage {
    flex: 1;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 20px;
    
    img {
      height: 100px;
    }
  }
}
</code></pre>



<h2 class="wp-block-heading">React.js Responsive Profile Page Design</h2>



<ul class="wp-block-list">
<li>在 routes 資料夾裡面建立 profilePage 資料夾</li>



<li>在 profilePage 資料夾裡面建立 profilePage.jsx 檔案</li>



<li>在 profilePage 資料夾裡面建立 profilePage.scss 檔案</li>



<li>修改 profilePage.jsx 檔案，使用程式碼片段 fcs 快速建立</li>



<li>修改 profilePage.scss 檔案</li>



<li>修改 App.jsx 檔案</li>



<li>修改 Navbar.jsx 檔案</li>



<li>修改 navbar.scss 檔案</li>



<li>修改 Navbar.jsx 檔案</li>



<li>修改 navbar.scss 檔案</li>



<li>修改 Navbar.jsx 檔案</li>



<li>修改 navbar.scss 檔案</li>



<li>修改 profilePage.jsx 檔案</li>



<li>在 components 資料夾裡面建立 list 資料夾</li>



<li>在 list 資料夾裡面建立 List.jsx 檔案</li>



<li>在 list 資料夾裡面建立 list.scss 檔案</li>



<li>修改 List.jsx 檔案，使用程式碼片段 fcs 快速建立</li>



<li>修改 list.scss 檔案</li>



<li>修改 List.jsx 檔案</li>



<li>修改 profilePage.jsx 檔案</li>



<li>修改 profilePage.scss 檔案</li>



<li>修改 list.scss 檔案</li>



<li>修改 profilePage.scss 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/routes/profilePage/profilePage.jsx
import List from '../../components/list/List'
import './profilePage.scss'

function ProfilePage(){
  return (
    &lt;div className='profilePage'&gt;
      &lt;div className="details"&gt;
        &lt;div className="wrapper"&gt;
          &lt;div className="title"&gt;
            &lt;h1&gt;User Information&lt;/h1&gt;
            &lt;button&gt;Update Profile&lt;/button&gt;
          &lt;/div&gt;
          &lt;div className="info"&gt;
            &lt;span&gt;
              Avatar:
              &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
            &lt;/span&gt;
            &lt;span&gt;Username: &lt;b&gt;John Doe&lt;/b&gt;&lt;/span&gt;
            &lt;span&gt;E-mail: &lt;b&gt;john@gmail.com&lt;/b&gt;&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className="title"&gt;
            &lt;h1&gt;My List&lt;/h1&gt;
            &lt;button&gt;Create New Post&lt;/button&gt;
          &lt;/div&gt;
          &lt;List /&gt;
          &lt;div className="title"&gt;
            &lt;h1&gt;Saved List&lt;/h1&gt;
          &lt;/div&gt;
          &lt;List /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div className="chatContainer"&gt;
        &lt;div className="wrapper"&gt;&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default ProfilePage
</code></pre>



<pre class="wp-block-code"><code>// src/routes/profilePage/profilePage.scss
.profilePage {
  display: flex;
  height: 100%;

  .details {
    flex: 3;
    overflow-y: scroll;
    padding-bottom: 50px;

    .wrapper {
      padding-right: 50px;
      display: flex;
      flex-direction: column;
      gap: 50px;

      .title {
        display: flex;
        align-items: center;
        justify-content: space-between;

        h1 {
          font-weight: 300;
        }

        button {
          padding: 12px 24px;
          background-color: #fece51;
          cursor: pointer;
          border: none;
        }
      }

      .info {
        display: flex;
        flex-direction: column;
        gap: 20px;

        span {
          display: flex;
          align-items: center;
          gap: 20px;
        }

        img {
          width: 40px;
          height: 40px;
          border-radius: 50%;
          object-fit: cover;
        }
      }
    }
  }

  .chatContainer {
    flex: 2;
    background-color: #fcf5f3;
    height: 100%;

    .wrapper {
      padding: 0px 20px;
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/App.jsx
import HomePage from './routes/homePage/homePage'
import {
  createBrowserRouter,
  RouterProvider,
} from 'react-router-dom'
import ListPage from './routes/listPage/listPage'
import Layout from './routes/layout/layout'
import SinglePage from './routes/singlePage/singlePage'
import ProfilePage from './routes/profilePage/profilePage'

function App() {

  const router = createBrowserRouter(&#91;
    {
      path: "/",
      element: &lt;Layout /&gt;,
      children: &#91;
        {
          path: "/",
          element: &lt;HomePage /&gt;
        },
        {
          path: "/list",
          element: &lt;ListPage /&gt;
        },
        {
          path: "/:id",
          element: &lt;SinglePage /&gt;
        },
        {
          path: "/profile",
          element: &lt;ProfilePage /&gt;
        }
      ]
    },
  ]);

  return (
    &lt;RouterProvider router={router} /&gt;
  )
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// src/components/navbar/Navbar.jsx
import { Link } from "react-router-dom"
import { useState } from "react"
import "./navbar.scss"

function Navbar() {
  const &#91;open, setOpen] = useState(false);
  
  const user = true;
  return (
    &lt;nav&gt;
      &lt;div className="left"&gt;
        &lt;Link to="/" className="logo"&gt;
          &lt;img src="/logo.png" /&gt;
          &lt;span&gt;LamaEstate&lt;/span&gt;
        &lt;/Link&gt;
        &lt;Link to="/"&gt;Home&lt;/Link&gt;
        &lt;Link to="/"&gt;About&lt;/Link&gt;
        &lt;Link to="/"&gt;Contact&lt;/Link&gt;
        &lt;Link to="/"&gt;Agents&lt;/Link&gt;
      &lt;/div&gt;
      &lt;div className="right"&gt;
        {user ? (
          &lt;div className="user"&gt;
            &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
            &lt;span&gt;John Doe&lt;/span&gt;
            &lt;Link to="/profile" className="profile"&gt;
              &lt;div className="notification"&gt;3&lt;/div&gt;
              &lt;span&gt;Profile&lt;/span&gt;
            &lt;/Link&gt;
          &lt;/div&gt;
        ) : (
          &lt;&gt;
            &lt;Link to="/"&gt;Sign in&lt;/Link&gt;
            &lt;Link to="/" className="register"&gt;Sign up&lt;/Link&gt;
          &lt;/&gt;
        )}
        &lt;div className="menuIcon"&gt;
          &lt;img src="/menu.png" alt="" onClick={() =&gt; setOpen((prev) =&gt; !prev)} /&gt;
        &lt;/div&gt;
        &lt;div className={open ? "menu active" : "menu"}&gt;
          &lt;Link to="/"&gt;Home&lt;/Link&gt;
          &lt;Link to="/"&gt;About&lt;/Link&gt;
          &lt;Link to="/"&gt;Contact&lt;/Link&gt;
          &lt;Link to="/"&gt;Agents&lt;/Link&gt;
          &lt;Link to="/"&gt;Sign in&lt;/Link&gt;
          &lt;Link to="/"&gt;Sign up&lt;/Link&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/nav&gt;
  )
}

export default Navbar;
</code></pre>



<pre class="wp-block-code"><code>// src/components/navbar/navbar.scss
@import "../../responsive.scss";

nav {
  height: 100px;
  display: flex;
  justify-content: space-between;
  align-items: center;

  a {
    transition: all 0.4s ease;

    @include sm {
      display: none;
    }

    &amp;:hover {
      scale: 1.05;
    }
  }

  .left{
    flex: 3;
    display: flex;
    align-items: center;
    gap: 50px;

    .logo {
      font-weight: bold;
      font-size: 20px;
      display: flex;
      align-items: center;
      gap: 10px;

      img {
        width: 28px;
      }

      span {
        @include md {
          display: none;
        }

        @include sm {
          display: initial;
        }
      }
    }
  }
  .right{
    flex: 2;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    background-color: #fcf5f3;
    height: 100%;

    @include md {
      background-color: transparent;
    }

    a {
      padding: 12px 24px;
      margin: 20px;
    }

    .user {
      display: flex;
      align-items: center;
      font-weight: bold;

      img {
        width: 40px;
        height: 40px;
        border-radius: 50%;
        object-fit: cover;
        margin-right: 20px;
      }

      span {
        @include sm {
          display: none;
        }
      }

      .profile {
        padding: 12px 24px;
        background-color: #fece51;
        cursor: pointer;
        border: none;
        position: relative;

        .notification {
          position: absolute;
          top: -8px;
          right: -8px;
          background-color: red;
          color: white;
          border-radius: 50%;
          width: 26px;
          height: 26px;
          display: flex;
          align-items: center;
          justify-content: center;
        }
      }
    }

    .register {
      background-color: #fece51;
    }

    .menuIcon {
      display: none;
      z-index: 999;

      img {
        width: 36px;
        height: 36px;
        cursor: pointer;
      }

      @include sm {
        display: inline;
      }
    }

    .menu {
      position: absolute;
      top: 0;
      right: -50%;
      background-color: black;
      color: white;
      height: 100vh;
      width: 50%;
      transition: all 1s ease;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      font-size: 24px;

      &amp;.active {
        right: 0;
      }

      @include sm {
        a {
          display: initial;
        }
      }
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/components/list/List.jsx
import './list.scss'
import Card from '../card/Card'
import { listData } from '../../lib/dummydata'

function List(){
  return (
    &lt;div className='list'&gt;
      {listData.map(item=&gt;(
        &lt;Card key={item.id} item={item} /&gt;
      ))}
    &lt;/div&gt;
  )
}

export default List
</code></pre>



<pre class="wp-block-code"><code>// src/components/list/list.scss
.list {
  display: flex;
  flex-direction: column;
  gap: 50px;
}
</code></pre>



<h2 class="wp-block-heading">React.js Chat Component Design</h2>



<ul class="wp-block-list">
<li>在 components 資料夾裡面建立 chat 資料夾</li>



<li>在 chat 資料夾裡面建立 Chat.jsx 檔案</li>



<li>在 chat 資料夾裡面建立 chat.scss 檔案</li>



<li>修改 Chat.jsx 檔案，使用程式碼片段 fcs 快速建立</li>



<li>修改 chat.scss 檔案</li>



<li>修改 Chat.jsx 檔案</li>



<li>修改 chat.scss 檔案</li>



<li>修改 profilePage.jsx 檔案</li>



<li>修改 chat.scss 檔案</li>



<li>修改 profilePage.scss 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/Chat.jsx
import './chat.scss'

function Chat(){
  return (
    &lt;div className='chat'&gt;
      &lt;div className="messages"&gt;
        &lt;h1&gt;Messages&lt;/h1&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div className="chatBox"&gt;box&lt;/div&gt;
    &lt;/div&gt;
  )
}

export default Chat
</code></pre>



<pre class="wp-block-code"><code>// src/components/chat/chat.scss
.chat {
  height: 100%;
  display: flex;
  flex-direction: column;

  .messages {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 20px;
    overflow-y: scroll;

    h1 {
      font-weight: 300;
    }

    .message {
      background-color: white;
      padding: 20px;
      border-radius: 10px;
      display: flex;
      align-items: center;
      gap: 20px;
      cursor: pointer;

      img {
        width: 40px;
        height: 40px;
        border-radius: 50%;
        object-fit: cover;
      }

      span {
        font-weight: bold;
      }
    }
  }

  .chatBox {
    flex: 1;
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/routes/profilePage/profilePage.jsx
import Chat from '../../components/chat/Chat'
import List from '../../components/list/List'
import './profilePage.scss'

function ProfilePage(){
  return (
    &lt;div className='profilePage'&gt;
      &lt;div className="details"&gt;
        &lt;div className="wrapper"&gt;
          &lt;div className="title"&gt;
            &lt;h1&gt;User Information&lt;/h1&gt;
            &lt;button&gt;Update Profile&lt;/button&gt;
          &lt;/div&gt;
          &lt;div className="info"&gt;
            &lt;span&gt;
              Avatar:
              &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
            &lt;/span&gt;
            &lt;span&gt;Username: &lt;b&gt;John Doe&lt;/b&gt;&lt;/span&gt;
            &lt;span&gt;E-mail: &lt;b&gt;john@gmail.com&lt;/b&gt;&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className="title"&gt;
            &lt;h1&gt;My List&lt;/h1&gt;
            &lt;button&gt;Create New Post&lt;/button&gt;
          &lt;/div&gt;
          &lt;List /&gt;
          &lt;div className="title"&gt;
            &lt;h1&gt;Saved List&lt;/h1&gt;
          &lt;/div&gt;
          &lt;List /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div className="chatContainer"&gt;
        &lt;div className="wrapper"&gt;
          &lt;Chat /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default ProfilePage
</code></pre>



<pre class="wp-block-code"><code>// src/routes/profilePage/profilePage.scss
.profilePage {
  display: flex;
  height: 100%;

  .details {
    flex: 3;
    overflow-y: scroll;
    padding-bottom: 50px;

    .wrapper {
      padding-right: 50px;
      display: flex;
      flex-direction: column;
      gap: 50px;

      .title {
        display: flex;
        align-items: center;
        justify-content: space-between;

        h1 {
          font-weight: 300;
        }

        button {
          padding: 12px 24px;
          background-color: #fece51;
          cursor: pointer;
          border: none;
        }
      }

      .info {
        display: flex;
        flex-direction: column;
        gap: 20px;

        span {
          display: flex;
          align-items: center;
          gap: 20px;
        }

        img {
          width: 40px;
          height: 40px;
          border-radius: 50%;
          object-fit: cover;
        }
      }
    }
  }

  .chatContainer {
    flex: 2;
    background-color: #fcf5f3;
    height: 100%;

    .wrapper {
      padding: 0px 20px;
      height: 100%;
    }
  }
}
</code></pre>



<h2 class="wp-block-heading">React Messenger Chat Window Design</h2>



<ul class="wp-block-list">
<li>修改 Chat.jsx 檔案</li>



<li>修改 chat.scss 檔案</li>



<li>修改 Chat.jsx 檔案</li>



<li>修改 chat.scss 檔案</li>



<li>修改 profilePage.scss 檔案</li>



<li>修改 card.scss 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/chat/Chat.jsx
import { useState } from 'react'
import './chat.scss'

function Chat(){
  const &#91;chat, setChat] = useState(true);

  return (
    &lt;div className='chat'&gt;
      &lt;div className="messages"&gt;
        &lt;h1&gt;Messages&lt;/h1&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
        &lt;div className="message"&gt;
          &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
          &lt;span&gt;John Doe&lt;/span&gt;
          &lt;p&gt;Lorem ipsum dolor sit amet...&lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      {chat &amp;&amp; (
        &lt;div className="chatBox"&gt;
        &lt;div className="top"&gt;
          &lt;div className="user"&gt;
            &lt;img src="https://images.pexels.com/photos/91227/pexels-photo-91227.jpeg?auto=compress&amp;cs=tinysrgb&amp;w=1260&amp;h=750&amp;dpr=2" alt="" /&gt;
            John Doe
          &lt;/div&gt;
          &lt;span className="close" onClick={()=&gt;setChat(null)}&gt;X&lt;/span&gt;
        &lt;/div&gt;
        &lt;div className="center"&gt;
          &lt;div className="chatMessage"&gt;
            &lt;p&gt;Lorem ipsum dolor sit amet&lt;/p&gt;
            &lt;span&gt;1 hour ago&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className="chatMessage own"&gt;
            &lt;p&gt;Lorem ipsum dolor sit amet&lt;/p&gt;
            &lt;span&gt;1 hour ago&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className="chatMessage"&gt;
            &lt;p&gt;Lorem ipsum dolor sit amet&lt;/p&gt;
            &lt;span&gt;1 hour ago&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className="chatMessage own"&gt;
            &lt;p&gt;Lorem ipsum dolor sit amet&lt;/p&gt;
            &lt;span&gt;1 hour ago&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className="chatMessage"&gt;
            &lt;p&gt;Lorem ipsum dolor sit amet&lt;/p&gt;
            &lt;span&gt;1 hour ago&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className="chatMessage own"&gt;
            &lt;p&gt;Lorem ipsum dolor sit amet&lt;/p&gt;
            &lt;span&gt;1 hour ago&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className="chatMessage"&gt;
            &lt;p&gt;Lorem ipsum dolor sit amet&lt;/p&gt;
            &lt;span&gt;1 hour ago&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className="chatMessage own"&gt;
            &lt;p&gt;Lorem ipsum dolor sit amet&lt;/p&gt;
            &lt;span&gt;1 hour ago&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className="chatMessage"&gt;
            &lt;p&gt;Lorem ipsum dolor sit amet&lt;/p&gt;
            &lt;span&gt;1 hour ago&lt;/span&gt;
          &lt;/div&gt;
          &lt;div className="chatMessage own"&gt;
            &lt;p&gt;Lorem ipsum dolor sit amet&lt;/p&gt;
            &lt;span&gt;1 hour ago&lt;/span&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div className="bottom"&gt;
          &lt;textarea&gt;&lt;/textarea&gt;
          &lt;button&gt;Send&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      )}
    &lt;/div&gt;
  )
}

export default Chat
</code></pre>



<pre class="wp-block-code"><code>// src/components/chat/chat.scss
.chat {
  height: 100%;
  display: flex;
  flex-direction: column;

  .messages {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 20px;
    overflow-y: scroll;

    h1 {
      font-weight: 300;
    }

    .message {
      background-color: white;
      padding: 20px;
      border-radius: 10px;
      display: flex;
      align-items: center;
      gap: 20px;
      cursor: pointer;

      img {
        width: 40px;
        height: 40px;
        border-radius: 50%;
        object-fit: cover;
      }

      span {
        font-weight: bold;
      }
    }
  }

  .chatBox {
    flex: 1;
    background-color: white;
    display: flex;
    flex-direction: column;
    justify-content: space-between;

    .top {
      background-color: #f7c14b85;
      padding: 20px;
      font-weight: bold;
      display: flex;
      align-items: center;
      justify-content: space-between;

      .user {
        display: flex;
        align-items: center;
        gap: 20px;

        img {
          width: 30px;
          height: 30px;
          border-radius: 50%;
          object-fit: cover;
        }
      }

      .close {
        cursor: pointer;
      }
    }

    .center {
      height: 350px;
      overflow: scroll;
      padding: 20px;
      display: flex;
      flex-direction: column;
      gap: 20px;

      .chatMessage {
        width: 50%;
        
        &amp;.own {
          align-self: flex-end;
          text-align: right;
        }

        span {
          font-size: 12px;
          background-color: #f7c14b39;
          padding: 2px;
          border-radius: 5px;
        }
      }
    }

    .bottom {
      border-top: 2px solid #f7c14b85;
      height: 60px;
      display: flex;
      align-items: center;
      justify-content: space-between;

      textarea {
        flex: 3;
        height: 100%;
        border: none;
        padding: 20px;
      }
      
      button {
        flex: 1;
        background-color: #f7c14b85;
        height: 100%;
        border: none;
        cursor: pointer;
      }
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/routes/profilePage/profilePage.scss
@import '../../responsive.scss';

.profilePage {
  display: flex;
  height: 100%;

  @include md {
    flex-direction: column;
    overflow: scroll;
  }

  .details {
    flex: 3;
    overflow-y: scroll;
    padding-bottom: 50px;

    @include md {
      flex: none;
      height: max-content;
    }

    .wrapper {
      padding-right: 50px;
      display: flex;
      flex-direction: column;
      gap: 50px;

      .title {
        display: flex;
        align-items: center;
        justify-content: space-between;

        h1 {
          font-weight: 300;
        }

        button {
          padding: 12px 24px;
          background-color: #fece51;
          cursor: pointer;
          border: none;
        }
      }

      .info {
        display: flex;
        flex-direction: column;
        gap: 20px;

        span {
          display: flex;
          align-items: center;
          gap: 20px;
        }

        img {
          width: 40px;
          height: 40px;
          border-radius: 50%;
          object-fit: cover;
        }
      }
    }
  }

  .chatContainer {
    flex: 2;
    background-color: #fcf5f3;
    height: 100%;

    @include md {
      flex: none;
      height: max-content;
    }

    .wrapper {
      padding: 0px 20px;
      height: 100%;
    }
  }
}
</code></pre>



<pre class="wp-block-code"><code>// src/components/card/card.scss
@import '../../responsive.scss';

.card {
  display: flex;
  gap: 20px;

  .imageContainer {
    flex: 2;
    height: 200px;

    @include sm {
      display: none;
    }

    img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      border-radius: 10px;
    }
  }

  .textContainer {
    flex: 3;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 10px;

    img {
      width: 16px;
      height: 16px;
    }

    .title {
      font-size: 20px;
      font-weight: 600;
      color: #444;
      transition: all 0.4s ease;

      &amp;:hover {
        color: #000;
        scale: 1.01;
      }
    }

    .address {
      font-size: 14px;
      display: flex;
      align-items: center;
      gap: 5px;
      color: #888;
    }

    .price {
      font-size: 20px;
      font-weight: 300;
      padding: 5px;
      border-radius: 5px;
      background-color: rgba(254, 205, 81, 0.438);
      width: max-content;
    }

    .bottom {
      display: flex;
      justify-content: space-between;
      gap: 10px;

      .features {
        display: flex;
        gap: 20px;
        font-size: 14px;

        .feature {
          display: flex;
          align-items: center;
          gap: 5px;
          background-color: whitesmoke;
          padding: 5px;
          border-radius: 5px;
        }
      }

      .icons {
        display: flex;
        gap: 20px;

        .icon {
          border: 1px solid #999;
          padding: 2px 5px;
          border-radius: 5px;
          cursor: pointer;
          display: flex;
          align-items: center;
          justify-content: center;

          &amp;:hover {
            background-color: lightgray;
          }
        }
      }
    }
  }
}
</code></pre>



<h2 class="wp-block-heading">What’s Next?</h2>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Vue.js Crash Course 2024</title>
		<link>/wordpress_blog/vuejs-crash-2024/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Thu, 05 Sep 2024 08:48:10 +0000</pubDate>
				<category><![CDATA[Youtube]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=873</guid>

					<description><![CDATA[Learning From Youtube Channel: T [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Learning From Youtube Channel: Traversy Media<br>Video: Vue.js Crash Course 2024<br>Thank you.</p>



<p>Timestamps:<br>00:00:00 – Intro<br>00:02:19 – Daily.dev Sponsor<br>00:03:11 – What is Vue.js?<br>00:04:45 – Prerequisites<br>00:06:17 – Role of Frontend Frameworks<br>00:08:40 – Why Vue.js?<br>00:11:14 – Vue Components<br>00:13:39 – Getting Setup<br>00:15:40 – Using The Vue CDN<br>00:20:54 – Create-Vue Setup<br>00:22:58 – Exploring Folders &amp; Files<br>00:26:10 – Boilerplate Clean Up<br>00:26:50 – Component Structure<br>00:27:25 – Optinos API data() &amp; Interpolation<br>00:28:36 – v-if, v-else &amp; v-else-if Directives<br>00:30:43 – v-for Directive &amp; Looping<br>00:32:17 – v-bind Directive<br>00:33:36 – v-on Directive, Events &amp; Methods<br>00:35:55 – Composition API – Long Form<br>00:39:08 – ref() &amp; Reactive Values<br>00:40:35 – Composition API Short Form<br>00:42:41 – Forms &amp; v-model<br>00:46:38 – Delete task<br>00:48:36 – Lifecycle Methods<br>00:49:50 – onMounted &amp; Fetching Data<br>00:51:58 – Vue Jobs Project Start<br>00:52:26 – Tailwind CSS Setup<br>00:56:47 – Theme Files &amp; Images<br>00:58:16 – Navbar Component<br>01:01:20 – Hero Component<br>01:02:30 – Props<br>01:04:57 – HomeCards &amp; Card Container Component<br>01:10:20 – JobListings Component &amp; JSON Data<br>01:16:47 – JobListing Limit &amp; showButton Props<br>01:24:26 – computed() &amp; Truncate Description<br>01:30:41 – PrimeIcons<br>01:32:35 – Vue Router &amp; Home View<br>01:39:52 – Jobs View<br>01:41:55 – RouterLink<br>01:46:07 – Navbar Active Link<br>01:50:42 – Not Found Page<br>01:56:27 – JSON Server REST API<br>01:59:50 – Fetch Data For JobListings<br>02:03:42 – reactive() Function<br>02:05:15 – JobListings Refactor To reactive()<br>02:07:26 – Vue Spinner<br>02:09:50 – Fetch Single Job &amp; Display Data<br>02:19:06 – BackButton Component<br>02:21:03 – Proxying<br>02:23:54 – Add Job Page<br>02:32:20 – Save Job POST<br>02:37:15 – Toast Notifications<br>02:40:08 – Delete Job<br>02:44:14 – Edit Page<br>02:47:06 – Fetch Job To Edit<br>02:50:58 – Update Job<br>02:52:50 – Netlify Deployment</p>



<h3 class="wp-block-heading">Intro</h3>



<h3 class="wp-block-heading">Daily.dev Sponsor</h3>



<h3 class="wp-block-heading">What is Vue.js?</h3>



<ul class="wp-block-list">
<li>Progressive JS framework for building user interfaces &amp; SPAs</li>



<li>Designed to be simple, flexible and incrementally adoptable</li>



<li>Used for projects of all sizes</li>



<li>Reactive data-binding &amp; component-based architecture</li>
</ul>



<h3 class="wp-block-heading">Prerequisites</h3>



<ul class="wp-block-list">
<li>JavaScript Fundamentals (loops, functions, object, etc)</li>



<li>Events &amp; DOM Manipulation</li>



<li>Fetch API &amp; Basic HTTP</li>



<li>Arrow Functions, High-Order Array Methods, Destructuring, etc</li>



<li>NMP (Node Package Manager)</li>
</ul>



<h3 class="wp-block-heading">Role of Frontend Frameworks</h3>



<h4 class="wp-block-heading">The Role Of Frontend Frameworks</h4>



<h4 class="wp-block-heading">Enhanced UI/UX</h4>



<p>Makes it easy to create UIs that are dynamic and interactive</p>



<h4 class="wp-block-heading">Organization</h4>



<p>UI is broken into components with their own state and properties</p>



<h4 class="wp-block-heading">Performance</h4>



<p>Optimized for performance with features such as the virtual DOM</p>



<h4 class="wp-block-heading">Modularity</h4>



<p>Allow developers to break down their applications into smaller, reusable components</p>



<h3 class="wp-block-heading">Why Vue?</h3>



<ul class="wp-block-list">
<li>Simplicity &amp; Approachability</li>



<li>Flexibility</li>



<li>Performance &amp; Size</li>



<li>Component-Based Architecture</li>



<li>Active Community &amp; Rich Ecosystem</li>
</ul>



<h3 class="wp-block-heading">Vue Components</h3>



<ul class="wp-block-list">
<li>Reusable, self-contained pieces of code</li>



<li>Includes the logic/JS, dynamic HTML output &amp; scoped styling</li>



<li>Options API vs Composition API</li>
</ul>



<h3 class="wp-block-heading">Getting Setup</h3>



<ul class="wp-block-list">
<li>CDN – Include the script tag with the CDN url</li>



<li>Vue CLI – Command line interface for setting up Vue projects. This is no longer recommended</li>



<li>Create Vue – Uses Vite, which includes features like hot-reloading, out of the box TypeScript and an ecosystem of plugins</li>



<li>Nuxt.js &amp; Gridsome – SSR &amp; SSG frameworks that use Vue</li>
</ul>



<h3 class="wp-block-heading">Using The Vue CDN</h3>



<p><a rel="noreferrer noopener" href="https://vuejs.org/guide/quick-start.html#using-vue-from-cdn" target="_blank">Vue.js Quick Start – Using Vue from CDN</a></p>



<ul class="wp-block-list">
<li>建立 vue-test 資料夾</li>



<li>在 vue-test 資料夾裡面建立 index.html 檔案</li>



<li>修改 index.html 檔案</li>
</ul>



<pre class="wp-block-code"><code>// vue-test/index.html
&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;script src="https://unpkg.com/vue@3/dist/vue.global.js"&gt;&lt;/script&gt;
  &lt;title&gt;Vue Test&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div id="app"&gt;
    &lt;h1&gt;
      {{message}}
    &lt;/h1&gt;
    &lt;button @click="clickMe"&gt;Click Me&lt;/button&gt;
  &lt;/div&gt;

  &lt;script&gt;
    const app = Vue.createApp({
      data() {
        return {
          message: 'Hello From Vue!'
        }
      },
      methods: {
        clickMe() {
          console.log('Button Clicked!');
          this.message = 'Updated Message';
        },
      },
    });

    app.mount('#app');
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h3 class="wp-block-heading">Create-Vue Setup</h3>



<ul class="wp-block-list">
<li>使用終端機建立專案<br>npm create vue@latest vue-carsh-2024</li>



<li>Vue.js – The Progressive JavaScript Framework
<ul class="wp-block-list">
<li>Add TypeScript? No</li>



<li>Add JSX Support? No</li>



<li>Add Vue Router for Single Page Application development? No</li>



<li>Add Pinia for state management? No</li>



<li>Add Vitest for Unit Testing? No</li>



<li>Add an End-to-End Testing Solution?</li>



<li>Add ESLint for code quality? No</li>



<li>Add Vue DevTools 7 extensin for debugging? (experimental) No</li>
</ul>
</li>



<li>cd vue-crash-2024<br>npm install<br>npm run dev</li>



<li>安裝 VSCode 套件 – Vue – Official</li>
</ul>



<h3 class="wp-block-heading">Exploring Folders &amp; Files</h3>



<ul class="wp-block-list">
<li>講解 package.json 檔案</li>



<li>講解 vite.config.js 檔案</li>



<li>修改 vite.config.js 檔案</li>



<li>講解 index.html 檔案</li>



<li>修改 index.html 檔案</li>



<li>講解 src 資料夾以及裡面的檔案
<ul class="wp-block-list">
<li>main.js</li>



<li>App.vue</li>
</ul>
</li>



<li>使用終端機執行指令 – npm run dev</li>
</ul>



<pre class="wp-block-code"><code>// vite.config.js
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: &#91;
    vue(),
  ],
  server: {
    port: 3000,
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})
</code></pre>



<pre class="wp-block-code"><code>// index.html
&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;link rel="icon" href="/favicon.ico"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Vue Jobs&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id="app"&gt;&lt;/div&gt;
    &lt;script type="module" src="/src/main.js"&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h3 class="wp-block-heading">Boilerplate Clean Up</h3>



<ul class="wp-block-list">
<li>刪除在 components 資料夾裡面的檔案</li>



<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;template&gt;
  &lt;h1&gt;Vue Jobs&lt;/h1&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Component Structure</h3>



<h3 class="wp-block-heading">Options API data() &amp; Interpolation</h3>



<ul class="wp-block-list">
<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script&gt;
export default {
  data() {
    return {
      name: 'John Doe',
    };
  },
};
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;{{ name }}&lt;/h1&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">v-if, v-else &amp; v-else-if Directives</h3>



<ul class="wp-block-list">
<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script&gt;
export default {
  data() {
    return {
      name: 'John Doe',
      status: 'pending',
    }
  }
};
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;{{ name }}&lt;/h1&gt;
  &lt;p v-if="status === 'active'"&gt;User is active&lt;/p&gt;
  &lt;p v-else-if="status === 'pending'"&gt;User is pending&lt;/p&gt;
  &lt;p v-else&gt;User is inactive&lt;/p&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">v-for Directive &amp; Looping</h3>



<ul class="wp-block-list">
<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script&gt;
export default {
  data() {
    return {
      name: 'John Doe',
      status: 'pending',
      tasks: &#91;'Task One', 'Task Two', 'Task Three']
    }
  }
};
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;{{ name }}&lt;/h1&gt;
  &lt;p v-if="status === 'active'"&gt;User is active&lt;/p&gt;
  &lt;p v-else-if="status === 'pending'"&gt;User is pending&lt;/p&gt;
  &lt;p v-else&gt;User is inactive&lt;/p&gt;

  &lt;h3&gt;Tasks:&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="task in tasks" :key="task"&gt;{{ task }}&lt;/li&gt;
  &lt;/ul&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">v-bind Directive</h3>



<ul class="wp-block-list">
<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script&gt;
export default {
  data() {
    return {
      name: 'John Doe',
      status: 'pending',
      tasks: &#91;'Task One', 'Task Two', 'Task Three'],
      link: 'https://google.com/'
    }
  }
}
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;{{ name }}&lt;/h1&gt;
  &lt;p v-if="status === 'active'"&gt;User is active&lt;/p&gt;
  &lt;p v-else-if="status === 'pending'"&gt;User is pending&lt;/p&gt;
  &lt;p v-else&gt;User is inactive&lt;/p&gt;

  &lt;h3&gt;Tasks:&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="task in tasks" :key="task"&gt;{{ task }}&lt;/li&gt;
  &lt;/ul&gt;
  &lt;!-- &lt;a v-bind:href="link"&gt;Click for Google&lt;/a&gt; --&gt;
  &lt;a :href="link"&gt;Click for Google&lt;/a&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">v-on Directive, Events &amp; Methods</h3>



<ul class="wp-block-list">
<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script&gt;
export default {
  data() {
    return {
      name: 'John Doe',
      status: 'pending',
      tasks: &#91;'Task One', 'Task Two', 'Task Three'],
      link: 'https://google.com/'
    }
  },
  methods: {
    toggleStatus() {
      if (this.status === 'active') {
        this.status = 'pending';
      } else if (this.status === 'pending') {
        this.status = 'inactive';
      } else {
        this.status = 'active';
      }
    }
  }
};
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;{{ name }}&lt;/h1&gt;
  &lt;p v-if="status === 'active'"&gt;User is active&lt;/p&gt;
  &lt;p v-else-if="status === 'pending'"&gt;User is pending&lt;/p&gt;
  &lt;p v-else&gt;User is inactive&lt;/p&gt;

  &lt;h3&gt;Tasks:&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="task in tasks" :key="task"&gt;{{ task }}&lt;/li&gt;
  &lt;/ul&gt;
  &lt;!-- &lt;a v-bind:href="link"&gt;Click for Google&lt;/a&gt; --&gt;
  &lt;a :href="link"&gt;Click for Google&lt;/a&gt;
  &lt;br /&gt;
  &lt;!-- &lt;button v-on:click="toggleStatus"&gt;Change Status&lt;/button&gt; --&gt;
  &lt;button @click="toggleStatus"&gt;Change Status&lt;/button&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Composition API – Long Form</h3>



<ul class="wp-block-list">
<li>複製 App.vue 檔案，並把 Options API 改名為 App2.vue 檔名</li>



<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script&gt;
export default {
  setup() {
    const name = 'John Doe';
    const status = 'active';
    const tasks = &#91;'Task One', 'Task Two', 'Task Three'];

    const toggleStatus = () =&gt; {
      if (this.status === 'active') {
        this.status = 'pending';
      } else if (this.status === 'pending') {
        this.status = 'inactive';
      } else {
        this.status = 'active';
      }
    }

    return {
      name,
      status,
      tasks,
      toggleStatus,
    };
  }
};
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;{{ name }}&lt;/h1&gt;
  &lt;p v-if="status === 'active'"&gt;User is active&lt;/p&gt;
  &lt;p v-else-if="status === 'pending'"&gt;User is pending&lt;/p&gt;
  &lt;p v-else&gt;User is inactive&lt;/p&gt;

  &lt;h3&gt;Tasks:&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="task in tasks" :key="task"&gt;{{ task }}&lt;/li&gt;
  &lt;/ul&gt;
  &lt;br /&gt;
  &lt;!-- &lt;button v-on:click="toggleStatus"&gt;Change Status&lt;/button&gt; --&gt;
  &lt;button @click="toggleStatus"&gt;Change Status&lt;/button&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">ref() &amp; Reactive Values</h3>



<ul class="wp-block-list">
<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script&gt;
import { ref } from 'vue';

export default {
  setup() {
    const name = ref('John Doe');
    const status = ref('active');
    const tasks = ref(&#91;'Task One', 'Task Two', 'Task Three']);

    const toggleStatus = () =&gt; {
      if (status.value === 'active') {
        status.value = 'pending';
      } else if (status.value === 'pending') {
        status.value = 'inactive';
      } else {
        status.value = 'active';
      }
    }

    return {
      name,
      status,
      tasks,
      toggleStatus,
    };
  },
};
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;{{ name }}&lt;/h1&gt;
  &lt;p v-if="status === 'active'"&gt;User is active&lt;/p&gt;
  &lt;p v-else-if="status === 'pending'"&gt;User is pending&lt;/p&gt;
  &lt;p v-else&gt;User is inactive&lt;/p&gt;

  &lt;h3&gt;Tasks:&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="task in tasks" :key="task"&gt;{{ task }}&lt;/li&gt;
  &lt;/ul&gt;
  &lt;br /&gt;
  &lt;!-- &lt;button v-on:click="toggleStatus"&gt;Change Status&lt;/button&gt; --&gt;
  &lt;button @click="toggleStatus"&gt;Change Status&lt;/button&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Composition API Short Form</h3>



<ul class="wp-block-list">
<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script setup&gt;
import { ref } from 'vue';

const name = ref('John Doe');
const status = ref('active');
const tasks = ref(&#91;'Task One', 'Task Two', 'Task Three']);

const toggleStatus = () =&gt; {
  if (status.value === 'active') {
    status.value = 'pending';
  } else if (status.value === 'pending') {
    status.value = 'inactive';
  } else {
    status.value = 'active';
  }
};
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;{{ name }}&lt;/h1&gt;
  &lt;p v-if="status === 'active'"&gt;User is active&lt;/p&gt;
  &lt;p v-else-if="status === 'pending'"&gt;User is pending&lt;/p&gt;
  &lt;p v-else&gt;User is inactive&lt;/p&gt;

  &lt;h3&gt;Tasks:&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="task in tasks" :key="task"&gt;{{ task }}&lt;/li&gt;
  &lt;/ul&gt;
  &lt;br /&gt;
  &lt;!-- &lt;button v-on:click="toggleStatus"&gt;Change Status&lt;/button&gt; --&gt;
  &lt;button @click="toggleStatus"&gt;Change Status&lt;/button&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Forms &amp; v-model</h3>



<ul class="wp-block-list">
<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script setup&gt;
import { ref } from 'vue';

const name = ref('John Doe');
const status = ref('active');
const tasks = ref(&#91;'Task One', 'Task Two', 'Task Three']);
const newTask = ref('');

const toggleStatus = () =&gt; {
  if (status.value === 'active') {
    status.value = 'pending';
  } else if (status.value === 'pending') {
    status.value = 'inactive';
  } else {
    status.value = 'active';
  }
};

const addTask = () =&gt; {
  if (newTask.value.trim() !== '') {
    tasks.value.push(newTask.value);
    newTask.value = '';
  }
};
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;{{ name }}&lt;/h1&gt;
  &lt;p v-if="status === 'active'"&gt;User is active&lt;/p&gt;
  &lt;p v-else-if="status === 'pending'"&gt;User is pending&lt;/p&gt;
  &lt;p v-else&gt;User is inactive&lt;/p&gt;

  &lt;form @submit.prevent="addTask"&gt;
    &lt;label for="newTask"&gt;Add Task&lt;/label&gt;
    &lt;input type="text" id="newTask" name="newTask" v-model="newTask"&gt;
    &lt;button type="submit"&gt;Submit&lt;/button&gt;
  &lt;/form&gt;

  &lt;h3&gt;Tasks:&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="task in tasks" :key="task"&gt;{{ task }}&lt;/li&gt;
  &lt;/ul&gt;
  &lt;br /&gt;
  &lt;!-- &lt;button v-on:click="toggleStatus"&gt;Change Status&lt;/button&gt; --&gt;
  &lt;button @click="toggleStatus"&gt;Change Status&lt;/button&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Delete task</h3>



<ul class="wp-block-list">
<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script setup&gt;
import { ref } from 'vue';

const name = ref('John Doe');
const status = ref('active');
const tasks = ref(&#91;'Task One', 'Task Two', 'Task Three']);
const newTask = ref('');

const toggleStatus = () =&gt; {
  if (status.value === 'active') {
    status.value = 'pending';
  } else if (status.value === 'pending') {
    status.value = 'inactive';
  } else {
    status.value = 'active';
  }
};

const addTask = () =&gt; {
  if (newTask.value.trim() !== '') {
    tasks.value.push(newTask.value);
    newTask.value = '';
  }
};

const deleteTask = (index) =&gt; {
  tasks.value.splice(index, 1);
};
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;{{ name }}&lt;/h1&gt;
  &lt;p v-if="status === 'active'"&gt;User is active&lt;/p&gt;
  &lt;p v-else-if="status === 'pending'"&gt;User is pending&lt;/p&gt;
  &lt;p v-else&gt;User is inactive&lt;/p&gt;

  &lt;form @submit.prevent="addTask"&gt;
    &lt;label for="newTask"&gt;Add Task&lt;/label&gt;
    &lt;input type="text" id="newTask" name="newTask" v-model="newTask"&gt;
    &lt;button type="submit"&gt;Submit&lt;/button&gt;
  &lt;/form&gt;

  &lt;h3&gt;Tasks:&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="(task, index) in tasks" :key="task"&gt;
      &lt;span&gt;
        {{ task }}
      &lt;/span&gt;
      &lt;button @click="deleteTask(index)"&gt;x&lt;/button&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
  &lt;br /&gt;
  &lt;!-- &lt;button v-on:click="toggleStatus"&gt;Change Status&lt;/button&gt; --&gt;
  &lt;button @click="toggleStatus"&gt;Change Status&lt;/button&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Lifecycle Methods</h3>



<ul class="wp-block-list">
<li>講解 Lifecycle Methods</li>
</ul>



<h4 class="wp-block-heading">Lifecycle Methods</h4>



<ul class="wp-block-list">
<li>onBeforeMount – called before mounting begins</li>



<li>onMounted – called when component is mounted</li>



<li>onBeforeUpdate – called when reactive data changes and before re-render</li>



<li>onUpdated – called after re-render</li>



<li>onBeforeUnmount – called before the Vue instance is destroyed</li>



<li>onUnmounted – called after the instance is destroyed</li>



<li>onActivated – called when a kept-alive component is activated</li>



<li>onDeactivated – called when a kept-alive component is deactivated</li>



<li>onErrorCaptured – called when an error is captured from a child component</li>
</ul>



<h3 class="wp-block-heading">onMounted &amp; Fetching Data</h3>



<ul class="wp-block-list">
<li>關於 onMounted 用法，使用 jsonplaceholder todos</li>



<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script setup&gt;
import { ref, onMounted } from 'vue';

const name = ref('John Doe');
const status = ref('active');
const tasks = ref(&#91;'Task One', 'Task Two', 'Task Three']);
const newTask = ref('');

const toggleStatus = () =&gt; {
  if (status.value === 'active') {
    status.value = 'pending';
  } else if (status.value === 'pending') {
    status.value = 'inactive';
  } else {
    status.value = 'active';
  }
};

const addTask = () =&gt; {
  if (newTask.value.trim() !== '') {
    tasks.value.push(newTask.value);
    newTask.value = '';
  }
};

const deleteTask = (index) =&gt; {
  tasks.value.splice(index, 1);
};

onMounted(async () =&gt; {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos');
    const data = await response.json();
    tasks.value = data.map((task) =&gt; task.title);
  } catch (error) {
    console.log('Error fetching tasks');
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;h1&gt;{{ name }}&lt;/h1&gt;
  &lt;p v-if="status === 'active'"&gt;User is active&lt;/p&gt;
  &lt;p v-else-if="status === 'pending'"&gt;User is pending&lt;/p&gt;
  &lt;p v-else&gt;User is inactive&lt;/p&gt;

  &lt;form @submit.prevent="addTask"&gt;
    &lt;label for="newTask"&gt;Add Task&lt;/label&gt;
    &lt;input type="text" id="newTask" name="newTask" v-model="newTask"&gt;
    &lt;button type="submit"&gt;Submit&lt;/button&gt;
  &lt;/form&gt;

  &lt;h3&gt;Tasks:&lt;/h3&gt;
  &lt;ul&gt;
    &lt;li v-for="(task, index) in tasks" :key="task"&gt;
      &lt;span&gt;
        {{ task }}
      &lt;/span&gt;
      &lt;button @click="deleteTask(index)"&gt;x&lt;/button&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
  &lt;br /&gt;
  &lt;!-- &lt;button v-on:click="toggleStatus"&gt;Change Status&lt;/button&gt; --&gt;
  &lt;button @click="toggleStatus"&gt;Change Status&lt;/button&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Vue Jobs Project Start</h3>



<ul class="wp-block-list">
<li>複製 App.vue 檔案程式碼，貼到 App2.vue 檔案</li>



<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;template&gt;
  &lt;h1&gt;Vue Jobs&lt;/h1&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Tailwind CSS Setup</h3>



<ul class="wp-block-list">
<li>搜尋 Vue Tailwind<br><a href="https://v2.tailwindcss.com/docs/guides/vue-3-vite" target="_blank" rel="noreferrer noopener">Setting up Tailwind CSS</a></li>



<li>安裝 Tailwind via npm<br>npm install -D tailwindcss@latest postcss@latest autoprefixer@latest</li>



<li>開啟新的終端機然後安裝</li>



<li>建立 your configuration files<br>npx tailwindcss init -p</li>



<li>修改 tailwind.config.js 檔案</li>



<li>Include Tailwind in your CSS<br>修改 main.css 檔案</li>



<li>重新執行終端機 – npm run dev</li>



<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: &#91;'./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {
      fontFamily: {
        sans: &#91;'Poppins', 'sans-serif']
      },
      gridTemplateColumns: {
        '70/30': '70% 28%'
      }
    },
  },
  variants: {
    extend: {},
  },
  plugins: &#91;],
}
</code></pre>



<pre class="wp-block-code"><code>// src/assets/main.css
@tailwind base;
@tailwind components;
@tailwind utilities;
</code></pre>



<pre class="wp-block-code"><code>// App.vue
&lt;template&gt;
  &lt;h1 class="text-2xl"&gt;Vue Jobs&lt;/h1&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Theme Files &amp; Images</h3>



<ul class="wp-block-list">
<li><a href="https://github.com/bradtraversy/vue-crash-2024">Full </a><a href="https://github.com/bradtraversy/vue-crash-2024" target="_blank" rel="noreferrer noopener">Project </a><a href="https://github.com/bradtraversy/vue-crash-2024">Code</a></li>



<li>複製 _theme_files 檔案到專案裡面</li>



<li>複製 img 資料夾到 assets 資料夾裡面</li>



<li>刪除在 assets 資料夾裡面的 base.css、logo.svg 檔案</li>
</ul>



<h3 class="wp-block-heading">Navbar Component</h3>



<ul class="wp-block-list">
<li>在 components 資料夾裡面建立 Navbar.vue 檔案</li>



<li>修改 Navbar.vue 檔案</li>



<li>複製 _theme_files 資料夾裡面的 index.html 檔案程式碼</li>



<li>修改 Navbar.vue 檔案</li>



<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/Navbar.vue
&lt;script setup&gt;
  import logo from '@/assets/img/logo.png'
&lt;/script&gt;

&lt;template&gt;
  &lt;nav class="bg-green-700 border-b border-green-500"&gt;
    &lt;div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8"&gt;
      &lt;div class="flex h-20 items-center justify-between"&gt;
        &lt;div
          class="flex flex-1 items-center justify-center md:items-stretch md:justify-start"
        &gt;
          &lt;!-- Logo --&gt;
          &lt;a class="flex flex-shrink-0 items-center mr-4" href="index.html"&gt;
            &lt;img class="h-10 w-auto" :src="logo" alt="Vue Jobs" /&gt;
            &lt;span class="hidden md:block text-white text-2xl font-bold ml-2"
              &gt;Vue Jobs&lt;/span
            &gt;
          &lt;/a&gt;
          &lt;div class="md:ml-auto"&gt;
            &lt;div class="flex space-x-2"&gt;
              &lt;a
                href="index.html"
                class="text-white bg-green-900 hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                &gt;Home&lt;/a
              &gt;
              &lt;a
                href="jobs.html"
                class="text-white hover:bg-green-900 hover:text-white rounded-md px-3 py-2"
                &gt;Jobs&lt;/a
              &gt;
              &lt;a
                href="add-job.html"
                class="text-white hover:bg-green-900 hover:text-white rounded-md px-3 py-2"
                &gt;Add Job&lt;/a
              &gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// App.vue
&lt;script setup&gt;
import Navbar from '@/components/Navbar.vue'
&lt;/script&gt;

&lt;template&gt;
  &lt;Navbar /&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Hero Component</h3>



<ul class="wp-block-list">
<li>在 components 資料夾裡面建立 Hero.vue 檔案</li>



<li>修改 Hero.vue 檔案</li>



<li>複製 _theme_files 資料夾裡面的 index.html 檔案 Hero 的部分</li>



<li>修改 App.vue 檔案</li>



<li>修改 Hero.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/Hero.vue
&lt;script setup&gt;

&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-green-700 py-20 mb-4"&gt;
    &lt;div
      class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col items-center"
    &gt;
      &lt;div class="text-center"&gt;
        &lt;h1
          class="text-4xl font-extrabold text-white sm:text-5xl md:text-6xl"
        &gt;
          Become a Vue Dev
        &lt;/h1&gt;
        &lt;p class="my-4 text-xl text-white"&gt;
          Find the Vue job that fits your skills and needs
        &lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// App.vue
&lt;script setup&gt;
import Navbar from '@/components/Navbar.vue'
import Hero from '@/components/Hero.vue'
&lt;/script&gt;

&lt;template&gt;
  &lt;Navbar /&gt;
  &lt;Hero /&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Props</h3>



<ul class="wp-block-list">
<li>修改 App.vue 檔案</li>



<li>修改 Hero.vue 檔案</li>



<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.vue
&lt;script setup&gt;
import Navbar from '@/components/Navbar.vue'
import Hero from '@/components/Hero.vue'
&lt;/script&gt;

&lt;template&gt;
  &lt;Navbar /&gt;
  &lt;Hero /&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// Hero.vue
&lt;script setup&gt;
import { defineProps } from 'vue';

defineProps({
  title: {
    type: String,
    default: 'Become a Vue Dev'
  },
  subtitle: {
    type: String,
    default: 'Find the Vue job that fits your skills and needs'
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-green-700 py-20 mb-4"&gt;
    &lt;div
      class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col items-center"
    &gt;
      &lt;div class="text-center"&gt;
        &lt;h1
          class="text-4xl font-extrabold text-white sm:text-5xl md:text-6xl"
        &gt;
          {{ title }}
        &lt;/h1&gt;
        &lt;p class="my-4 text-xl text-white"&gt;
          {{  subtitle }}
        &lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">HomeCards &amp; Card Container Component</h3>



<ul class="wp-block-list">
<li>在 components 資料夾裡面建立 HomeCards.vue 檔案</li>



<li>修改 HomeCards.vue 檔案</li>



<li>複製 _theme_files 資料夾裡面的 index.html 檔案 Developers and Employers 的部分</li>



<li>修改 App.vue 檔案</li>



<li>在 components 資料夾裡面建立 Card.vue 檔案</li>



<li>修改 Card.vue 檔案</li>



<li>修改 HomeCards.vue 檔案</li>



<li>修改 Card.vue 檔案</li>



<li>修改 HomeCards.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/HomeCards.vue
&lt;script setup&gt;
import Card from '@/components/Card.vue'
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="py-4"&gt;
    &lt;div class="container-xl lg:container m-auto"&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-lg"&gt;
        &lt;Card&gt;
          &lt;h2 class="text-2xl font-bold"&gt;For Developers&lt;/h2&gt;
          &lt;p class="mt-2 mb-4"&gt;
            Browse our Vue jobs and start your career today
          &lt;/p&gt;
          &lt;a
            href="jobs.html"
            class="inline-block bg-black text-white rounded-lg px-4 py-2 hover:bg-gray-700"
          &gt;
            Browse Jobs
          &lt;/a&gt;
        &lt;/Card&gt;
        &lt;Card bg="bg-green-100"&gt;
          &lt;h2 class="text-2xl font-bold"&gt;For Employers&lt;/h2&gt;
          &lt;p class="mt-2 mb-4"&gt;
            List your job to find the perfect developer for the role
          &lt;/p&gt;
          &lt;a
            href="add-job.html"
            class="inline-block bg-green-500 text-white rounded-lg px-4 py-2 hover:bg-green-600"
          &gt;
            Add Job
          &lt;/a&gt;
        &lt;/Card&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// App.vue
&lt;script setup&gt;
import Navbar from '@/components/Navbar.vue'
import Hero from '@/components/Hero.vue'
import HomeCards from '@/components/HomeCards.vue'
&lt;/script&gt;

&lt;template&gt;
  &lt;Navbar /&gt;
  &lt;Hero /&gt;
  &lt;HomeCards /&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/Card.vue
&lt;script setup&gt;
import { defineProps } from 'vue';

defineProps({
  bg: {
    type: String,
    default: 'bg-gray-100'
  }
})
&lt;/script&gt;

&lt;template&gt;
  &lt;div :class="`${bg} p-6 rounded-lg shadow-md`"&gt;
    &lt;slot&gt;&lt;/slot&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">JobListings Component &amp; JSON Data</h3>



<ul class="wp-block-list">
<li>在 components 資料夾裡面建立 JobListings.vue 檔案</li>



<li>修改 JobListings.vue 檔案</li>



<li>修改 App.vue 檔案</li>



<li>複製 job2.json 檔案到 src 資料夾，重新命名為 jobs.json 檔案</li>



<li>修改 JobListings.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/JobListings.vue
&lt;script setup&gt;
import jobData from '@/jobs.json';
import { ref } from 'vue';

const jobs = ref(jobData);
console.log(jobs.value);

&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-blue-50 px-4 py-10"&gt;
    &lt;div class="container-xl lg:container m-auto"&gt;
      &lt;h2 class="text-3xl font-bold text-green-500 mb-6 text-center"&gt;
        Browse Jobs
      &lt;/h2&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
        &lt;div v-for="job in jobs" :key="job.id"&gt;
          {{ job.title }}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/jobs.json
&#91;
  {
    "id": 1,
    "title": "Senior Vue Developer",
    "type": "Full-Time",
    "description": "We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript, with experience working with modern JavaScript frameworks such as Vue or Angular.",
    "location": "Boston, MA",
    "salary": "$70K - $80K",
    "company": {
      "name": "NewTek Solutions",
      "description": "NewTek Solutions is a leading technology company specializing in web development and digital solutions. We pride ourselves on delivering high-quality products and services to our clients while fostering a collaborative and innovative work environment.",
      "contactEmail": "contact@teksolutions.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": 2,
    "title": "Front-End Engineer (Vue)",
    "type": "Full-Time",
    "description": "Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion for crafting beautiful and responsive web applications. Experience with UI/UX design principles and a strong attention to detail are highly desirable.",
    "location": "Miami, FL",
    "salary": "$70K - $80K",
    "company": {
      "name": "Veneer Solutions",
      "description": "Veneer Solutions is a creative agency specializing in digital design and development. Our team is dedicated to pushing the boundaries of creativity and innovation to deliver exceptional results for our clients.",
      "contactEmail": "contact@loremipsum.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": 3,
    "title": "Vue.js Developer",
    "type": "Full-Time",
    "description": "Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference. We offer competitive compensation and a collaborative work environment where your ideas are valued.",
    "location": "Brooklyn, NY",
    "salary": "$70K - $80K",
    "company": {
      "name": "Dolor Cloud",
      "description": "Dolor Cloud is a leading technology company specializing in digital solutions for businesses of all sizes. With a focus on innovation and customer satisfaction, we are committed to delivering cutting-edge products and services.",
      "contactEmail": "contact@dolorsitamet.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": 4,
    "title": "Vue Front-End Developer",
    "type": "Part-Time",
    "description": "Join our team as a Part-Time Front-End Developer in beautiful Pheonix, AZ. We are looking for a self-motivated individual with a passion for creating engaging user experiences. This position offers flexible hours and the opportunity to work remotely.",
    "location": "Pheonix, AZ",
    "salary": "$60K - $70K",
    "company": {
      "name": "Alpha Elite",
      "description": "Alpha Elite is a dynamic startup specializing in digital marketing and web development. We are committed to fostering a diverse and inclusive workplace where creativity and innovation thrive.",
      "contactEmail": "contact@adipisicingelit.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": 5,
    "title": "Full Stack Vue Developer",
    "type": "Full-Time",
    "description": "Exciting opportunity for a Full-Time Front-End Developer in bustling Atlanta, GA. We are seeking a talented individual with a passion for building elegant and scalable web applications. Join our team and make an impact!",
    "location": "Atlanta, GA",
    "salary": "$90K - $100K",
    "company": {
      "name": "Browning Technologies",
      "description": "Browning Technologies is a rapidly growing technology company specializing in e-commerce solutions. We offer a dynamic and collaborative work environment where employees are encouraged to think creatively and innovate.",
      "contactEmail": "contact@consecteturadipisicing.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": 6,
    "title": "Vue Native Developer",
    "type": "Full-Time",
    "description": "Join our team as a Front-End Developer in beautiful Portland, OR. We are looking for a skilled and enthusiastic individual to help us create innovative web solutions. Competitive salary and great benefits package available.",
    "location": "Portland, OR",
    "salary": "$100K - $110K",
    "company": {
      "name": "Port Solutions INC",
      "description": "Port Solutions is a leading technology company specializing in software development and digital marketing. We are committed to providing our clients with cutting-edge solutions and our employees with a supportive and rewarding work environment.",
      "contactEmail": "contact@ipsumlorem.com",
      "contactPhone": "555-555-5555"
    }
  }
]
</code></pre>



<h3 class="wp-block-heading">JobListing Component</h3>



<ul class="wp-block-list">
<li>到 _theme_files 資料夾裡面的 index.html 檔案複製程式碼</li>



<li>在 components 資料夾裡面建立 JobListing.vue 檔案</li>



<li>修改 JobListing.vue 檔案</li>



<li>修改 JobListings.vue 檔案</li>



<li>修改 JobListing.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/JobListing.vue
&lt;script setup&gt;
import { defineProps } from 'vue';

defineProps({
  job: Object
});
&lt;/script&gt;

&lt;template&gt;
  &lt;div class="bg-white rounded-xl shadow-md relative"&gt;
    &lt;div class="p-4"&gt;
      &lt;div class="mb-6"&gt;
        &lt;div class="text-gray-600 my-2"&gt;{{ job.type }}&lt;/div&gt;
        &lt;h3 class="text-xl font-bold"&gt;{{  job.title }}&lt;/h3&gt;
      &lt;/div&gt;

      &lt;div class="mb-5"&gt;
        {{  job.description }}
      &lt;/div&gt;

      &lt;h3 class="text-green-500 mb-2"&gt;{{ job.salary }} / Year&lt;/h3&gt;

      &lt;div class="border border-gray-100 mb-5"&gt;&lt;/div&gt;

      &lt;div class="flex flex-col lg:flex-row justify-between mb-4"&gt;
        &lt;div class="text-orange-700 mb-3"&gt;
          &lt;i class="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
          {{  job.location }}
        &lt;/div&gt;
        &lt;a
          :href="'/job/' + job.id"
          class="h-&#91;36px] bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg text-center text-sm"
        &gt;
          Read More
        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/JobListings.vue
&lt;script setup&gt;
import JobListing from './JobListing.vue';
import jobData from '@/jobs.json';
import { ref } from 'vue';

const jobs = ref(jobData);
console.log(jobs.value);

&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-blue-50 px-4 py-10"&gt;
    &lt;div class="container-xl lg:container m-auto"&gt;
      &lt;h2 class="text-3xl font-bold text-green-500 mb-6 text-center"&gt;
        Browse Jobs
      &lt;/h2&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
        &lt;JobListing v-for="job in jobs" :key="job.id" :job="job" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">JobListings Limit &amp; showButton Props</h3>



<ul class="wp-block-list">
<li>修改 JobListings.vue 檔案</li>



<li>修改 App.vue 檔案</li>



<li>到 _theme_files 資料夾裡面的 index.html 檔案複製程式碼貼到 JobListings.vue 檔案</li>



<li>修改 JobListings.vue 檔案</li>



<li>修改 App.vue 檔案</li>



<li>修改 JobListings.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/JobListings.vue
&lt;script setup&gt;
import JobListing from './JobListing.vue';
import jobData from '@/jobs.json';
import { ref, defineProps } from 'vue';

defineProps({
  limit: Number,
  showButton: {
    type: Boolean,
    default: false
  }
});

const jobs = ref(jobData);
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-blue-50 px-4 py-10"&gt;
    &lt;div class="container-xl lg:container m-auto"&gt;
      &lt;h2 class="text-3xl font-bold text-green-500 mb-6 text-center"&gt;
        Browse Jobs
      &lt;/h2&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
        &lt;JobListing v-for="job in jobs.slice(0, limit || jobs.length)" :key="job.id" :job="job" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;section v-if="showButton" class="m-auto max-w-lg my-10 px-6"&gt;
    &lt;a
      href="/jobs"
      class="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
      &gt;View All Jobs&lt;/a
    &gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// App.vue
&lt;script setup&gt;
import Navbar from '@/components/Navbar.vue'
import Hero from '@/components/Hero.vue'
import HomeCards from '@/components/HomeCards.vue'
import JobListings from './components/JobListings.vue'
&lt;/script&gt;

&lt;template&gt;
  &lt;Navbar /&gt;
  &lt;Hero /&gt;
  &lt;HomeCards /&gt;
  &lt;JobListings :limit="3" :showButton="true" /&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">computed() &amp; Truncate Description</h3>



<ul class="wp-block-list">
<li>修改 JobListing.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/JobListing.vue
&lt;script setup&gt;
import { defineProps, ref, computed } from 'vue';

const props = defineProps({
  job: Object
});

const showFullDescription = ref(false);

const toggleFullDescription = () =&gt; {
  showFullDescription.value = !showFullDescription.value;
}

const truncatedDescription = computed(() =&gt; {
  let description = props.job.description;
  if (!showFullDescription.value) {
    description = description.substring(0, 90) + '...';
  }
  return description;
});
&lt;/script&gt;

&lt;template&gt;
  &lt;div class="bg-white rounded-xl shadow-md relative"&gt;
    &lt;div class="p-4"&gt;
      &lt;div class="mb-6"&gt;
        &lt;div class="text-gray-600 my-2"&gt;{{ job.type }}&lt;/div&gt;
        &lt;h3 class="text-xl font-bold"&gt;{{  job.title }}&lt;/h3&gt;
      &lt;/div&gt;

      &lt;div class="mb-5"&gt;
        &lt;div&gt;
          {{  truncatedDescription }}
        &lt;/div&gt;
        &lt;button @click="toggleFullDescription" class="text-green-500 hover:text-green-600 mb-5"&gt;
          {{ showFullDescription ? 'Less' : 'More' }}
        &lt;/button&gt;
      &lt;/div&gt;

      &lt;h3 class="text-green-500 mb-2"&gt;{{ job.salary }} / Year&lt;/h3&gt;

      &lt;div class="border border-gray-100 mb-5"&gt;&lt;/div&gt;

      &lt;div class="flex flex-col lg:flex-row justify-between mb-4"&gt;
        &lt;div class="text-orange-700 mb-3"&gt;
          &lt;i class="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
          {{  job.location }}
        &lt;/div&gt;
        &lt;a
          :href="'/job/' + job.id"
          class="h-&#91;36px] bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg text-center text-sm"
        &gt;
          Read More
        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">PrimeIcons</h3>



<ul class="wp-block-list">
<li>搜尋 <a href="https://github.com/primefaces/primeicons" target="_blank" rel="noreferrer noopener">primeicons</a></li>



<li>使用 <a href="https://primevue.org/icons" target="_blank" rel="noreferrer noopener">PrimeVue</a></li>



<li>修改 main.js 檔案</li>



<li>修改 JobListing.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/main.js
import './assets/main.css'
import 'primeicons/primeicons.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
</code></pre>



<pre class="wp-block-code"><code>// src/components/JobListing.vue
&lt;script setup&gt;
import { defineProps, ref, computed } from 'vue';

const props = defineProps({
  job: Object
});

const showFullDescription = ref(false);

const toggleFullDescription = () =&gt; {
  showFullDescription.value = !showFullDescription.value;
}

const truncatedDescription = computed(() =&gt; {
  let description = props.job.description;
  if (!showFullDescription.value) {
    description = description.substring(0, 90) + '...';
  }
  return description;
});
&lt;/script&gt;

&lt;template&gt;
  &lt;div class="bg-white rounded-xl shadow-md relative"&gt;
    &lt;div class="p-4"&gt;
      &lt;div class="mb-6"&gt;
        &lt;div class="text-gray-600 my-2"&gt;{{ job.type }}&lt;/div&gt;
        &lt;h3 class="text-xl font-bold"&gt;{{  job.title }}&lt;/h3&gt;
      &lt;/div&gt;

      &lt;div class="mb-5"&gt;
        &lt;div&gt;
          {{  truncatedDescription }}
        &lt;/div&gt;
        &lt;button @click="toggleFullDescription" class="text-green-500 hover:text-green-600 mb-5"&gt;
          {{ showFullDescription ? 'Less' : 'More' }}
        &lt;/button&gt;
      &lt;/div&gt;

      &lt;h3 class="text-green-500 mb-2"&gt;{{ job.salary }} / Year&lt;/h3&gt;

      &lt;div class="border border-gray-100 mb-5"&gt;&lt;/div&gt;

      &lt;div class="flex flex-col lg:flex-row justify-between mb-4"&gt;
        &lt;div class="text-orange-700 mb-3"&gt;
          &lt;i class="pi pi-map-marker text-orange-700"&gt;&lt;/i&gt;
          {{  job.location }}
        &lt;/div&gt;
        &lt;a
          :href="'/job/' + job.id"
          class="h-&#91;36px] bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg text-center text-sm"
        &gt;
          Read More
        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Vue Router &amp; Home View</h3>



<ul class="wp-block-list">
<li>開啟終端機、安裝 npm i vue-router</li>



<li>在 src 資料夾裡面建立 router 資料夾</li>



<li>在 router 資料夾裡面建立 index.js 檔案</li>



<li>修改 index.js 檔案</li>



<li>在 src 資料夾裡面建立 views 資料夾</li>



<li>修改 App.vue 檔案</li>



<li>在 views 資料夾裡面建立 HomeView.vue 檔案</li>



<li>修改 HomeView.vue 檔案</li>



<li>修改 App.vue 檔案</li>



<li>修改 HomeView.vue 檔案</li>



<li>修改 index.js 檔案</li>



<li>修改 main.js 檔案</li>



<li>修改 HomeView.vue 檔案，除錯</li>



<li>修改 App.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '@/views/HomeView.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: &#91;
    {
      path: '/',
      name: 'home',
      component: HomeView
    }
  ]
});

export default router;
</code></pre>



<pre class="wp-block-code"><code>// src/App.vue
&lt;script setup&gt;
import Navbar from '@/components/Navbar.vue'
import { RouterView } from 'vue-router'
&lt;/script&gt;

&lt;template&gt;
  &lt;Navbar /&gt;
  &lt;RouterView /&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/views/HomeView.vue
&lt;script setup&gt;
import Hero from '@/components/Hero.vue'
import HomeCards from '@/components/HomeCards.vue'
import JobListings from '@/components/JobListings.vue'
&lt;/script&gt;

&lt;template&gt;
  &lt;Hero /&gt;
  &lt;HomeCards /&gt;
  &lt;JobListings :limit="3" :showButton="true" /&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/main.js
import './assets/main.css'
import 'primeicons/primeicons.css'
import router from './router'

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.use(router);

app.mount('#app')
</code></pre>



<h3 class="wp-block-heading">Jobs View</h3>



<ul class="wp-block-list">
<li>修改 Navbar.vue 檔案</li>



<li>在 views 資料夾裡面建立 JobsView.vue 檔案</li>



<li>修改 JobsView.vue 檔案</li>



<li>修改 index.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/Navbar.vue
&lt;script setup&gt;
  import logo from '@/assets/img/logo.png'
&lt;/script&gt;

&lt;template&gt;
  &lt;nav class="bg-green-700 border-b border-green-500"&gt;
    &lt;div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8"&gt;
      &lt;div class="flex h-20 items-center justify-between"&gt;
        &lt;div
          class="flex flex-1 items-center justify-center md:items-stretch md:justify-start"
        &gt;
          &lt;!-- Logo --&gt;
          &lt;a class="flex flex-shrink-0 items-center mr-4" href="index.html"&gt;
            &lt;img class="h-10 w-auto" :src="logo" alt="Vue Jobs" /&gt;
            &lt;span class="hidden md:block text-white text-2xl font-bold ml-2"
              &gt;Vue Jobs&lt;/span
            &gt;
          &lt;/a&gt;
          &lt;div class="md:ml-auto"&gt;
            &lt;div class="flex space-x-2"&gt;
              &lt;a
                href="/"
                class="text-white bg-green-900 hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                &gt;Home&lt;/a
              &gt;
              &lt;a
                href="/jobs"
                class="text-white hover:bg-green-900 hover:text-white rounded-md px-3 py-2"
                &gt;Jobs&lt;/a
              &gt;
              &lt;a
                href="/jobs/add"
                class="text-white hover:bg-green-900 hover:text-white rounded-md px-3 py-2"
                &gt;Add Job&lt;/a
              &gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/views/JobsView.vue
&lt;script setup&gt;
import JobListings from '@/components/JobListings.vue';
&lt;/script&gt;

&lt;template&gt;
  &lt;JobListings /&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '@/views/HomeView.vue';
import JobsView from '@/views/JobsView.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: &#91;
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/jobs',
      name: 'jobs',
      component: JobsView
    }
  ]
});

export default router;
</code></pre>



<h3 class="wp-block-heading">RouterLink</h3>



<ul class="wp-block-list">
<li>修改 Navbar.vue 檔案</li>



<li>修改 HomeCards.vue 檔案</li>



<li>修改 JobListing.vue 檔案</li>



<li>修改 JobListings.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/Navbar.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router'
import logo from '@/assets/img/logo.png'
&lt;/script&gt;

&lt;template&gt;
  &lt;nav class="bg-green-700 border-b border-green-500"&gt;
    &lt;div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8"&gt;
      &lt;div class="flex h-20 items-center justify-between"&gt;
        &lt;div
          class="flex flex-1 items-center justify-center md:items-stretch md:justify-start"
        &gt;
          &lt;!-- Logo --&gt;
          &lt;RouterLink class="flex flex-shrink-0 items-center mr-4" to="/"&gt;
            &lt;img class="h-10 w-auto" :src="logo" alt="Vue Jobs" /&gt;
            &lt;span class="hidden md:block text-white text-2xl font-bold ml-2"
              &gt;Vue Jobs&lt;/span
            &gt;
          &lt;/RouterLink&gt;
          &lt;div class="md:ml-auto"&gt;
            &lt;div class="flex space-x-2"&gt;
              &lt;RouterLink
                to="/"
                class="text-white bg-green-900 hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                &gt;Home&lt;/RouterLink
              &gt;
              &lt;RouterLink
                to="/jobs"
                class="text-white hover:bg-green-900 hover:text-white rounded-md px-3 py-2"
                &gt;Jobs&lt;/RouterLink
              &gt;
              &lt;RouterLink
                to="/jobs/add"
                class="text-white hover:bg-green-900 hover:text-white rounded-md px-3 py-2"
                &gt;Add Job&lt;/RouterLink
              &gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/HomeCards.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router'
import Card from '@/components/Card.vue'
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="py-4"&gt;
    &lt;div class="container-xl lg:container m-auto"&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-lg"&gt;
        &lt;Card&gt;
          &lt;h2 class="text-2xl font-bold"&gt;For Developers&lt;/h2&gt;
          &lt;p class="mt-2 mb-4"&gt;
            Browse our Vue jobs and start your career today
          &lt;/p&gt;
          &lt;RouterLink
            to="/jobs"
            class="inline-block bg-black text-white rounded-lg px-4 py-2 hover:bg-gray-700"
          &gt;
            Browse Jobs
          &lt;/RouterLink&gt;
        &lt;/Card&gt;
        &lt;Card bg="bg-green-100"&gt;
          &lt;h2 class="text-2xl font-bold"&gt;For Employers&lt;/h2&gt;
          &lt;p class="mt-2 mb-4"&gt;
            List your job to find the perfect developer for the role
          &lt;/p&gt;
          &lt;RouterLink
            to="/jobs/add"
            class="inline-block bg-green-500 text-white rounded-lg px-4 py-2 hover:bg-green-600"
          &gt;
            Add Job
          &lt;/RouterLink&gt;
        &lt;/Card&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/JobListing.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router';
import { defineProps, ref, computed } from 'vue';

const props = defineProps({
  job: Object
});

const showFullDescription = ref(false);

const toggleFullDescription = () =&gt; {
  showFullDescription.value = !showFullDescription.value;
}

const truncatedDescription = computed(() =&gt; {
  let description = props.job.description;
  if (!showFullDescription.value) {
    description = description.substring(0, 90) + '...';
  }
  return description;
});
&lt;/script&gt;

&lt;template&gt;
  &lt;div class="bg-white rounded-xl shadow-md relative"&gt;
    &lt;div class="p-4"&gt;
      &lt;div class="mb-6"&gt;
        &lt;div class="text-gray-600 my-2"&gt;{{ job.type }}&lt;/div&gt;
        &lt;h3 class="text-xl font-bold"&gt;{{  job.title }}&lt;/h3&gt;
      &lt;/div&gt;

      &lt;div class="mb-5"&gt;
        &lt;div&gt;
          {{  truncatedDescription }}
        &lt;/div&gt;
        &lt;button @click="toggleFullDescription" class="text-green-500 hover:text-green-600 mb-5"&gt;
          {{ showFullDescription ? 'Less' : 'More' }}
        &lt;/button&gt;
      &lt;/div&gt;

      &lt;h3 class="text-green-500 mb-2"&gt;{{ job.salary }} / Year&lt;/h3&gt;

      &lt;div class="border border-gray-100 mb-5"&gt;&lt;/div&gt;

      &lt;div class="flex flex-col lg:flex-row justify-between mb-4"&gt;
        &lt;div class="text-orange-700 mb-3"&gt;
          &lt;i class="pi pi-map-marker text-orange-700"&gt;&lt;/i&gt;
          {{  job.location }}
        &lt;/div&gt;
        &lt;RouterLink
          :to="'/job/' + job.id"
          class="h-&#91;36px] bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg text-center text-sm"
        &gt;
          Read More
        &lt;/RouterLink&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/JobListings.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router';
import JobListing from './JobListing.vue';
import jobData from '@/jobs.json';
import { ref, defineProps } from 'vue';

defineProps({
  limit: Number,
  showButton: {
    type: Boolean,
    default: false
  }
});

const jobs = ref(jobData);
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-blue-50 px-4 py-10"&gt;
    &lt;div class="container-xl lg:container m-auto"&gt;
      &lt;h2 class="text-3xl font-bold text-green-500 mb-6 text-center"&gt;
        Browse Jobs
      &lt;/h2&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
        &lt;JobListing v-for="job in jobs.slice(0, limit || jobs.length)" :key="job.id" :job="job" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;section v-if="showButton" class="m-auto max-w-lg my-10 px-6"&gt;
    &lt;RouterLink
      to="/jobs"
      class="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
      &gt;View All Jobs&lt;/RouterLink
    &gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Navbar Active Link</h3>



<ul class="wp-block-list">
<li>修改 Navbar.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/Navbar.vue
&lt;script setup&gt;
import { RouterLink, useRoute } from 'vue-router'
import logo from '@/assets/img/logo.png'

const isActiveLink = (routePath) =&gt; {
  const route = useRoute();
  return route.path === routePath;
}

&lt;/script&gt;

&lt;template&gt;
  &lt;nav class="bg-green-700 border-b border-green-500"&gt;
    &lt;div class="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8"&gt;
      &lt;div class="flex h-20 items-center justify-between"&gt;
        &lt;div
          class="flex flex-1 items-center justify-center md:items-stretch md:justify-start"
        &gt;
          &lt;!-- Logo --&gt;
          &lt;RouterLink class="flex flex-shrink-0 items-center mr-4" to="/"&gt;
            &lt;img class="h-10 w-auto" :src="logo" alt="Vue Jobs" /&gt;
            &lt;span class="hidden md:block text-white text-2xl font-bold ml-2"
              &gt;Vue Jobs&lt;/span
            &gt;
          &lt;/RouterLink&gt;
          &lt;div class="md:ml-auto"&gt;
            &lt;div class="flex space-x-2"&gt;
              &lt;RouterLink
                to="/"
                :class="&#91;isActiveLink('/') ? 'bg-green-900' : 'hover:bg-gray-900 hover:text-white', 'text-white', 'px-3', 'py-2', 'rounded-md']"
                &gt;Home&lt;/RouterLink
              &gt;
              &lt;RouterLink
                to="/jobs"
                :class="&#91;isActiveLink('/jobs') ? 'bg-green-900' : 'hover:bg-gray-900 hover:text-white', 'text-white', 'px-3', 'py-2', 'rounded-md']"
                &gt;Jobs&lt;/RouterLink
              &gt;
              &lt;RouterLink
                to="/jobs/add"
                :class="&#91;isActiveLink('/jobs/add') ? 'bg-green-900' : 'hover:bg-gray-900 hover:text-white', 'text-white', 'px-3', 'py-2', 'rounded-md']"
                &gt;Add Job&lt;/RouterLink
              &gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/nav&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Not Found Page</h3>



<ul class="wp-block-list">
<li>在 views 資料夾裡面建立 NotFoundView.vue 檔案</li>



<li>到 _theme_files 資料夾裡面複製 not-found.html 檔案程式碼</li>



<li>修改 NotFoundView.vue 檔案</li>



<li>修改 index.js 檔案</li>



<li>在 views 資料夾裡面建立 JobView.vue 檔案</li>



<li>修改 JobView.vue 檔案</li>



<li>到 _theme_files 資料夾裡面複製 job.html 檔案程式碼</li>



<li>修改 JobView.vue 檔案</li>



<li>修改 index.js 檔案</li>



<li>修改 JobListing.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/views/NotFoundView.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router';
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="text-center flex flex-col justify-center items-center h-96"&gt;
    &lt;i class="pi pi-exclamation-triangle text-yellow-500 text-7xl mb-5"&gt;&lt;/i&gt;
    &lt;h1 class="text-6xl font-bold mb-4"&gt;404 Not Found&lt;/h1&gt;
    &lt;p class="text-xl mb-5"&gt;This page does not exist&lt;/p&gt;
    &lt;RouterLink
      to="/"
      class="text-white bg-green-700 hover:bg-green-900 rounded-md px-3 py-2 mt-4"
      &gt;Go Back&lt;/RouterLink
    &gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '@/views/HomeView.vue';
import JobsView from '@/views/JobsView.vue';
import NotFoundView from '@/views/NotFoundView.vue';
import JobView from '@/views/JobView.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: &#91;
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/jobs',
      name: 'jobs',
      component: JobsView
    },
    {
      path: '/jobs/:id',
      name: 'job',
      component: JobView
    },
    {
      path: '/:catchAll(.*)',
      name: 'not-found',
      component: NotFoundView
    },
  ]
});

export default router;
</code></pre>



<pre class="wp-block-code"><code>// src/views/JobView.vue
&lt;script setup&gt;

&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-green-50"&gt;
    &lt;div class="container m-auto py-10 px-6"&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-70/30 w-full gap-6"&gt;
        &lt;main&gt;
          &lt;div
            class="bg-white p-6 rounded-lg shadow-md text-center md:text-left"
          &gt;
            &lt;div class="text-gray-500 mb-4"&gt;Full-Time&lt;/div&gt;
            &lt;h1 class="text-3xl font-bold mb-4"&gt;Senior Vue Developer&lt;/h1&gt;
            &lt;div
              class="text-gray-500 mb-4 flex align-middle justify-center md:justify-start"
            &gt;
              &lt;i
                class="fa-solid fa-location-dot text-lg text-orange-700 mr-2"
              &gt;&lt;/i&gt;
              &lt;p class="text-orange-700"&gt;Boston, MA&lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;

          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-green-800 text-lg font-bold mb-6"&gt;
              Job Description
            &lt;/h3&gt;

            &lt;p class="mb-4"&gt;
              We are seeking a talented Front-End Developer to join our team
              in Boston, MA. The ideal candidate will have strong skills in
              HTML, CSS, and JavaScript, with experience working with modern
              JavaScript frameworks such as Vue or Angular.
            &lt;/p&gt;

            &lt;h3 class="text-green-800 text-lg font-bold mb-2"&gt;Salary&lt;/h3&gt;

            &lt;p class="mb-4"&gt;$70k - $80K / Year&lt;/p&gt;
          &lt;/div&gt;
        &lt;/main&gt;

        &lt;!-- Sidebar --&gt;
        &lt;aside&gt;
          &lt;!-- Company Info --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Company Info&lt;/h3&gt;

            &lt;h2 class="text-2xl"&gt;NewTek Solutions&lt;/h2&gt;

            &lt;p class="my-2"&gt;
              NewTek Solutions is a leading technology company specializing in
              web development and digital solutions. We pride ourselves on
              delivering high-quality products and services to our clients
              while fostering a collaborative and innovative work environment.
            &lt;/p&gt;

            &lt;hr class="my-4" /&gt;

            &lt;h3 class="text-xl"&gt;Contact Email:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;
              contact@newteksolutions.com
            &lt;/p&gt;

            &lt;h3 class="text-xl"&gt;Contact Phone:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;555-555-5555&lt;/p&gt;
          &lt;/div&gt;

          &lt;!-- Manage --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Manage Job&lt;/h3&gt;
            &lt;a
              href="add-job.html"
              class="bg-green-500 hover:bg-green-600 text-white text-center font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
              &gt;Edit Job&lt;/a
            &gt;
            &lt;button
              class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
            &gt;
              Delete Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/aside&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/JobListing.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router';
import { defineProps, ref, computed } from 'vue';

const props = defineProps({
  job: Object
});

const showFullDescription = ref(false);

const toggleFullDescription = () =&gt; {
  showFullDescription.value = !showFullDescription.value;
}

const truncatedDescription = computed(() =&gt; {
  let description = props.job.description;
  if (!showFullDescription.value) {
    description = description.substring(0, 90) + '...';
  }
  return description;
});
&lt;/script&gt;

&lt;template&gt;
  &lt;div class="bg-white rounded-xl shadow-md relative"&gt;
    &lt;div class="p-4"&gt;
      &lt;div class="mb-6"&gt;
        &lt;div class="text-gray-600 my-2"&gt;{{ job.type }}&lt;/div&gt;
        &lt;h3 class="text-xl font-bold"&gt;{{  job.title }}&lt;/h3&gt;
      &lt;/div&gt;

      &lt;div class="mb-5"&gt;
        &lt;div&gt;
          {{  truncatedDescription }}
        &lt;/div&gt;
        &lt;button @click="toggleFullDescription" class="text-green-500 hover:text-green-600 mb-5"&gt;
          {{ showFullDescription ? 'Less' : 'More' }}
        &lt;/button&gt;
      &lt;/div&gt;

      &lt;h3 class="text-green-500 mb-2"&gt;{{ job.salary }} / Year&lt;/h3&gt;

      &lt;div class="border border-gray-100 mb-5"&gt;&lt;/div&gt;

      &lt;div class="flex flex-col lg:flex-row justify-between mb-4"&gt;
        &lt;div class="text-orange-700 mb-3"&gt;
          &lt;i class="pi pi-map-marker text-orange-700"&gt;&lt;/i&gt;
          {{  job.location }}
        &lt;/div&gt;
        &lt;RouterLink
          :to="'/jobs/' + job.id"
          class="h-&#91;36px] bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg text-center text-sm"
        &gt;
          Read More
        &lt;/RouterLink&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">JSON Server REST API</h3>



<ul class="wp-block-list">
<li>安裝 json-server<br>npm i json-server</li>



<li>複製 jobs.json 檔案並把它改名為 jobs2.json 檔案</li>



<li>修改 jobs.json 檔案</li>



<li>修改 package.json 檔案</li>



<li>開啟第二個終端機並執行指令<br>npm run server</li>



<li>如果 port 5000 有重複可改用 port 8000</li>
</ul>



<pre class="wp-block-code"><code>// src/jobs2.json
&#91;
  {
    "id": 1,
    "title": "Senior Vue Developer",
    "type": "Full-Time",
    "description": "We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript, with experience working with modern JavaScript frameworks such as Vue or Angular.",
    "location": "Boston, MA",
    "salary": "$70K - $80K",
    "company": {
      "name": "NewTek Solutions",
      "description": "NewTek Solutions is a leading technology company specializing in web development and digital solutions. We pride ourselves on delivering high-quality products and services to our clients while fostering a collaborative and innovative work environment.",
      "contactEmail": "contact@teksolutions.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": 2,
    "title": "Front-End Engineer (Vue)",
    "type": "Full-Time",
    "description": "Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion for crafting beautiful and responsive web applications. Experience with UI/UX design principles and a strong attention to detail are highly desirable.",
    "location": "Miami, FL",
    "salary": "$70K - $80K",
    "company": {
      "name": "Veneer Solutions",
      "description": "Veneer Solutions is a creative agency specializing in digital design and development. Our team is dedicated to pushing the boundaries of creativity and innovation to deliver exceptional results for our clients.",
      "contactEmail": "contact@loremipsum.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": 3,
    "title": "Vue.js Developer",
    "type": "Full-Time",
    "description": "Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference. We offer competitive compensation and a collaborative work environment where your ideas are valued.",
    "location": "Brooklyn, NY",
    "salary": "$70K - $80K",
    "company": {
      "name": "Dolor Cloud",
      "description": "Dolor Cloud is a leading technology company specializing in digital solutions for businesses of all sizes. With a focus on innovation and customer satisfaction, we are committed to delivering cutting-edge products and services.",
      "contactEmail": "contact@dolorsitamet.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": 4,
    "title": "Vue Front-End Developer",
    "type": "Part-Time",
    "description": "Join our team as a Part-Time Front-End Developer in beautiful Pheonix, AZ. We are looking for a self-motivated individual with a passion for creating engaging user experiences. This position offers flexible hours and the opportunity to work remotely.",
    "location": "Pheonix, AZ",
    "salary": "$60K - $70K",
    "company": {
      "name": "Alpha Elite",
      "description": "Alpha Elite is a dynamic startup specializing in digital marketing and web development. We are committed to fostering a diverse and inclusive workplace where creativity and innovation thrive.",
      "contactEmail": "contact@adipisicingelit.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": 5,
    "title": "Full Stack Vue Developer",
    "type": "Full-Time",
    "description": "Exciting opportunity for a Full-Time Front-End Developer in bustling Atlanta, GA. We are seeking a talented individual with a passion for building elegant and scalable web applications. Join our team and make an impact!",
    "location": "Atlanta, GA",
    "salary": "$90K - $100K",
    "company": {
      "name": "Browning Technologies",
      "description": "Browning Technologies is a rapidly growing technology company specializing in e-commerce solutions. We offer a dynamic and collaborative work environment where employees are encouraged to think creatively and innovate.",
      "contactEmail": "contact@consecteturadipisicing.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": 6,
    "title": "Vue Native Developer",
    "type": "Full-Time",
    "description": "Join our team as a Front-End Developer in beautiful Portland, OR. We are looking for a skilled and enthusiastic individual to help us create innovative web solutions. Competitive salary and great benefits package available.",
    "location": "Portland, OR",
    "salary": "$100K - $110K",
    "company": {
      "name": "Port Solutions INC",
      "description": "Port Solutions is a leading technology company specializing in software development and digital marketing. We are committed to providing our clients with cutting-edge solutions and our employees with a supportive and rewarding work environment.",
      "contactEmail": "contact@ipsumlorem.com",
      "contactPhone": "555-555-5555"
    }
  }
]
</code></pre>



<pre class="wp-block-code"><code>// src/jobs.json
{
  "jobs":&#91;
    {
      "id": 1,
      "title": "Senior Vue Developer",
      "type": "Full-Time",
      "description": "We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript, with experience working with modern JavaScript frameworks such as Vue or Angular.",
      "location": "Boston, MA",
      "salary": "$70K - $80K",
      "company": {
        "name": "NewTek Solutions",
        "description": "NewTek Solutions is a leading technology company specializing in web development and digital solutions. We pride ourselves on delivering high-quality products and services to our clients while fostering a collaborative and innovative work environment.",
        "contactEmail": "contact@teksolutions.com",
        "contactPhone": "555-555-5555"
      }
    },
    {
      "id": 2,
      "title": "Front-End Engineer (Vue)",
      "type": "Full-Time",
      "description": "Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion for crafting beautiful and responsive web applications. Experience with UI/UX design principles and a strong attention to detail are highly desirable.",
      "location": "Miami, FL",
      "salary": "$70K - $80K",
      "company": {
        "name": "Veneer Solutions",
        "description": "Veneer Solutions is a creative agency specializing in digital design and development. Our team is dedicated to pushing the boundaries of creativity and innovation to deliver exceptional results for our clients.",
        "contactEmail": "contact@loremipsum.com",
        "contactPhone": "555-555-5555"
      }
    },
    {
      "id": 3,
      "title": "Vue.js Developer",
      "type": "Full-Time",
      "description": "Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference. We offer competitive compensation and a collaborative work environment where your ideas are valued.",
      "location": "Brooklyn, NY",
      "salary": "$70K - $80K",
      "company": {
        "name": "Dolor Cloud",
        "description": "Dolor Cloud is a leading technology company specializing in digital solutions for businesses of all sizes. With a focus on innovation and customer satisfaction, we are committed to delivering cutting-edge products and services.",
        "contactEmail": "contact@dolorsitamet.com",
        "contactPhone": "555-555-5555"
      }
    },
    {
      "id": 4,
      "title": "Vue Front-End Developer",
      "type": "Part-Time",
      "description": "Join our team as a Part-Time Front-End Developer in beautiful Pheonix, AZ. We are looking for a self-motivated individual with a passion for creating engaging user experiences. This position offers flexible hours and the opportunity to work remotely.",
      "location": "Pheonix, AZ",
      "salary": "$60K - $70K",
      "company": {
        "name": "Alpha Elite",
        "description": "Alpha Elite is a dynamic startup specializing in digital marketing and web development. We are committed to fostering a diverse and inclusive workplace where creativity and innovation thrive.",
        "contactEmail": "contact@adipisicingelit.com",
        "contactPhone": "555-555-5555"
      }
    },
    {
      "id": 5,
      "title": "Full Stack Vue Developer",
      "type": "Full-Time",
      "description": "Exciting opportunity for a Full-Time Front-End Developer in bustling Atlanta, GA. We are seeking a talented individual with a passion for building elegant and scalable web applications. Join our team and make an impact!",
      "location": "Atlanta, GA",
      "salary": "$90K - $100K",
      "company": {
        "name": "Browning Technologies",
        "description": "Browning Technologies is a rapidly growing technology company specializing in e-commerce solutions. We offer a dynamic and collaborative work environment where employees are encouraged to think creatively and innovate.",
        "contactEmail": "contact@consecteturadipisicing.com",
        "contactPhone": "555-555-5555"
      }
    },
    {
      "id": 6,
      "title": "Vue Native Developer",
      "type": "Full-Time",
      "description": "Join our team as a Front-End Developer in beautiful Portland, OR. We are looking for a skilled and enthusiastic individual to help us create innovative web solutions. Competitive salary and great benefits package available.",
      "location": "Portland, OR",
      "salary": "$100K - $110K",
      "company": {
        "name": "Port Solutions INC",
        "description": "Port Solutions is a leading technology company specializing in software development and digital marketing. We are committed to providing our clients with cutting-edge solutions and our employees with a supportive and rewarding work environment.",
        "contactEmail": "contact@ipsumlorem.com",
        "contactPhone": "555-555-5555"
      }
    }
  ]
}
</code></pre>



<pre class="wp-block-code"><code>// package.json
{
  "name": "vue-crash-2024",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "server": "json-server --watch src/jobs.json --port 8000"
  },
  "dependencies": {
    "json-server": "^1.0.0-beta.2",
    "primeicons": "^7.0.0",
    "vue": "^3.4.29",
    "vue-router": "^4.4.3"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.5",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.4.41",
    "tailwindcss": "^3.4.10",
    "vite": "^5.3.1"
  }
}
</code></pre>



<h3 class="wp-block-heading">Fetch Data For JobListings</h3>



<ul class="wp-block-list">
<li>開啟第三個終端機<br>安裝 axios 套件 – npm i axios</li>



<li>修改 JobListings.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/JobListings.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router';
import JobListing from './JobListing.vue';
import { ref, defineProps, onMounted } from 'vue';
import axios from 'axios';

defineProps({
  limit: Number,
  showButton: {
    type: Boolean,
    default: false
  }
});

const jobs = ref(&#91;]);

onMounted(async () =&gt; {
  try {
    const response = await axios.get('http://localhost:8000/jobs');
    jobs.value = response.data;
  } catch (error) {
    console.error('Error fetching jobs', error);
    
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-blue-50 px-4 py-10"&gt;
    &lt;div class="container-xl lg:container m-auto"&gt;
      &lt;h2 class="text-3xl font-bold text-green-500 mb-6 text-center"&gt;
        Browse Jobs
      &lt;/h2&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
        &lt;JobListing v-for="job in jobs.slice(0, limit || jobs.length)" :key="job.id" :job="job" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;section v-if="showButton" class="m-auto max-w-lg my-10 px-6"&gt;
    &lt;RouterLink
      to="/jobs"
      class="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
      &gt;View All Jobs&lt;/RouterLink
    &gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">reactive() Function</h3>



<ul class="wp-block-list">
<li>修改 JobListings.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/JobListings.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router';
import JobListing from './JobListing.vue';
import { reactive, defineProps, onMounted } from 'vue';
import axios from 'axios';

defineProps({
  limit: Number,
  showButton: {
    type: Boolean,
    default: false
  }
});

const jobs = ref(&#91;]);

onMounted(async () =&gt; {
  try {
    const response = await axios.get('http://localhost:8000/jobs');
    jobs.value = response.data;
  } catch (error) {
    console.error('Error fetching jobs', error);
    
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-blue-50 px-4 py-10"&gt;
    &lt;div class="container-xl lg:container m-auto"&gt;
      &lt;h2 class="text-3xl font-bold text-green-500 mb-6 text-center"&gt;
        Browse Jobs
      &lt;/h2&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
        &lt;JobListing v-for="job in jobs.slice(0, limit || jobs.length)" :key="job.id" :job="job" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;section v-if="showButton" class="m-auto max-w-lg my-10 px-6"&gt;
    &lt;RouterLink
      to="/jobs"
      class="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
      &gt;View All Jobs&lt;/RouterLink
    &gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<h4 class="wp-block-heading">ref vs reactive</h4>



<ul class="wp-block-list">
<li>reactive() only takes objects. It does not take primitives like strings, numbers and booleans. It uses `ref()` under the hood.</li>



<li>ref() cant take objects or primitives.</li>



<li>ref() has a `.value` property for reassigning, `reactive()` doesn’t use `.value` and can’t be reassigned</li>
</ul>



<h3 class="wp-block-heading">JobListings Refactor To reactive()</h3>



<ul class="wp-block-list">
<li>修改 JobListings.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/JobListings.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router';
import JobListing from './JobListing.vue';
import { reactive, defineProps, onMounted } from 'vue';
import axios from 'axios';

defineProps({
  limit: Number,
  showButton: {
    type: Boolean,
    default: false
  }
});

const state = reactive({
  jobs: &#91;],
  isLoading: true
});

onMounted(async () =&gt; {
  try {
    const response = await axios.get('http://localhost:8000/jobs');
    state.jobs = response.data;
  } catch (error) {
    console.error('Error fetching jobs', error);
  } finally {
    state.isLoading = false;
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-blue-50 px-4 py-10"&gt;
    &lt;div class="container-xl lg:container m-auto"&gt;
      &lt;h2 class="text-3xl font-bold text-green-500 mb-6 text-center"&gt;
        Browse Jobs
      &lt;/h2&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
        &lt;JobListing v-for="job in state.jobs.slice(0, limit || state.jobs.length)" :key="job.id" :job="job" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;section v-if="showButton" class="m-auto max-w-lg my-10 px-6"&gt;
    &lt;RouterLink
      to="/jobs"
      class="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
      &gt;View All Jobs&lt;/RouterLink
    &gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Vue Spinner</h3>



<ul class="wp-block-list">
<li>使用終端機安裝 Vue Spinner<br>npm i vue-spinner</li>



<li>修改 JobListings.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/JobListings.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router';
import JobListing from './JobListing.vue';
import { reactive, defineProps, onMounted } from 'vue';
import PulseLoader from 'vue-spinner/src/PulseLoader.vue';
import axios from 'axios';

defineProps({
  limit: Number,
  showButton: {
    type: Boolean,
    default: false
  }
});

const state = reactive({
  jobs: &#91;],
  isLoading: true
});

onMounted(async () =&gt; {
  try {
    const response = await axios.get('http://localhost:8000/jobs');
    state.jobs = response.data;
  } catch (error) {
    console.error('Error fetching jobs', error);
  } finally {
    state.isLoading = false;
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-blue-50 px-4 py-10"&gt;
    &lt;div class="container-xl lg:container m-auto"&gt;
      &lt;h2 class="text-3xl font-bold text-green-500 mb-6 text-center"&gt;
        Browse Jobs
      &lt;/h2&gt;
      &lt;!-- Show loading spinner while loading is true --&gt;
      &lt;div v-if="state.isLoading" class="text-center text-gray-500 py-6"&gt;
        &lt;PulseLoader /&gt;
      &lt;/div&gt;

      &lt;!-- Show job listing when done loading --&gt;
      &lt;div v-else class="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
        &lt;JobListing v-for="job in state.jobs.slice(0, limit || state.jobs.length)" :key="job.id" :job="job" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;section v-if="showButton" class="m-auto max-w-lg my-10 px-6"&gt;
    &lt;RouterLink
      to="/jobs"
      class="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
      &gt;View All Jobs&lt;/RouterLink
    &gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Fetch Single Job &amp; Display Data</h3>



<ul class="wp-block-list">
<li>修改 JobView.vue 檔案</li>



<li>複製 JobListings.vue 檔案中的程式碼 onMounted 的部分</li>



<li>修改 JobView.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/views/JobView.vue
&lt;script setup&gt;
import PulseLoader from 'vue-spinner/src/PulseLoader.vue';
import { reactive, onMounted } from 'vue';
import { useRoute, RouterLink } from 'vue-router';
import axios from 'axios';

const route = useRoute();

const jobId = route.params.id;

const state = reactive({
  job: {},
  isLoading: true
});

onMounted(async () =&gt; {
  try {
    const response = await axios.get(`http://localhost:8000/jobs/${jobId}`);
    state.job = response.data;
  } catch (error) {
    console.error('Error fetching job', error);
  } finally {
    state.isLoading = false;
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;section v-if="!state.isLoading" class="bg-green-50"&gt;
    &lt;div class="container m-auto py-10 px-6"&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-70/30 w-full gap-6"&gt;
        &lt;main&gt;
          &lt;div
            class="bg-white p-6 rounded-lg shadow-md text-center md:text-left"
          &gt;
            &lt;div class="text-gray-500 mb-4"&gt;{{ state.job.type }}&lt;/div&gt;
            &lt;h1 class="text-3xl font-bold mb-4"&gt;{{  state.job.title }}&lt;/h1&gt;
            &lt;div
              class="text-gray-500 mb-4 flex align-middle justify-center md:justify-start"
            &gt;
              &lt;i
                class="fa-solid fa-location-dot text-lg text-orange-700 mr-2"
              &gt;&lt;/i&gt;
              &lt;p class="text-orange-700"&gt;{{ state.job.location }}&lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;

          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-green-800 text-lg font-bold mb-6"&gt;
              Job Description
            &lt;/h3&gt;

            &lt;p class="mb-4"&gt;
              {{  state.job.description }}
            &lt;/p&gt;

            &lt;h3 class="text-green-800 text-lg font-bold mb-2"&gt;Salary&lt;/h3&gt;

            &lt;p class="mb-4"&gt;{{ state.job.salary }} / Year&lt;/p&gt;
          &lt;/div&gt;
        &lt;/main&gt;

        &lt;!-- Sidebar --&gt;
        &lt;aside&gt;
          &lt;!-- Company Info --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Company Info&lt;/h3&gt;

            &lt;h2 class="text-2xl"&gt;{{ state.job.company.name }}&lt;/h2&gt;

            &lt;p class="my-2"&gt;
              {{ state.job.company.description }}
            &lt;/p&gt;

            &lt;hr class="my-4" /&gt;

            &lt;h3 class="text-xl"&gt;Contact Email:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;
              {{ state.job.company.contactEmail }}
            &lt;/p&gt;

            &lt;h3 class="text-xl"&gt;Contact Phone:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;{{ state.job.company.contactPhone }}&lt;/p&gt;
          &lt;/div&gt;

          &lt;!-- Manage --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Manage Job&lt;/h3&gt;
            &lt;RouterLink
              :to="`/jobs/edit/${state.job.id}`"
              class="bg-green-500 hover:bg-green-600 text-white text-center font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
              &gt;Edit Job&lt;/RouterLink
            &gt;
            &lt;button
              class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
            &gt;
              Delete Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/aside&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;div v-else class="text-center text-gray-500 py-6"&gt;
    &lt;PulseLoader /&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">BackButton Component</h3>



<ul class="wp-block-list">
<li>在 components 資料夾裡面建立 BackButton.vue 檔案</li>



<li>修改 BackButton.vue 檔案</li>



<li>到 _theme_files 資料夾的 job.html 檔案複製 Go Back 程式碼貼到 BackButton.vue 檔案</li>



<li>修改 BackButton.vue 檔案</li>



<li>修改 JobView.vue 檔案</li>



<li>修改 BackButton.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/components/BackButton.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router';
&lt;/script&gt;

&lt;template&gt;
  &lt;section&gt;
    &lt;div class="container m-auto py-6 px-6"&gt;
      &lt;RouterLink
        to="/jobs"
        class="text-green-500 hover:text-green-600 flex items-center"
      &gt;
        &lt;i class="pi pi-arrow-circle-left mr-3"&gt;&lt;/i&gt; Back to Job Listings
      &lt;/RouterLink&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/views/JobView.vue
&lt;script setup&gt;
import PulseLoader from 'vue-spinner/src/PulseLoader.vue';
import BackButton from '@/components/BackButton.vue';
import { reactive, onMounted } from 'vue';
import { useRoute, RouterLink } from 'vue-router';
import axios from 'axios';

const route = useRoute();

const jobId = route.params.id;

const state = reactive({
  job: {},
  isLoading: true
});

onMounted(async () =&gt; {
  try {
    const response = await axios.get(`http://localhost:8000/jobs/${jobId}`);
    state.job = response.data;
  } catch (error) {
    console.error('Error fetching job', error);
  } finally {
    state.isLoading = false;
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;BackButton /&gt;
  &lt;section v-if="!state.isLoading" class="bg-green-50"&gt;
    &lt;div class="container m-auto py-10 px-6"&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-70/30 w-full gap-6"&gt;
        &lt;main&gt;
          &lt;div
            class="bg-white p-6 rounded-lg shadow-md text-center md:text-left"
          &gt;
            &lt;div class="text-gray-500 mb-4"&gt;{{ state.job.type }}&lt;/div&gt;
            &lt;h1 class="text-3xl font-bold mb-4"&gt;{{  state.job.title }}&lt;/h1&gt;
            &lt;div
              class="text-gray-500 mb-4 flex align-middle justify-center md:justify-start"
            &gt;
              &lt;i
                class="fa-solid fa-location-dot text-lg text-orange-700 mr-2"
              &gt;&lt;/i&gt;
              &lt;p class="text-orange-700"&gt;{{ state.job.location }}&lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;

          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-green-800 text-lg font-bold mb-6"&gt;
              Job Description
            &lt;/h3&gt;

            &lt;p class="mb-4"&gt;
              {{  state.job.description }}
            &lt;/p&gt;

            &lt;h3 class="text-green-800 text-lg font-bold mb-2"&gt;Salary&lt;/h3&gt;

            &lt;p class="mb-4"&gt;{{ state.job.salary }} / Year&lt;/p&gt;
          &lt;/div&gt;
        &lt;/main&gt;

        &lt;!-- Sidebar --&gt;
        &lt;aside&gt;
          &lt;!-- Company Info --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Company Info&lt;/h3&gt;

            &lt;h2 class="text-2xl"&gt;{{ state.job.company.name }}&lt;/h2&gt;

            &lt;p class="my-2"&gt;
              {{ state.job.company.description }}
            &lt;/p&gt;

            &lt;hr class="my-4" /&gt;

            &lt;h3 class="text-xl"&gt;Contact Email:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;
              {{ state.job.company.contactEmail }}
            &lt;/p&gt;

            &lt;h3 class="text-xl"&gt;Contact Phone:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;{{ state.job.company.contactPhone }}&lt;/p&gt;
          &lt;/div&gt;

          &lt;!-- Manage --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Manage Job&lt;/h3&gt;
            &lt;RouterLink
              :to="`/jobs/edit/${state.job.id}`"
              class="bg-green-500 hover:bg-green-600 text-white text-center font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
              &gt;Edit Job&lt;/RouterLink
            &gt;
            &lt;button
              class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
            &gt;
              Delete Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/aside&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;div v-else class="text-center text-gray-500 py-6"&gt;
    &lt;PulseLoader /&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Proxying</h3>



<ul class="wp-block-list">
<li>修改 vite.config.js 檔案</li>



<li>修改 JobView.vue 檔案</li>



<li>修改 JobListings.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// vite.config.js
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: &#91;
    vue(),
  ],
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8000/',
        changeOrigin: true,
        rewrite: (path) =&gt; path.replace(/^\/api/, ''),
      }
    }
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})
</code></pre>



<pre class="wp-block-code"><code>// src/views/JobView.vue
&lt;script setup&gt;
import PulseLoader from 'vue-spinner/src/PulseLoader.vue';
import BackButton from '@/components/BackButton.vue';
import { reactive, onMounted } from 'vue';
import { useRoute, RouterLink } from 'vue-router';
import axios from 'axios';

const route = useRoute();

const jobId = route.params.id;

const state = reactive({
  job: {},
  isLoading: true
});

onMounted(async () =&gt; {
  try {
    const response = await axios.get(`/api/jobs/${jobId}`);
    state.job = response.data;
  } catch (error) {
    console.error('Error fetching job', error);
  } finally {
    state.isLoading = false;
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;BackButton /&gt;
  &lt;section v-if="!state.isLoading" class="bg-green-50"&gt;
    &lt;div class="container m-auto py-10 px-6"&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-70/30 w-full gap-6"&gt;
        &lt;main&gt;
          &lt;div
            class="bg-white p-6 rounded-lg shadow-md text-center md:text-left"
          &gt;
            &lt;div class="text-gray-500 mb-4"&gt;{{ state.job.type }}&lt;/div&gt;
            &lt;h1 class="text-3xl font-bold mb-4"&gt;{{  state.job.title }}&lt;/h1&gt;
            &lt;div
              class="text-gray-500 mb-4 flex align-middle justify-center md:justify-start"
            &gt;
              &lt;i
                class="fa-solid fa-location-dot text-lg text-orange-700 mr-2"
              &gt;&lt;/i&gt;
              &lt;p class="text-orange-700"&gt;{{ state.job.location }}&lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;

          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-green-800 text-lg font-bold mb-6"&gt;
              Job Description
            &lt;/h3&gt;

            &lt;p class="mb-4"&gt;
              {{  state.job.description }}
            &lt;/p&gt;

            &lt;h3 class="text-green-800 text-lg font-bold mb-2"&gt;Salary&lt;/h3&gt;

            &lt;p class="mb-4"&gt;{{ state.job.salary }} / Year&lt;/p&gt;
          &lt;/div&gt;
        &lt;/main&gt;

        &lt;!-- Sidebar --&gt;
        &lt;aside&gt;
          &lt;!-- Company Info --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Company Info&lt;/h3&gt;

            &lt;h2 class="text-2xl"&gt;{{ state.job.company.name }}&lt;/h2&gt;

            &lt;p class="my-2"&gt;
              {{ state.job.company.description }}
            &lt;/p&gt;

            &lt;hr class="my-4" /&gt;

            &lt;h3 class="text-xl"&gt;Contact Email:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;
              {{ state.job.company.contactEmail }}
            &lt;/p&gt;

            &lt;h3 class="text-xl"&gt;Contact Phone:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;{{ state.job.company.contactPhone }}&lt;/p&gt;
          &lt;/div&gt;

          &lt;!-- Manage --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Manage Job&lt;/h3&gt;
            &lt;RouterLink
              :to="`/jobs/edit/${state.job.id}`"
              class="bg-green-500 hover:bg-green-600 text-white text-center font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
              &gt;Edit Job&lt;/RouterLink
            &gt;
            &lt;button
              class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
            &gt;
              Delete Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/aside&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;div v-else class="text-center text-gray-500 py-6"&gt;
    &lt;PulseLoader /&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/components/JobListings.vue
&lt;script setup&gt;
import { RouterLink } from 'vue-router';
import JobListing from './JobListing.vue';
import { reactive, defineProps, onMounted } from 'vue';
import PulseLoader from 'vue-spinner/src/PulseLoader.vue';
import axios from 'axios';

defineProps({
  limit: Number,
  showButton: {
    type: Boolean,
    default: false
  }
});

const state = reactive({
  jobs: &#91;],
  isLoading: true
});

onMounted(async () =&gt; {
  try {
    const response = await axios.get('/api/jobs');
    state.jobs = response.data;
  } catch (error) {
    console.error('Error fetching jobs', error);
  } finally {
    state.isLoading = false;
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-blue-50 px-4 py-10"&gt;
    &lt;div class="container-xl lg:container m-auto"&gt;
      &lt;h2 class="text-3xl font-bold text-green-500 mb-6 text-center"&gt;
        Browse Jobs
      &lt;/h2&gt;
      &lt;!-- Show loading spinner while loading is true --&gt;
      &lt;div v-if="state.isLoading" class="text-center text-gray-500 py-6"&gt;
        &lt;PulseLoader /&gt;
      &lt;/div&gt;

      &lt;!-- Show job listing when done loading --&gt;
      &lt;div v-else class="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
        &lt;JobListing v-for="job in state.jobs.slice(0, limit || state.jobs.length)" :key="job.id" :job="job" /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;section v-if="showButton" class="m-auto max-w-lg my-10 px-6"&gt;
    &lt;RouterLink
      to="/jobs"
      class="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
      &gt;View All Jobs&lt;/RouterLink
    &gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Add Job Page</h3>



<ul class="wp-block-list">
<li>在 views 資料夾裡面建立 AddJobView.vue 檔案</li>



<li>修改 AddJobView.vue 檔案</li>



<li>修改 index.js 檔案</li>



<li>到 _theme_files 資料夾裡面找到 add-job.html 檔案複製程式碼</li>



<li>修改 AddJobView.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/views/AddJobView.vue
&lt;script setup&gt;
import { reactive } from 'vue';

const form = reactive({
  type: 'Full-Time',
  title: '',
  description: '',
  salary: '',
  location: '',
  company: {
    name: '',
    description: '',
    contactEmail: '',
    contactPhone: ''
  }
});

const handleSubmit = async () =&gt; {
  console.log(form.title);
};
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-green-50"&gt;
    &lt;div class="container m-auto max-w-2xl py-24"&gt;
      &lt;div
        class="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"
      &gt;
        &lt;form @submit.prevent="handleSubmit"&gt;
          &lt;h2 class="text-3xl text-center font-semibold mb-6"&gt;Add Job&lt;/h2&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Job Type&lt;/label
            &gt;
            &lt;select
              v-model="form.type"
              id="type"
              name="type"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
              &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
              &lt;option value="Remote"&gt;Remote&lt;/option&gt;
              &lt;option value="Internship"&gt;Internship&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"
              &gt;Job Listing Name&lt;/label
            &gt;
            &lt;input
              v-model="form.title"
              type="text"
              id="name"
              name="name"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="eg. Beautiful Apartment In Miami"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.description"
              id="description"
              name="description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="Add any job duties, expectations, requirements, etc"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Salary&lt;/label
            &gt;
            &lt;select
              v-model="form.salary"
              id="salary"
              name="salary"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Under $50K"&gt;under $50K&lt;/option&gt;
              &lt;option value="$50K - $60K"&gt;$50 - $60K&lt;/option&gt;
              &lt;option value="$60K - $70K"&gt;$60 - $70K&lt;/option&gt;
              &lt;option value="$70K - $80K"&gt;$70 - $80K&lt;/option&gt;
              &lt;option value="$80K - $90K"&gt;$80 - $90K&lt;/option&gt;
              &lt;option value="$90K - $100K"&gt;$90 - $100K&lt;/option&gt;
              &lt;option value="$100K - $125K"&gt;$100 - $125K&lt;/option&gt;
              &lt;option value="$125K - $150K"&gt;$125 - $150K&lt;/option&gt;
              &lt;option value="$150K - $175K"&gt;$150 - $175K&lt;/option&gt;
              &lt;option value="$175K - $200K"&gt;$175 - $200K&lt;/option&gt;
              &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"&gt;
              Location
            &lt;/label&gt;
            &lt;input
              v-model="form.location"
              type="text"
              id="location"
              name="location"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="Company Location"
              required
            /&gt;
          &lt;/div&gt;

          &lt;h3 class="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="company" class="block text-gray-700 font-bold mb-2"
              &gt;Company Name&lt;/label
            &gt;
            &lt;input
              v-model="form.company.name"
              type="text"
              id="company"
              name="company"
              class="border rounded w-full py-2 px-3"
              placeholder="Company Name"
            /&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="company_description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Company Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.company.description"
              id="company_description"
              name="company_description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="What does your company do?"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_email"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Email&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactEmail"
              type="email"
              id="contact_email"
              name="contact_email"
              class="border rounded w-full py-2 px-3"
              placeholder="Email address for applicants"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_phone"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Phone&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactPhone"
              type="tel"
              id="contact_phone"
              name="contact_phone"
              class="border rounded w-full py-2 px-3"
              placeholder="Optional phone for applicants"
            /&gt;
          &lt;/div&gt;

          &lt;div&gt;
            &lt;button
              class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
              type="submit"
            &gt;
              Add Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '@/views/HomeView.vue';
import JobsView from '@/views/JobsView.vue';
import NotFoundView from '@/views/NotFoundView.vue';
import JobView from '@/views/JobView.vue';
import AddJobView from '@/views/AddJobView.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: &#91;
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/jobs',
      name: 'jobs',
      component: JobsView
    },
    {
      path: '/jobs/:id',
      name: 'job',
      component: JobView
    },
    {
      path: '/jobs/add',
      name: 'add-job',
      component: AddJobView,
    },
    {
      path: '/:catchAll(.*)',
      name: 'not-found',
      component: NotFoundView
    },
  ]
});

export default router;
</code></pre>



<h3 class="wp-block-heading">Save Job POST</h3>



<ul class="wp-block-list">
<li>修改 AddJobView.vue 檔案</li>



<li>到 JobView.vue 檔案複製程式碼關於 trycatch 的部分</li>



<li>修改 AddJobView.vue 檔案</li>



<li>測試表單</li>



<li>修改 JobView.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/views/AddJobView.vue
&lt;script setup&gt;
import router from '@/router';
import { reactive } from 'vue';
import axios from 'axios';

const form = reactive({
  type: 'Full-Time',
  title: '',
  description: '',
  salary: '',
  location: '',
  company: {
    name: '',
    description: '',
    contactEmail: '',
    contactPhone: ''
  }
});

const handleSubmit = async () =&gt; {
  const newJob = {
    title: form.title,
    type: form.type,
    location: form.location,
    description: form.description,
    salary: form.salary,
    company: {
      name: form.company.name,
      description: form.company.description,
      contactEmail: form.company.contactEmail,
      contactPhone: form.company.contactPhone,
    }
  };

  try {
    const response = await axios.post(`/api/jobs`, newJob);
    // @todo - show toast
    router.push(`/jobs/${response.data.id}`);
  } catch (error) {
    console.error('Error fetching job', error);
    // @todo - show toast
  }
};
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-green-50"&gt;
    &lt;div class="container m-auto max-w-2xl py-24"&gt;
      &lt;div
        class="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"
      &gt;
        &lt;form @submit.prevent="handleSubmit"&gt;
          &lt;h2 class="text-3xl text-center font-semibold mb-6"&gt;Add Job&lt;/h2&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Job Type&lt;/label
            &gt;
            &lt;select
              v-model="form.type"
              id="type"
              name="type"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
              &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
              &lt;option value="Remote"&gt;Remote&lt;/option&gt;
              &lt;option value="Internship"&gt;Internship&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"
              &gt;Job Listing Name&lt;/label
            &gt;
            &lt;input
              v-model="form.title"
              type="text"
              id="name"
              name="name"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="eg. Beautiful Apartment In Miami"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.description"
              id="description"
              name="description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="Add any job duties, expectations, requirements, etc"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Salary&lt;/label
            &gt;
            &lt;select
              v-model="form.salary"
              id="salary"
              name="salary"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Under $50K"&gt;under $50K&lt;/option&gt;
              &lt;option value="$50K - $60K"&gt;$50 - $60K&lt;/option&gt;
              &lt;option value="$60K - $70K"&gt;$60 - $70K&lt;/option&gt;
              &lt;option value="$70K - $80K"&gt;$70 - $80K&lt;/option&gt;
              &lt;option value="$80K - $90K"&gt;$80 - $90K&lt;/option&gt;
              &lt;option value="$90K - $100K"&gt;$90 - $100K&lt;/option&gt;
              &lt;option value="$100K - $125K"&gt;$100 - $125K&lt;/option&gt;
              &lt;option value="$125K - $150K"&gt;$125 - $150K&lt;/option&gt;
              &lt;option value="$150K - $175K"&gt;$150 - $175K&lt;/option&gt;
              &lt;option value="$175K - $200K"&gt;$175 - $200K&lt;/option&gt;
              &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"&gt;
              Location
            &lt;/label&gt;
            &lt;input
              v-model="form.location"
              type="text"
              id="location"
              name="location"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="Company Location"
              required
            /&gt;
          &lt;/div&gt;

          &lt;h3 class="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="company" class="block text-gray-700 font-bold mb-2"
              &gt;Company Name&lt;/label
            &gt;
            &lt;input
              v-model="form.company.name"
              type="text"
              id="company"
              name="company"
              class="border rounded w-full py-2 px-3"
              placeholder="Company Name"
            /&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="company_description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Company Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.company.description"
              id="company_description"
              name="company_description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="What does your company do?"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_email"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Email&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactEmail"
              type="email"
              id="contact_email"
              name="contact_email"
              class="border rounded w-full py-2 px-3"
              placeholder="Email address for applicants"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_phone"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Phone&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactPhone"
              type="tel"
              id="contact_phone"
              name="contact_phone"
              class="border rounded w-full py-2 px-3"
              placeholder="Optional phone for applicants"
            /&gt;
          &lt;/div&gt;

          &lt;div&gt;
            &lt;button
              class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
              type="submit"
            &gt;
              Add Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/views/JobView.vue
&lt;script setup&gt;
import PulseLoader from 'vue-spinner/src/PulseLoader.vue';
import BackButton from '@/components/BackButton.vue';
import { reactive, onMounted } from 'vue';
import { useRoute, RouterLink } from 'vue-router';
import axios from 'axios';

const route = useRoute();

const jobId = route.params.id;

const state = reactive({
  job: {},
  isLoading: true
});

onMounted(async () =&gt; {
  try {
    const response = await axios.get(`/api/jobs/${jobId}`);
    state.job = response.data;
  } catch (error) {
    console.error('Error fetching job', error);
  } finally {
    state.isLoading = false;
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;BackButton /&gt;
  &lt;section v-if="!state.isLoading" class="bg-green-50"&gt;
    &lt;div class="container m-auto py-10 px-6"&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-70/30 w-full gap-6"&gt;
        &lt;main&gt;
          &lt;div
            class="bg-white p-6 rounded-lg shadow-md text-center md:text-left"
          &gt;
            &lt;div class="text-gray-500 mb-4"&gt;{{ state.job.type }}&lt;/div&gt;
            &lt;h1 class="text-3xl font-bold mb-4"&gt;{{  state.job.title }}&lt;/h1&gt;
            &lt;div
              class="text-gray-500 mb-4 flex align-middle justify-center md:justify-start"
            &gt;
              &lt;i
                class="pi pi-map-marker text-xl text-orange-700 mr-2"
              &gt;&lt;/i&gt;
              &lt;p class="text-orange-700"&gt;{{ state.job.location }}&lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;

          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-green-800 text-lg font-bold mb-6"&gt;
              Job Description
            &lt;/h3&gt;

            &lt;p class="mb-4"&gt;
              {{  state.job.description }}
            &lt;/p&gt;

            &lt;h3 class="text-green-800 text-lg font-bold mb-2"&gt;Salary&lt;/h3&gt;

            &lt;p class="mb-4"&gt;{{ state.job.salary }} / Year&lt;/p&gt;
          &lt;/div&gt;
        &lt;/main&gt;

        &lt;!-- Sidebar --&gt;
        &lt;aside&gt;
          &lt;!-- Company Info --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Company Info&lt;/h3&gt;

            &lt;h2 class="text-2xl"&gt;{{ state.job.company.name }}&lt;/h2&gt;

            &lt;p class="my-2"&gt;
              {{ state.job.company.description }}
            &lt;/p&gt;

            &lt;hr class="my-4" /&gt;

            &lt;h3 class="text-xl"&gt;Contact Email:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;
              {{ state.job.company.contactEmail }}
            &lt;/p&gt;

            &lt;h3 class="text-xl"&gt;Contact Phone:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;{{ state.job.company.contactPhone }}&lt;/p&gt;
          &lt;/div&gt;

          &lt;!-- Manage --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Manage Job&lt;/h3&gt;
            &lt;RouterLink
              :to="`/jobs/edit/${state.job.id}`"
              class="bg-green-500 hover:bg-green-600 text-white text-center font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
              &gt;Edit Job&lt;/RouterLink
            &gt;
            &lt;button
              class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
            &gt;
              Delete Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/aside&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;div v-else class="text-center text-gray-500 py-6"&gt;
    &lt;PulseLoader /&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Toast Notifications</h3>



<ul class="wp-block-list">
<li>安裝 vue-toastification 套件<br>npm i vue-toastification@next</li>



<li>修改 main.js 檔案</li>



<li>修改 AddJobView.vue 檔案</li>



<li>測試表單送出後彈跳訊息是否能正常運作</li>
</ul>



<pre class="wp-block-code"><code>// src/main.js
import './assets/main.css'
import 'primeicons/primeicons.css'
import Toast from 'vue-toastification'
import 'vue-toastification/dist/index.css'
import router from './router'

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.use(router)
app.use(Toast)

app.mount('#app')
</code></pre>



<pre class="wp-block-code"><code>// src/views/AddJobView.vue
&lt;script setup&gt;
import router from '@/router';
import { reactive } from 'vue';
import { useToast } from 'vue-toastification';
import axios from 'axios';

const form = reactive({
  type: 'Full-Time',
  title: '',
  description: '',
  salary: '',
  location: '',
  company: {
    name: '',
    description: '',
    contactEmail: '',
    contactPhone: ''
  }
});

const toast = useToast();

const handleSubmit = async () =&gt; {
  const newJob = {
    title: form.title,
    type: form.type,
    location: form.location,
    description: form.description,
    salary: form.salary,
    company: {
      name: form.company.name,
      description: form.company.description,
      contactEmail: form.company.contactEmail,
      contactPhone: form.company.contactPhone,
    }
  };

  try {
    const response = await axios.post(`/api/jobs`, newJob);
    toast.success('Job Added Successfully');
    router.push(`/jobs/${response.data.id}`);
  } catch (error) {
    console.error('Error fetching job', error);
    toast.error('Job Was Not Added');
  }
};
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-green-50"&gt;
    &lt;div class="container m-auto max-w-2xl py-24"&gt;
      &lt;div
        class="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"
      &gt;
        &lt;form @submit.prevent="handleSubmit"&gt;
          &lt;h2 class="text-3xl text-center font-semibold mb-6"&gt;Add Job&lt;/h2&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Job Type&lt;/label
            &gt;
            &lt;select
              v-model="form.type"
              id="type"
              name="type"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
              &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
              &lt;option value="Remote"&gt;Remote&lt;/option&gt;
              &lt;option value="Internship"&gt;Internship&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"
              &gt;Job Listing Name&lt;/label
            &gt;
            &lt;input
              v-model="form.title"
              type="text"
              id="name"
              name="name"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="eg. Beautiful Apartment In Miami"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.description"
              id="description"
              name="description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="Add any job duties, expectations, requirements, etc"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Salary&lt;/label
            &gt;
            &lt;select
              v-model="form.salary"
              id="salary"
              name="salary"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Under $50K"&gt;under $50K&lt;/option&gt;
              &lt;option value="$50K - $60K"&gt;$50 - $60K&lt;/option&gt;
              &lt;option value="$60K - $70K"&gt;$60 - $70K&lt;/option&gt;
              &lt;option value="$70K - $80K"&gt;$70 - $80K&lt;/option&gt;
              &lt;option value="$80K - $90K"&gt;$80 - $90K&lt;/option&gt;
              &lt;option value="$90K - $100K"&gt;$90 - $100K&lt;/option&gt;
              &lt;option value="$100K - $125K"&gt;$100 - $125K&lt;/option&gt;
              &lt;option value="$125K - $150K"&gt;$125 - $150K&lt;/option&gt;
              &lt;option value="$150K - $175K"&gt;$150 - $175K&lt;/option&gt;
              &lt;option value="$175K - $200K"&gt;$175 - $200K&lt;/option&gt;
              &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"&gt;
              Location
            &lt;/label&gt;
            &lt;input
              v-model="form.location"
              type="text"
              id="location"
              name="location"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="Company Location"
              required
            /&gt;
          &lt;/div&gt;

          &lt;h3 class="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="company" class="block text-gray-700 font-bold mb-2"
              &gt;Company Name&lt;/label
            &gt;
            &lt;input
              v-model="form.company.name"
              type="text"
              id="company"
              name="company"
              class="border rounded w-full py-2 px-3"
              placeholder="Company Name"
            /&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="company_description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Company Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.company.description"
              id="company_description"
              name="company_description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="What does your company do?"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_email"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Email&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactEmail"
              type="email"
              id="contact_email"
              name="contact_email"
              class="border rounded w-full py-2 px-3"
              placeholder="Email address for applicants"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_phone"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Phone&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactPhone"
              type="tel"
              id="contact_phone"
              name="contact_phone"
              class="border rounded w-full py-2 px-3"
              placeholder="Optional phone for applicants"
            /&gt;
          &lt;/div&gt;

          &lt;div&gt;
            &lt;button
              class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
              type="submit"
            &gt;
              Add Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Delete Job</h3>



<ul class="wp-block-list">
<li>修改 JobView.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/views/JobView.vue
&lt;script setup&gt;
import PulseLoader from 'vue-spinner/src/PulseLoader.vue';
import BackButton from '@/components/BackButton.vue';
import { reactive, onMounted } from 'vue';
import { useRoute, RouterLink, useRouter } from 'vue-router';
import { useToast } from 'vue-toastification';
import axios from 'axios';

const route = useRoute();
const router = useRouter();
const toast = useToast();

const jobId = route.params.id;

const state = reactive({
  job: {},
  isLoading: true
});

const deleteJob = async () =&gt; {
  try {
    const confirm = window.confirm('Are you sure you want to delete this job?');
    if (confirm) {
      await axios.delete(`/api/jobs/${jobId}`);
      toast.success('Job Deleted Successfully');
      router.push('/jobs');
    }
  } catch (error) {
    console.error('Error deleting job', error);
    toast.error('Job Not Deleted');
  }
}

onMounted(async () =&gt; {
  try {
    const response = await axios.get(`/api/jobs/${jobId}`);
    state.job = response.data;
  } catch (error) {
    console.error('Error fetching job', error);
  } finally {
    state.isLoading = false;
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;BackButton /&gt;
  &lt;section v-if="!state.isLoading" class="bg-green-50"&gt;
    &lt;div class="container m-auto py-10 px-6"&gt;
      &lt;div class="grid grid-cols-1 md:grid-cols-70/30 w-full gap-6"&gt;
        &lt;main&gt;
          &lt;div
            class="bg-white p-6 rounded-lg shadow-md text-center md:text-left"
          &gt;
            &lt;div class="text-gray-500 mb-4"&gt;{{ state.job.type }}&lt;/div&gt;
            &lt;h1 class="text-3xl font-bold mb-4"&gt;{{  state.job.title }}&lt;/h1&gt;
            &lt;div
              class="text-gray-500 mb-4 flex align-middle justify-center md:justify-start"
            &gt;
              &lt;i
                class="pi pi-map-marker text-xl text-orange-700 mr-2"
              &gt;&lt;/i&gt;
              &lt;p class="text-orange-700"&gt;{{ state.job.location }}&lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;

          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-green-800 text-lg font-bold mb-6"&gt;
              Job Description
            &lt;/h3&gt;

            &lt;p class="mb-4"&gt;
              {{  state.job.description }}
            &lt;/p&gt;

            &lt;h3 class="text-green-800 text-lg font-bold mb-2"&gt;Salary&lt;/h3&gt;

            &lt;p class="mb-4"&gt;{{ state.job.salary }} / Year&lt;/p&gt;
          &lt;/div&gt;
        &lt;/main&gt;

        &lt;!-- Sidebar --&gt;
        &lt;aside&gt;
          &lt;!-- Company Info --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Company Info&lt;/h3&gt;

            &lt;h2 class="text-2xl"&gt;{{ state.job.company.name }}&lt;/h2&gt;

            &lt;p class="my-2"&gt;
              {{ state.job.company.description }}
            &lt;/p&gt;

            &lt;hr class="my-4" /&gt;

            &lt;h3 class="text-xl"&gt;Contact Email:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;
              {{ state.job.company.contactEmail }}
            &lt;/p&gt;

            &lt;h3 class="text-xl"&gt;Contact Phone:&lt;/h3&gt;

            &lt;p class="my-2 bg-green-100 p-2 font-bold"&gt;{{ state.job.company.contactPhone }}&lt;/p&gt;
          &lt;/div&gt;

          &lt;!-- Manage --&gt;
          &lt;div class="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
            &lt;h3 class="text-xl font-bold mb-6"&gt;Manage Job&lt;/h3&gt;
            &lt;RouterLink
              :to="`/jobs/edit/${state.job.id}`"
              class="bg-green-500 hover:bg-green-600 text-white text-center font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
              &gt;Edit Job&lt;/RouterLink
            &gt;
            &lt;button @click="deleteJob"
              class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
            &gt;
              Delete Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/aside&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;div v-else class="text-center text-gray-500 py-6"&gt;
    &lt;PulseLoader /&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Edit Page</h3>



<ul class="wp-block-list">
<li>在 views 資料夾裡面建立 EditJobView.vue 檔案</li>



<li>修改 EditJobView.vue 檔案</li>



<li>修改 index.js 檔案</li>



<li>到 views 資料夾裡面的 AddJobView.vue 檔案複製相關程式碼</li>



<li>修改 EditJobView.vue 檔案，把複製的程式碼貼上，然後做修改</li>
</ul>



<pre class="wp-block-code"><code>// src/views/EditJobView.vue
&lt;script setup&gt;
import router from '@/router';
import { reactive } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useToast } from 'vue-toastification';
import axios from 'axios';

const route = useRoute();
const router = useRouter();

const jobId = route.params.id;

const form = reactive({
  type: 'Full-Time',
  title: '',
  description: '',
  salary: '',
  location: '',
  company: {
    name: '',
    description: '',
    contactEmail: '',
    contactPhone: ''
  }
});

const state = reactive({
  job: {},
  isLoading: true
});

const toast = useToast();

const handleSubmit = async () =&gt; {
  const newJob = {
    title: form.title,
    type: form.type,
    location: form.location,
    description: form.description,
    salary: form.salary,
    company: {
      name: form.company.name,
      description: form.company.description,
      contactEmail: form.company.contactEmail,
      contactPhone: form.company.contactPhone,
    }
  };

  try {
    const response = await axios.post(`/api/jobs`, newJob);
    toast.success('Job Added Successfully');
    router.push(`/jobs/${response.data.id}`);
  } catch (error) {
    console.error('Error fetching job', error);
    toast.error('Job Was Not Added');
  }
};
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-green-50"&gt;
    &lt;div class="container m-auto max-w-2xl py-24"&gt;
      &lt;div
        class="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"
      &gt;
        &lt;form @submit.prevent="handleSubmit"&gt;
          &lt;h2 class="text-3xl text-center font-semibold mb-6"&gt;Edit Job&lt;/h2&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Job Type&lt;/label
            &gt;
            &lt;select
              v-model="form.type"
              id="type"
              name="type"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
              &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
              &lt;option value="Remote"&gt;Remote&lt;/option&gt;
              &lt;option value="Internship"&gt;Internship&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"
              &gt;Job Listing Name&lt;/label
            &gt;
            &lt;input
              v-model="form.title"
              type="text"
              id="name"
              name="name"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="eg. Beautiful Apartment In Miami"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.description"
              id="description"
              name="description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="Add any job duties, expectations, requirements, etc"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Salary&lt;/label
            &gt;
            &lt;select
              v-model="form.salary"
              id="salary"
              name="salary"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Under $50K"&gt;under $50K&lt;/option&gt;
              &lt;option value="$50K - $60K"&gt;$50 - $60K&lt;/option&gt;
              &lt;option value="$60K - $70K"&gt;$60 - $70K&lt;/option&gt;
              &lt;option value="$70K - $80K"&gt;$70 - $80K&lt;/option&gt;
              &lt;option value="$80K - $90K"&gt;$80 - $90K&lt;/option&gt;
              &lt;option value="$90K - $100K"&gt;$90 - $100K&lt;/option&gt;
              &lt;option value="$100K - $125K"&gt;$100 - $125K&lt;/option&gt;
              &lt;option value="$125K - $150K"&gt;$125 - $150K&lt;/option&gt;
              &lt;option value="$150K - $175K"&gt;$150 - $175K&lt;/option&gt;
              &lt;option value="$175K - $200K"&gt;$175 - $200K&lt;/option&gt;
              &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"&gt;
              Location
            &lt;/label&gt;
            &lt;input
              v-model="form.location"
              type="text"
              id="location"
              name="location"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="Company Location"
              required
            /&gt;
          &lt;/div&gt;

          &lt;h3 class="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="company" class="block text-gray-700 font-bold mb-2"
              &gt;Company Name&lt;/label
            &gt;
            &lt;input
              v-model="form.company.name"
              type="text"
              id="company"
              name="company"
              class="border rounded w-full py-2 px-3"
              placeholder="Company Name"
            /&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="company_description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Company Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.company.description"
              id="company_description"
              name="company_description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="What does your company do?"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_email"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Email&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactEmail"
              type="email"
              id="contact_email"
              name="contact_email"
              class="border rounded w-full py-2 px-3"
              placeholder="Email address for applicants"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_phone"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Phone&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactPhone"
              type="tel"
              id="contact_phone"
              name="contact_phone"
              class="border rounded w-full py-2 px-3"
              placeholder="Optional phone for applicants"
            /&gt;
          &lt;/div&gt;

          &lt;div&gt;
            &lt;button
              class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
              type="submit"
            &gt;
              Update Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<pre class="wp-block-code"><code>// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '@/views/HomeView.vue';
import JobsView from '@/views/JobsView.vue';
import NotFoundView from '@/views/NotFoundView.vue';
import JobView from '@/views/JobView.vue';
import AddJobView from '@/views/AddJobView.vue';
import EditJobView from '@/views/EditJobView.vue';

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: &#91;
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/jobs',
      name: 'jobs',
      component: JobsView
    },
    {
      path: '/jobs/:id',
      name: 'job',
      component: JobView
    },
    {
      path: '/jobs/add',
      name: 'add-job',
      component: AddJobView,
    },
    {
      path: '/jobs/edit/:id',
      name: 'edit-job',
      component: EditJobView,
    },
    {
      path: '/:catchAll(.*)',
      name: 'not-found',
      component: NotFoundView
    },
  ]
});

export default router;
</code></pre>



<h3 class="wp-block-heading">Fetch Job To Edit</h3>



<ul class="wp-block-list">
<li>修改 EditJobView.vue 檔案</li>
</ul>



<pre class="wp-block-code"><code>// src/views/EditJobView.vue
&lt;script setup&gt;
import router from '@/router';
import { reactive, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useToast } from 'vue-toastification';
import axios from 'axios';

const route = useRoute();

const jobId = route.params.id;

const form = reactive({
  type: 'Full-Time',
  title: '',
  description: '',
  salary: '',
  location: '',
  company: {
    name: '',
    description: '',
    contactEmail: '',
    contactPhone: ''
  }
});

const state = reactive({
  job: {},
  isLoading: true
});

const toast = useToast();

const handleSubmit = async () =&gt; {
  const newJob = {
    title: form.title,
    type: form.type,
    location: form.location,
    description: form.description,
    salary: form.salary,
    company: {
      name: form.company.name,
      description: form.company.description,
      contactEmail: form.company.contactEmail,
      contactPhone: form.company.contactPhone,
    }
  };

  try {
    const response = await axios.post(`/api/jobs`, newJob);
    toast.success('Job Added Successfully');
    router.push(`/jobs/${response.data.id}`);
  } catch (error) {
    console.error('Error fetching job', error);
    toast.error('Job Was Not Added');
  }
};

onMounted(async () =&gt; {
  try {
    const response = await axios.get(`/api/jobs/${jobId}`);
    state.job = response.data;
    // Populate inputs
    form.type = state.job.type;
    form.title = state.job.title;
    form.description = state.job.description;
    form.salary = state.job.salary;
    form.location = state.job.location;
    form.company.name = state.job.company.name;
    form.company.description = state.job.company.description;
    form.company.contactEmail = state.job.company.contactEmail;
    form.company.contactPhone = state.job.company.contactPhone;
  } catch (error) {
    console.error('Error fetching job', error);
  } finally {
    state.isLoading = false;
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-green-50"&gt;
    &lt;div class="container m-auto max-w-2xl py-24"&gt;
      &lt;div
        class="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"
      &gt;
        &lt;form @submit.prevent="handleSubmit"&gt;
          &lt;h2 class="text-3xl text-center font-semibold mb-6"&gt;Edit Job&lt;/h2&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Job Type&lt;/label
            &gt;
            &lt;select
              v-model="form.type"
              id="type"
              name="type"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
              &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
              &lt;option value="Remote"&gt;Remote&lt;/option&gt;
              &lt;option value="Internship"&gt;Internship&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"
              &gt;Job Listing Name&lt;/label
            &gt;
            &lt;input
              v-model="form.title"
              type="text"
              id="name"
              name="name"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="eg. Beautiful Apartment In Miami"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.description"
              id="description"
              name="description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="Add any job duties, expectations, requirements, etc"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Salary&lt;/label
            &gt;
            &lt;select
              v-model="form.salary"
              id="salary"
              name="salary"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Under $50K"&gt;under $50K&lt;/option&gt;
              &lt;option value="$50K - $60K"&gt;$50 - $60K&lt;/option&gt;
              &lt;option value="$60K - $70K"&gt;$60 - $70K&lt;/option&gt;
              &lt;option value="$70K - $80K"&gt;$70 - $80K&lt;/option&gt;
              &lt;option value="$80K - $90K"&gt;$80 - $90K&lt;/option&gt;
              &lt;option value="$90K - $100K"&gt;$90 - $100K&lt;/option&gt;
              &lt;option value="$100K - $125K"&gt;$100 - $125K&lt;/option&gt;
              &lt;option value="$125K - $150K"&gt;$125 - $150K&lt;/option&gt;
              &lt;option value="$150K - $175K"&gt;$150 - $175K&lt;/option&gt;
              &lt;option value="$175K - $200K"&gt;$175 - $200K&lt;/option&gt;
              &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"&gt;
              Location
            &lt;/label&gt;
            &lt;input
              v-model="form.location"
              type="text"
              id="location"
              name="location"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="Company Location"
              required
            /&gt;
          &lt;/div&gt;

          &lt;h3 class="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="company" class="block text-gray-700 font-bold mb-2"
              &gt;Company Name&lt;/label
            &gt;
            &lt;input
              v-model="form.company.name"
              type="text"
              id="company"
              name="company"
              class="border rounded w-full py-2 px-3"
              placeholder="Company Name"
            /&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="company_description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Company Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.company.description"
              id="company_description"
              name="company_description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="What does your company do?"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_email"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Email&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactEmail"
              type="email"
              id="contact_email"
              name="contact_email"
              class="border rounded w-full py-2 px-3"
              placeholder="Email address for applicants"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_phone"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Phone&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactPhone"
              type="tel"
              id="contact_phone"
              name="contact_phone"
              class="border rounded w-full py-2 px-3"
              placeholder="Optional phone for applicants"
            /&gt;
          &lt;/div&gt;

          &lt;div&gt;
            &lt;button
              class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
              type="submit"
            &gt;
              Update Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Update Job</h3>



<ul class="wp-block-list">
<li>修改 EditJobView.vue 檔案</li>



<li>測試更新資料功能是否正常運行</li>
</ul>



<pre class="wp-block-code"><code>// src/views/EditJobView.vue
&lt;script setup&gt;
import router from '@/router';
import { reactive, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { useToast } from 'vue-toastification';
import axios from 'axios';

const route = useRoute();

const jobId = route.params.id;

const form = reactive({
  type: 'Full-Time',
  title: '',
  description: '',
  salary: '',
  location: '',
  company: {
    name: '',
    description: '',
    contactEmail: '',
    contactPhone: ''
  }
});

const state = reactive({
  job: {},
  isLoading: true
});

const toast = useToast();

const handleSubmit = async () =&gt; {
  const updatedJob = {
    title: form.title,
    type: form.type,
    location: form.location,
    description: form.description,
    salary: form.salary,
    company: {
      name: form.company.name,
      description: form.company.description,
      contactEmail: form.company.contactEmail,
      contactPhone: form.company.contactPhone,
    }
  };

  try {
    const response = await axios.put(`/api/jobs/${jobId}`, updatedJob);
    toast.success('Job Updated Successfully');
    router.push(`/jobs/${response.data.id}`);
  } catch (error) {
    console.error('Error fetching job', error);
    toast.error('Job Was Not Updated');
  }
};

onMounted(async () =&gt; {
  try {
    const response = await axios.get(`/api/jobs/${jobId}`);
    state.job = response.data;
    // Populate inputs
    form.type = state.job.type;
    form.title = state.job.title;
    form.description = state.job.description;
    form.salary = state.job.salary;
    form.location = state.job.location;
    form.company.name = state.job.company.name;
    form.company.description = state.job.company.description;
    form.company.contactEmail = state.job.company.contactEmail;
    form.company.contactPhone = state.job.company.contactPhone;
  } catch (error) {
    console.error('Error fetching job', error);
  } finally {
    state.isLoading = false;
  }
});
&lt;/script&gt;

&lt;template&gt;
  &lt;section class="bg-green-50"&gt;
    &lt;div class="container m-auto max-w-2xl py-24"&gt;
      &lt;div
        class="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"
      &gt;
        &lt;form @submit.prevent="handleSubmit"&gt;
          &lt;h2 class="text-3xl text-center font-semibold mb-6"&gt;Edit Job&lt;/h2&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Job Type&lt;/label
            &gt;
            &lt;select
              v-model="form.type"
              id="type"
              name="type"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
              &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
              &lt;option value="Remote"&gt;Remote&lt;/option&gt;
              &lt;option value="Internship"&gt;Internship&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"
              &gt;Job Listing Name&lt;/label
            &gt;
            &lt;input
              v-model="form.title"
              type="text"
              id="name"
              name="name"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="eg. Beautiful Apartment In Miami"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.description"
              id="description"
              name="description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="Add any job duties, expectations, requirements, etc"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="type" class="block text-gray-700 font-bold mb-2"
              &gt;Salary&lt;/label
            &gt;
            &lt;select
              v-model="form.salary"
              id="salary"
              name="salary"
              class="border rounded w-full py-2 px-3"
              required
            &gt;
              &lt;option value="Under $50K"&gt;under $50K&lt;/option&gt;
              &lt;option value="$50K - $60K"&gt;$50 - $60K&lt;/option&gt;
              &lt;option value="$60K - $70K"&gt;$60 - $70K&lt;/option&gt;
              &lt;option value="$70K - $80K"&gt;$70 - $80K&lt;/option&gt;
              &lt;option value="$80K - $90K"&gt;$80 - $90K&lt;/option&gt;
              &lt;option value="$90K - $100K"&gt;$90 - $100K&lt;/option&gt;
              &lt;option value="$100K - $125K"&gt;$100 - $125K&lt;/option&gt;
              &lt;option value="$125K - $150K"&gt;$125 - $150K&lt;/option&gt;
              &lt;option value="$150K - $175K"&gt;$150 - $175K&lt;/option&gt;
              &lt;option value="$175K - $200K"&gt;$175 - $200K&lt;/option&gt;
              &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label class="block text-gray-700 font-bold mb-2"&gt;
              Location
            &lt;/label&gt;
            &lt;input
              v-model="form.location"
              type="text"
              id="location"
              name="location"
              class="border rounded w-full py-2 px-3 mb-2"
              placeholder="Company Location"
              required
            /&gt;
          &lt;/div&gt;

          &lt;h3 class="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

          &lt;div class="mb-4"&gt;
            &lt;label for="company" class="block text-gray-700 font-bold mb-2"
              &gt;Company Name&lt;/label
            &gt;
            &lt;input
              v-model="form.company.name"
              type="text"
              id="company"
              name="company"
              class="border rounded w-full py-2 px-3"
              placeholder="Company Name"
            /&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="company_description"
              class="block text-gray-700 font-bold mb-2"
              &gt;Company Description&lt;/label
            &gt;
            &lt;textarea
              v-model="form.company.description"
              id="company_description"
              name="company_description"
              class="border rounded w-full py-2 px-3"
              rows="4"
              placeholder="What does your company do?"
            &gt;&lt;/textarea&gt;
          &lt;/div&gt;

          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_email"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Email&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactEmail"
              type="email"
              id="contact_email"
              name="contact_email"
              class="border rounded w-full py-2 px-3"
              placeholder="Email address for applicants"
              required
            /&gt;
          &lt;/div&gt;
          &lt;div class="mb-4"&gt;
            &lt;label
              for="contact_phone"
              class="block text-gray-700 font-bold mb-2"
              &gt;Contact Phone&lt;/label
            &gt;
            &lt;input
              v-model="form.company.contactPhone"
              type="tel"
              id="contact_phone"
              name="contact_phone"
              class="border rounded w-full py-2 px-3"
              placeholder="Optional phone for applicants"
            /&gt;
          &lt;/div&gt;

          &lt;div&gt;
            &lt;button
              class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
              type="submit"
            &gt;
              Update Job
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/template&gt;
</code></pre>



<h3 class="wp-block-heading">Netlify Deployment</h3>



<ul class="wp-block-list">
<li>講解如何發布程式碼到 Netlify 網站</li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Git 和 GitHub 零基礎快速上手</title>
		<link>/wordpress_blog/git-github-crash/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Wed, 19 Jun 2024 01:21:43 +0000</pubDate>
				<category><![CDATA[Youtube]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=858</guid>

					<description><![CDATA[Learning From Youtube Channel: P [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Learning From Youtube Channel: PAPAYA 電腦教室<br>Video: <a href="https://www.youtube.com/watch?v=FKXRiAiQFiY" target="_blank" rel="noreferrer noopener">程式與網頁開發者必備技能! Git 和 GitHub 零基礎快速上手，輕鬆掌握版本控制的要訣!</a><br>Thank you.</p>



<p>00:00 – 開場白 &amp; Git 安裝<br>02:03 – Git 基本設定與初始化<br>03:36 – Git 檔案狀態<br>06:46 – 檢視提交紀錄與檔案還原<br>10:06 – 忽略檔案清單<br>10:34 – GitHub 註冊 &amp; 同步儲存庫<br>12:11 – 加入協作者至專案<br>13:36 – 建立分支<br>14:35 – 發起合併請求 &amp; 合併分支</p>



<h2 class="wp-block-heading">開場白 &amp; Git 安裝</h2>



<ul class="wp-block-list">
<li>Git 是協助檔案管理版本工具</li>



<li>安裝 VSCode 免費軟體</li>



<li>建立 My Project 資料夾</li>



<li>安裝 Git</li>



<li>打開終端機查詢版本，git –version</li>
</ul>



<h2 class="wp-block-heading">Git 基本設定與初始化</h2>



<ul class="wp-block-list">
<li>設定姓名和電子郵件地址<br>git config –global user.name “geehsu”<br>git config –global user.email “geehsu@gmaill.com”</li>



<li>Git 初始化，git init</li>



<li>清除終端機畫面，clear</li>



<li>建立 Price.md 檔案</li>



<li>建立 Specs.md 檔案</li>



<li>建立 Systems.md 檔案</li>
</ul>



<pre class="wp-block-code"><code>// Price.md
# 價格分析
價格分析是一種經濟學和市場分析的方法，用來研究產品或服務的價格如何影響消費者行為和市場動態。這類分析通常涉及以下幾個方面：

需求與供應: 分析價格如何影響產品或服務的需求和供應量。通常來說，價格上升可能會減少需求，但同時也可能刺激供應。

市場結構: 調查市場的競爭程度，包括壟斷、寡頭壟斷、完全競爭等形式。這會影響價格的設定方式和市場參與者的行為。

價格彈性: 衡量價格變動對需求量變動的敏感程度。價格彈性高意味著小幅價格變動可能會對需求產生顯著影響，而價格彈性低則相反。

競爭策略: 分析企業如何透過價格來制定競爭策略，例如價格領導、價格競爭、價格歧視等，以達到市場份額和利潤最大化的目標。

消費者行為: 研究價格如何影響消費者的購買決策和行為模式，包括價格感知、價格預期和購買意願等。

總結來說，價格分析不僅限於價格水平本身，還涉及價格與市場環境、消費者行為和企業策略之間的複雜互動。這些分析能夠幫助企業和政策制定者更好地理解市場運作，並做出相應的策略和政策決策。</code></pre>



<pre class="wp-block-code"><code>// Specs.md
# 技術規格
技術分析是一種用來預測金融市場走勢的方法，主要基於過去市場行為的統計數據和圖表模式來做出預測。這種分析方法主要包括以下幾個要素：

圖表模式分析: 技術分析師會觀察股票或其他金融資產的歷史價格走勢，並尋找特定的圖表模式，如頭肩頂、雙底形態、三角形等。這些模式被認為能夠預示未來的價格走勢。

技術指標: 技術分析使用各種技術指標來量化市場的價格和成交量數據，並從中提取有用的信號。常見的技術指標包括移動平均線、相對強弱指標（RSI）、MACD（移動平均匯聚/分歧指標）等。

市場心理學: 技術分析也關注市場參與者的情緒和心理狀態對市場的影響。例如，過度買入或過度賣出情況可能會影響市場走勢。

支撐與阻力: 技術分析師會觀察市場中的支撐位（價格下跌後停止下跌的水平）和阻力位（價格上漲後停止上漲的水平），這些水平通常被認為具有重要的價格反彈或反轉信號。

技術分析的目標是通過以上方法來預測市場走勢，進而制定交易策略或投資決策。然而，技術分析也有其局限性，例如忽略基本面因素、過度依賴歷史模式等，因此通常會與基本面分析結合使用，以獲得更全面的市場洞察。</code></pre>



<pre class="wp-block-code"><code>// Systems.md
# 軟體兼容性
軟體兼容性是指一個軟體應用程式或系統能夠在不同的硬體平台、作業系統或環境下正常運行和互動的能力。這在現代軟體開發中至關重要，特別是考慮到各種不同的使用情境和技術設定。

主要考慮因素：
硬體平台: 軟體必須能夠在不同的硬體設備上運行，包括桌面電腦、筆記型電腦、平板電腦、智能手機等。這些設備可能擁有不同的處理器架構、記憶體容量和圖形處理能力，軟體需要能夠有效利用這些硬體資源。

作業系統: 軟體必須與目標使用的作業系統兼容，如Windows、macOS、Linux、Android、iOS等。不同的作業系統可能有不同的應用程式介面（API）和系統限制，軟體需要根據這些特性進行開發和測試。

依賴的庫和框架: 許多軟體開發依賴於第三方庫和框架，這些庫和框架也需要與目標平台和環境兼容。開發者需要確保這些依賴項目的版本和更新與目標系統相容。

網路環境: 如果軟體需要在網際網路環境下運行，則需要考慮不同的網路設定和帶寬要求，以確保軟體在不同的網路速度和穩定性條件下仍能正常運作。

語言和協議: 如果軟體與其他系統或服務進行通訊，需要確保使用的通訊語言和協議在不同系統之間的相容性，如HTTP、REST API、SOAP等。

管理軟體兼容性的方法：
測試和驗證: 在開發過程中，進行系統化的測試，包括單元測試、整合測試和系統測試，以確保軟體在各種情況下的正常運行。

版本控制: 管理和追蹤依賴項目的版本，確保軟體與其依賴的庫和框架相容。

文檔和支援: 提供清晰的文檔和支援資源，幫助用戶在不同環境中安裝、配置和使用軟體。

總結來說，有效的軟體兼容性管理是軟體開發過程中的重要一環，它能夠確保軟體在不同平台和環境中的穩定性和功能性，提升用戶的滿意度和整體產品的可靠性。</code></pre>



<h2 class="wp-block-heading">Git 檔案狀態</h2>



<ul class="wp-block-list">
<li>檢查當前目錄中每個檔案的狀態，git status</li>



<li>檔案狀態:<br>Untracked 未追蹤<br>Tracked 已追蹤<br>Staged 已暫存<br>Committed 已提交</li>



<li>將檔案狀態未追蹤轉換成已追蹤<br>加到暫存區<br>git add Price.md</li>



<li>檢查檔案狀態，git status</li>



<li>把其他兩個檔案也從未追蹤轉換成已追蹤<br>加到暫存區<br>git add Specs.md<br>git add Systems.md</li>



<li>檢查檔案狀態，git status</li>



<li>提交叫做 git commit<br>git commit -m “建立還原點”</li>



<li>檢查檔案狀態，git status</li>



<li>修改檔案內容</li>



<li>再次加到暫存區<br>git add *.md<br>將所有變更都加入到暫存區，git add .</li>



<li>提交 git commit<br>git commit -m “加上新的內容”</li>



<li>修改檔案內容</li>



<li>再次加到暫存區<br>git add .</li>



<li>提交 git commit<br>git commit -m “修改加上新的內容”</li>
</ul>



<pre class="wp-block-code"><code>// Price.md
# 價格分析
價格分析是一種經濟學和市場分析的方法，用來研究產品或服務的價格如何影響消費者行為和市場動態。這類分析通常涉及以下幾個方面：

需求與供應: 分析價格如何影響產品或服務的需求和供應量。通常來說，價格上升可能會減少需求，但同時也可能刺激供應。

市場結構: 調查市場的競爭程度，包括壟斷、寡頭壟斷、完全競爭等形式。這會影響價格的設定方式和市場參與者的行為。

價格彈性: 衡量價格變動對需求量變動的敏感程度。價格彈性高意味著小幅價格變動可能會對需求產生顯著影響，而價格彈性低則相反。

競爭策略: 分析企業如何透過價格來制定競爭策略，例如價格領導、價格競爭、價格歧視等，以達到市場份額和利潤最大化的目標。

消費者行為: 研究價格如何影響消費者的購買決策和行為模式，包括價格感知、價格預期和購買意願等。

總結來說，價格分析不僅限於價格水平本身，還涉及價格與市場環境、消費者行為和企業策略之間的複雜互動。這些分析能夠幫助企業和政策制定者更好地理解市場運作，並做出相應的策略和政策決策。

謝謝你，ChatGPT</code></pre>



<pre class="wp-block-code"><code>// Specs.md
# 技術規格
技術分析是一種用來預測金融市場走勢的方法，主要基於過去市場行為的統計數據和圖表模式來做出預測。這種分析方法主要包括以下幾個要素：

圖表模式分析: 技術分析師會觀察股票或其他金融資產的歷史價格走勢，並尋找特定的圖表模式，如頭肩頂、雙底形態、三角形等。這些模式被認為能夠預示未來的價格走勢。

技術指標: 技術分析使用各種技術指標來量化市場的價格和成交量數據，並從中提取有用的信號。常見的技術指標包括移動平均線、相對強弱指標（RSI）、MACD（移動平均匯聚/分歧指標）等。

市場心理學: 技術分析也關注市場參與者的情緒和心理狀態對市場的影響。例如，過度買入或過度賣出情況可能會影響市場走勢。

支撐與阻力: 技術分析師會觀察市場中的支撐位（價格下跌後停止下跌的水平）和阻力位（價格上漲後停止上漲的水平），這些水平通常被認為具有重要的價格反彈或反轉信號。

技術分析的目標是通過以上方法來預測市場走勢，進而制定交易策略或投資決策。然而，技術分析也有其局限性，例如忽略基本面因素、過度依賴歷史模式等，因此通常會與基本面分析結合使用，以獲得更全面的市場洞察。

謝謝你，ChatGPT</code></pre>



<pre class="wp-block-code"><code>// Systems.md
# 軟體兼容性
軟體兼容性是指一個軟體應用程式或系統能夠在不同的硬體平台、作業系統或環境下正常運行和互動的能力。這在現代軟體開發中至關重要，特別是考慮到各種不同的使用情境和技術設定。

主要考慮因素：
硬體平台: 軟體必須能夠在不同的硬體設備上運行，包括桌面電腦、筆記型電腦、平板電腦、智能手機等。這些設備可能擁有不同的處理器架構、記憶體容量和圖形處理能力，軟體需要能夠有效利用這些硬體資源。

作業系統: 軟體必須與目標使用的作業系統兼容，如Windows、macOS、Linux、Android、iOS等。不同的作業系統可能有不同的應用程式介面（API）和系統限制，軟體需要根據這些特性進行開發和測試。

依賴的庫和框架: 許多軟體開發依賴於第三方庫和框架，這些庫和框架也需要與目標平台和環境兼容。開發者需要確保這些依賴項目的版本和更新與目標系統相容。

網路環境: 如果軟體需要在網際網路環境下運行，則需要考慮不同的網路設定和帶寬要求，以確保軟體在不同的網路速度和穩定性條件下仍能正常運作。

語言和協議: 如果軟體與其他系統或服務進行通訊，需要確保使用的通訊語言和協議在不同系統之間的相容性，如HTTP、REST API、SOAP等。

管理軟體兼容性的方法：
測試和驗證: 在開發過程中，進行系統化的測試，包括單元測試、整合測試和系統測試，以確保軟體在各種情況下的正常運行。

版本控制: 管理和追蹤依賴項目的版本，確保軟體與其依賴的庫和框架相容。

文檔和支援: 提供清晰的文檔和支援資源，幫助用戶在不同環境中安裝、配置和使用軟體。

總結來說，有效的軟體兼容性管理是軟體開發過程中的重要一環，它能夠確保軟體在不同平台和環境中的穩定性和功能性，提升用戶的滿意度和整體產品的可靠性。

謝謝你，ChatGPT</code></pre>



<h2 class="wp-block-heading">檢視提交紀錄與檔案還原</h2>



<ul class="wp-block-list">
<li>使用終端機輸入指令，git log<br>會列出先前的提交歷史</li>



<li>按下 q 就可以退出 log 的檢視模式</li>



<li>git log 指令的簡化版，git log –oneline<br>適合快速瀏覽過去的提交紀錄</li>



<li>HEAD、master 的說明<br>master 想成是遊戲中的主線劇情<br>HEAD 則是指向遊戲目前的進度，通常為遊戲最新的存檔點</li>



<li>將文件恢復成較早的狀態<br>還原前先比較新、舊版本的內容差異，git diff<br>git diff f6d9011 — Price.md</li>



<li>將檔案還原到這個版本，git checkout f6d9011 — Price.md</li>



<li>注意這個變動同樣需要進行提交<br>git commit -m “改為還原點內容”</li>



<li>檢視提交紀錄，使用 git log –oneline<br>基本上完整保留先前的歷史紀錄</li>



<li>有時也許想將全部檔案還原到某個時間點，並且捨棄掉之後的存檔紀錄，git reset –hard 08440ab<br>注意這個操作是不可逆的，有需要先做備份</li>



<li>建立 Marketing.md 檔案</li>



<li>修改 Marketing.md 檔案</li>



<li>同樣用 git add 將檔案加入到暫存區</li>



<li>同樣用 git commit 將檔案提交</li>



<li>刪除 Marketing.md 檔案</li>



<li>檢查當前目錄的狀態，git status</li>



<li>再次用 git add 加入到暫存區</li>



<li>檢查當前目錄的狀態，git status</li>



<li>觀念: Git 追蹤的是檔案的<strong>變化</strong>，而非檔案本身</li>



<li>再次用 git commit 將檔案提交</li>



<li>檢查當前目錄的狀態，git status</li>



<li>檢視提交紀錄(簡化版)，git log –oneline</li>
</ul>



<h2 class="wp-block-heading">忽略檔案清單</h2>



<ul class="wp-block-list">
<li>建立 .gitignore 檔案</li>



<li>修改 .gitignore 檔案<br>把要忽略的檔名或副檔案寫在裡面<br>git 在提交時就能避免無關的檔案混入到版本紀錄，保持專案整潔，同時避免私人資料不小心被提交到 Git 儲存庫</li>
</ul>



<pre class="wp-block-code"><code>// .gitignore
*.jpg
</code></pre>



<h2 class="wp-block-heading">GitHub 註冊 &amp; 同步儲存庫</h2>



<ul class="wp-block-list">
<li>git 版本控制系統<br>GitHub 雲端協作平台</li>



<li>在 GitHub 官網註冊免費帳號</li>



<li>在主頁面建立儲存庫</li>



<li>Respository name<br>Description<br>Public、Private<br>Create</li>



<li>GitHub 有提供現成的指令協作把本地的檔案推送到雲端</li>



<li>第一行是用來連結本地和遠端的儲存庫<br>git remote add origin https://github.com/papayaclass/VRFusion.git<br>remote: 遠端<br>add: 新增<br>origin: 遠端儲存庫名稱<br>https://github.com/papayaclass/VRFusion.git: 遠端儲存庫網址</li>



<li>第二行是把原來的主線分支從 master 改為較中性的詞語 main<br>git branch -M main<br>branch: 分支管理<br>-M: 重新命名<br>main: 新的分支名</li>



<li>第三行則是把本地位於 main 分支的資料推送到雲端<br>git push -u origin main<br>push: 推送<br>-u: 建立關聯<br>origin: 遠端名稱<br>main: 本地名稱</li>



<li>在 GitHub 打開 Code 頁面就能看到所有已上船的檔案<br>在 Commits 頁面看到所有提交的版本紀錄</li>



<li>增加 README.md 檔案</li>



<li>git add 檔案加到暫存區</li>



<li>git commit 檔案提交</li>



<li>git push 檔案推送</li>
</ul>



<h2 class="wp-block-heading">加入協作者至專案</h2>



<ul class="wp-block-list">
<li>打開 Collaborators (合作者) 的頁面</li>



<li>點擊新增成員，輸入帳號名稱或電子郵件</li>



<li>成為協作者的人，首先要將儲存庫複製到本地端電腦<br>git clone 儲存庫網址</li>



<li>打開並移動到該專案</li>



<li>本人修改專案內容後做推送</li>



<li>協作者此時只要輸入 git pull 指令就能將最新雲端版本紀錄拉回到本地端，團隊成員以維持在同步的狀態</li>
</ul>



<h2 class="wp-block-heading">建立分支</h2>



<ul class="wp-block-list">
<li>以遊戲儲存進度做舉例<br>要求對方儲存自己的遊戲進度<br>git checkout -b branch2<br>checkout: 切換分支<br>-b: 建立並切換至新分支<br>branch2: 新分支名稱</li>



<li>協作者在自己本地端修改文件<br>同樣可以加到暫存區、提交、推送<br>git push origin branch2<br>push: 推送<br>origin: 遠端名稱<br>branch2: 本地名稱</li>
</ul>



<h2 class="wp-block-heading">發起合併請求 &amp; 合併分支</h2>



<ul class="wp-block-list">
<li>回到 GitHub 的專案頁面會發現分支數量已變成兩個</li>



<li>協作者可以發起 Pull Request (合併請求)，讓團隊的其他成員知道他已完成了一項編輯或修復了一個問題，並請求將這些變更合併到主線分支</li>



<li>在自己的帳號中也能看到協作者發起的 Pull Request，我們可以把這個項目打開來檢視協作者在他的分支內做了哪些編輯，審核的過程中如果發現一些需要改進的地方，可以在評論區內寫下我們的建議</li>



<li>或是從右下角的選單中選擇 Request Change 正式請求協作者進行相應的修改</li>



<li>如果在審核時覺得協作者所做的變更沒有問題，我們就可以回到原頁面點擊 Merge Pull Request 的按鈕，把協作者在他的分支所做的變更合併到主線分支，合併完成後協作者的分支就完成了他的任務，我們就可以放心地把新分支刪除掉</li>



<li>至於我們在本地端的檔案則記得要執行 git pull 的指令從 GitHub 下載最新的變更，使本地和遠端的儲存庫能維持同步</li>



<li>透過這樣的工作流程，團隊成員就能共同協作來完成一個富有挑戰性的專案</li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Complete User Registration, Login &#038; Logout with React JWT, Bcrypt Password &#124; Nodejs &#124; Mern Stack</title>
		<link>/wordpress_blog/complete-user/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Wed, 12 Jun 2024 04:09:26 +0000</pubDate>
				<category><![CDATA[Youtube]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=855</guid>

					<description><![CDATA[Learning From Youtube Channel:&#038;n [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Learning From Youtube Channel:&nbsp;<a href="https://www.youtube.com/@CodeInfoofficial/videos" target="_blank" rel="noreferrer noopener">Code Info</a><br>Video:&nbsp;<a href="https://www.youtube.com/watch?v=GDs1eo36B-k" target="_blank" rel="noreferrer noopener">Complete User Registration, Login &amp; Logout with React JWT, Bcrypt Password | Nodejs | Mern Stack</a><br>Thank you.</p>



<h2 class="wp-block-heading">專案開始和安裝</h2>



<h3 class="wp-block-heading">Server</h3>



<ul class="wp-block-list">
<li>建立 authentication-app 資料夾</li>



<li>在 authentication-app 資料夾裡面建立 client 資料夾</li>



<li>在 authentication-app 資料夾裡面建立 server 資料夾</li>



<li>移動到 server 資料夾 – cd server</li>



<li>npm 初始化建立 package.json 檔案<br>description: authentication app<br>auther: your name</li>



<li>安裝 express 套件 – npm i express</li>



<li>在 server 資料夾裡面建立 index.js 檔案</li>



<li>修改 index.js 檔案</li>



<li>安裝套件<br>npm i mongoose cors jsonwebtoken</li>



<li>修改 index.js 檔案</li>



<li>執行終端機 – nodemon index.js</li>



<li>開啟 MongoDB Compass > connection</li>



<li>在 server 資料夾裡面建立 models 資料夾</li>



<li>在 models 資料夾裡面建立 userModel.js 檔案</li>



<li>修改 userModel.js 檔案</li>



<li>在 server 資料夾裡面建立 controllers 資料夾</li>



<li>在 controllers 資料夾裡面建立 authController.js 檔案</li>



<li>修改 index.js 檔案，GLOBAL ERROR HANDLER</li>



<li>在 server 資料夾裡面建立 utils 資料夾</li>



<li>在 utils 資料夾裡面建立 appError.js 檔案</li>



<li>修改 appError.js 檔案</li>



<li>修改 authController.js 檔案</li>



<li>安裝 bcryptjs 套件 – npm i bcryptjs</li>



<li>在 server 資料夾裡面建立 routes 資料夾</li>



<li>在 routes 資料夾裡面建立 authRoute.js 檔案</li>



<li>修改 index.js 檔案，新增 authRouter 變數、修改 ROUTE</li>



<li>使用 API 測試工具<br>http://localhost:3000/api/auth/signup，POST 方法<br>Body > JSON</li>



<li>修改 authController.js 檔案，LOGGING USER</li>



<li>使用 API 測試工具<br>http://localhost:3000/api/auth/login，POST 方法<br>Body > JSON</li>



<li>修改 authController.js 檔案，trycatch</li>



<li>使用 API 測試工具<br>http://localhost:3000/api/auth/login，POST 方法</li>



<li>使用 API 測試工具<br>http://localhost:3000/api/auth/signup，POST 方法<br>Body > JSON</li>



<li>修改 authController.js 檔案</li>
</ul>



<pre class="wp-block-code"><code>// server/index.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const authRouter = require('./routes/authRoute');
const app = express();

// 1. MIDDLEWARES
app.use(cors());
app.use(express.json());

// 2. ROUTE
app.use('/api/auth', authRouter);

// 3. MONGO DB CONNECTION
mongoose.connect('mongodb://127.0.0.1:27017/authentication')
  .then(() =&gt; console.log('Connected to MongoDB!'))
  .catch((error) =&gt; console.error('Failed to connect to MongoDB:', error));

// 4. GLOBAL ERROR HANDLER
app.use((err, req, res, next) =&gt; {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || 'error';

  res.status(err.statusCode).json({
    status: err.status,
    message: err.message,
  });
});

// 5. SERVER
const PORT = 3000;
app.listen(PORT, () =&gt; {
  console.log(`App running on ${PORT}`);
});
</code></pre>



<pre class="wp-block-code"><code>// models/userModel.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
  },
  email: {
    type: String,
    unique: true,
    required: true,
  },
  role: {
    type: String,
    default: 'user',
  },
  password: {
    type: String,
    required: true,
  },
});

const User = mongoose.model('User', userSchema);

module.exports = User;
</code></pre>



<pre class="wp-block-code"><code>// server/utils/appError.js
class createError extends Error{
  constructor(message, statusCode) {
    super(message);

    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';

    Error.captureStackTrace(this, this.constructor);
  }
}

module.exports = createError;
</code></pre>



<pre class="wp-block-code"><code>// server/controllers/authController.js
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const User = require('../models/userModel');
const createError = require('../utils/appError');

// REGISTER USER
exports.signup = async (req, res, next) =&gt; {
  try {
    const user = await User.findOne({ email: req.body.email });

    if (user) {
      return next(new createError('User already exists!', 400));
    }
    const hashedPassword = await bcrypt.hash(req.body.password, 12);

    const newUser = await User.create({
      ...req.body,
      password: hashedPassword,
    });

    // Assign JWT ( json web token)
    const token = jwt.sign({_id: newUser._id}, "secretkey123", {
      expiresIn: '90d',
    });

    res.status(201).json({
      status: 'success',
      message: 'User registered successfully',
      token,
      user: {
        _id: newUser._id,
        name: newUser.name,
        email: newUser.email,
        role: newUser.role,
      },
    });
  } catch (error) {
    next(error);
  }
};

// LOGGING USER
exports.login = async (req, res, next) =&gt; {
  try {
    const { email, password } = req.body;

    const user = await User.findOne({ email });

    if (!user) return next(new createError('User not found!', 404));

    const isPasswordValid = await bcrypt.compare(password, user.password);

    if (!isPasswordValid) {
      return next(new createError('Invalid email or password', 401));
    }

    const token = jwt.sign({_id: user._id}, "secretkey123", {
      expiresIn: '90d',
    });

    res.status(200).json({
      status: 'success',
      token,
      message: 'Logged in successfully',
      user: {
        _id: user._id,
        name: user.name,
        email: user.email,
        role: user.role,
      },
    })
  } catch (error) {
    next(error);
  }
};
</code></pre>



<pre class="wp-block-code"><code>// server/routes/authRoute.js
const express = require('express');
const authController = require('../controllers/authController');

const router = express.Router();

router.post('/signup', authController.signup);
router.post('/login', authController.login);

module.exports = router;
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; JSON - 1
{
  "name": "test user",
  "email": "test1@gmail.com",
  "password": "123",
}
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; JSON - 2
{
  "name": "test user",
  "email": "test2@gmail.com",
  "password": "test@123"
}
</code></pre>



<pre class="wp-block-code"><code>// Body &gt; JSON - 3
{
  "name": "test user 3",
  "email": "test3@gmail.com",
  "password": "123"
}
</code></pre>



<h3 class="wp-block-heading">Client</h3>



<ul class="wp-block-list">
<li>分別開兩個終端機<br>1個是 server、1個是 client<br>移動到 server – cd server<br>移動到 cleint – cd client</li>



<li>使用 Vite 快速建立 React<br>npm create vite@latest .</li>



<li>Select a framework: Reactt<br>Select a variant: JavaScript<br>npm install<br>npm run dev</li>



<li>搜尋 <a href="https://ant.design/" target="_blank" rel="noreferrer noopener">Ant Design</a></li>



<li>使用 npm 安裝 antd、react-router-dom 套件<br>npm i antd react-router-dom</li>



<li>修改 src/App.jsx 檔案<br>使用 rafce 片段快速建立程式碼</li>



<li>在 src 資料夾裡面建立 Auth 資料夾</li>



<li>在 Auth 資料夾裡面建立 Register.jsx 檔案</li>



<li>使用 rafce 片段快速建立程式碼<br>修改 Auth/Register.jsx 檔案</li>



<li>在 Auth 資料夾裡面建立 Login.jsx 檔案</li>



<li>使用 rafce 片段快速建立程式碼<br>修改 Auth/Login.jsx 檔案</li>



<li>在 src 資料夾裡面建立 pages 資料夾</li>



<li>在 pages 資料夾裡面建立 Dashboard.jsx 檔案</li>



<li>使用 rafce 片段快速建立程式碼<br>修改 pages/Dashboard.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>



<li>修改 App.css 檔案</li>



<li>修改 Register.jsx 檔案，修改 &lt;Card>、&lt;Flex></li>



<li>修改 App.css 檔案， &lt;form></li>



<li>修改 Register.jsx 檔案，img 部分</li>



<li>下載 register.png 檔案，尋找替代的圖片</li>



<li>修改 App.css 檔案，.auth-image</li>



<li>修改 Register.jsx 檔案，Alert 部分</li>



<li>修改App.css 檔案 .alert 部分</li>



<li>修改 Register.jsx 檔案，loading 部分</li>



<li>複製 Register.jsx 檔案，&lt;Card> 部分、import 部分<br>貼到 Login.jsx 檔案</li>



<li>修改 Login.jsx 檔案</li>



<li>下載 login.png 檔案，尋找替代的圖片</li>



<li>在 src 資料夾裡面建立 hooks 資料夾</li>



<li>在 src 資料夾裡面建立 contexts 資料夾</li>



<li>在 hooks 資料夾裡面建立 useSignup.js 檔案</li>



<li>使用 rafce 片段快速建立程式碼<br>修改 useSignup.js 檔案</li>



<li>在建立帳戶的頁面填寫表單測試 console 查詢</li>



<li>在 contexts 資料夾裡面建立 AuthContext.js 檔案</li>



<li>使用 rafce 片段快速建立程式碼<br>修改 AuthContext.js 檔案</li>



<li>修改 userSignup.js 檔案</li>



<li>刪除 index.css 檔案<br>修改 main.jsx 檔案</li>



<li>修改 useSignup.js 檔案</li>



<li>修改 Register.jsx 檔案，registerUser、loading、error 部分</li>



<li>AuthContext.js 檔案名稱修改成 AuthContext.jsx 檔案</li>



<li>修改 main.jsx 檔案，匯入 AuthProvider</li>



<li>修改 useSignup.jsx 檔案，匯入 useAuth</li>



<li>修改 AuthContext.jsx 檔案，匯入 useState</li>



<li>Debug 排除錯誤程式碼</li>



<li>在建立帳戶的頁面填寫資料送出測試</li>



<li>執行終端機 server – nodemon index.js</li>



<li>修改 useSignup.js 檔案，增加 headers、修改 setLoading 改為 true</li>



<li>修改 App.jsx 檔案，&lt;Route /></li>



<li>輸入 http://localhost:5173/dashboard 前往 dashboard 頁面</li>



<li>修改 Dashboard.jsx 檔案</li>



<li>在 hook 資料夾裡面建立 useLogin.js 檔案</li>



<li>修改 useLogin.js 檔案<br>複製 useSignup.js 檔案程式碼貼到 useLogin 檔案修改</li>



<li>修改 Login.jsx 檔案</li>



<li>修改 useLogin.js 檔案，修改 res.status 狀態碼</li>



<li>測試註冊和登入功能是否能正常運行</li>



<li>修改 Dashboard.jsx 檔案</li>



<li>修改 App.css 檔案</li>



<li>測試註冊、登入、登出功能是否都能正常運作</li>
</ul>



<pre class="wp-block-code"><code>// src/App.jsx
import React from 'react';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import './App.css';
import Register from './Auth/Register';
import Login from './Auth/Login';
import Dashboard from './pages/Dashboard';
import { useAuth } from './contexts/AuthContext';

const App = () =&gt; {
  const {isAuthenticated} = useAuth();
  return (
    &lt;Router&gt;
      &lt;Routes&gt;
        &lt;Route
          path='/'
          element={
            !isAuthenticated ? &lt;Register /&gt; : &lt;Navigate to='/dashboard' /&gt;
          } 
        /&gt;
        &lt;Route
          path='/login'
          element={ !isAuthenticated ? &lt;Login /&gt; : &lt;Navigate to='/dashboard' /&gt;}
        /&gt;
        &lt;Route
          path='/dashboard'
          element={ isAuthenticated ? &lt;Dashboard /&gt; : &lt;Login /&gt;}
        /&gt;
      &lt;/Routes&gt;
    &lt;/Router&gt;
  )
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// Auth/Register.jsx
import React from 'react'
import { Alert, Card, Flex, Form, Typography, Input, Spin, Button } from 'antd';
import { Link } from 'react-router-dom';
import registerImage from '../assets/register.jpg';
import useSignup from '../hooks/useSignup';

const Register = () =&gt; {
  const { loading, error, registerUser } = useSignup();
  const handleRegister = (values) =&gt; {
    registerUser(values);
  };

  return (
    &lt;Card className='form-container'&gt;
      &lt;Flex gap='large' align='center'&gt;
        {/* Form */}
        &lt;Flex vertical flex={1}&gt;
          &lt;Typography.Title level={3} strong className='title'&gt;
            Create an account
          &lt;/Typography.Title&gt;
          &lt;Typography.Text type='secondary' strong className='slogan'&gt;Join for exclusive access&lt;/Typography.Text&gt;
          &lt;Form
            layout="vertical"
            onFinish={handleRegister}
            autoComplete="off"&gt;
              &lt;Form.Item
                label="Full Name"
                name="name"
                rules={&#91;
                  {
                    required: true,
                    message: 'Please input your full name!'
                  },
                ]}&gt;
                &lt;Input size='large' placeholder="Enter your full name" /&gt;
              &lt;/Form.Item&gt;
              &lt;Form.Item
                label="Email"
                name="email"
                rules={&#91;
                  {
                    required: true,
                    message: 'Please input your Email!'
                  },
                  {
                    type: 'email',
                    message: 'The input is not valid Email'
                  }
                ]}&gt;
                &lt;Input size='large' placeholder="Enter your email" /&gt;
              &lt;/Form.Item&gt;
              &lt;Form.Item
                label="Password"
                name="password"
                rules={&#91;
                  {
                    required: true,
                    message: 'Please input your Password!'
                  },
                ]}&gt;
                &lt;Input.Password size='large' placeholder="Enter your password" /&gt;
              &lt;/Form.Item&gt;
              &lt;Form.Item
                label="Password"
                name="passwordConfirm"
                rules={&#91;
                  {
                    required: true,
                    message: 'Please input your Confirm Password!'
                  },
                ]}&gt;
                &lt;Input.Password size='large' placeholder="Re-enter your password" /&gt;
              &lt;/Form.Item&gt;

              {error &amp;&amp; (
                  &lt;Alert
                    description={error}
                    type='error'
                    showIcon
                    closable
                    className='alert'
                  /&gt;
                )}

              &lt;Form.Item&gt;
                &lt;Button
                  type={`${loading ? '' : 'primary'}`}
                  htmlType="submit"
                  size="large"
                  className="btn"&gt;
                    {loading ? &lt;Spin /&gt; : 'Create Account'}
                  &lt;/Button&gt;
              &lt;/Form.Item&gt;
              &lt;Form.Item&gt;
                &lt;Link to="/login"&gt;
                  &lt;Button size="large" className='btn'&gt;Sign In&lt;/Button&gt;
                &lt;/Link&gt;
              &lt;/Form.Item&gt;
          &lt;/Form&gt;
        &lt;/Flex&gt;

        {/* Image */}
        &lt;Flex flex={1}&gt;
          &lt;img src={registerImage} className='auth-image' /&gt;
        &lt;/Flex&gt;
      &lt;/Flex&gt;
    &lt;/Card&gt;
  )
}

export default Register
</code></pre>



<pre class="wp-block-code"><code>// Auth/Login.jsx
import React from 'react'
import { Alert, Card, Flex, Form, Typography, Input, Spin, Button } from 'antd';
import { Link } from 'react-router-dom';
import loginImage from '../assets/login.jpg';
import useLogin from '../hooks/useLogin';

const Login = () =&gt; {
  const { error, loading, loginUser } = useLogin();
  const handleLogin = async (values) =&gt; {
    await loginUser(values);
  };

  return (
    &lt;Card className='form-container'&gt;
      &lt;Flex gap='large' align='center'&gt;
        {/* Image */}
        &lt;Flex flex={1}&gt;
          &lt;img src={loginImage} className='auth-image' /&gt;
        &lt;/Flex&gt;
        {/* Form */}
        &lt;Flex vertical flex={1}&gt;
          &lt;Typography.Title level={3} strong className='title'&gt;
            Sign In
          &lt;/Typography.Title&gt;
          &lt;Typography.Text type='secondary' strong className='slogan'&gt;Unlock you world.&lt;/Typography.Text&gt;
          &lt;Form
            layout="vertical"
            onFinish={handleLogin}
            autoComplete="off"&gt;
              
              &lt;Form.Item
                label="Email"
                name="email"
                rules={&#91;
                  {
                    required: true,
                    message: 'Please input your Email!'
                  },
                  {
                    type: 'email',
                    message: 'The input is not valid Email'
                  }
                ]}&gt;
                &lt;Input size='large' placeholder="Enter your email" /&gt;
              &lt;/Form.Item&gt;
              &lt;Form.Item
                label="Password"
                name="password"
                rules={&#91;
                  {
                    required: true,
                    message: 'Please input your Password!'
                  },
                ]}&gt;
                &lt;Input.Password size='large' placeholder="Enter your password" /&gt;
              &lt;/Form.Item&gt;
              
              {error &amp;&amp; (
                &lt;Alert
                  description={error}
                  type='error'
                  showIcon
                  closable
                  className='alert'
                /&gt;
              )}

              &lt;Form.Item&gt;
                &lt;Button
                  type={`${loading ? '' : 'primary'}`}
                  htmlType="submit"
                  size="large"
                  className="btn"&gt;
                    {loading ? &lt;Spin /&gt; : 'Sign In'}
                  &lt;/Button&gt;
              &lt;/Form.Item&gt;
              &lt;Form.Item&gt;
                &lt;Link to="/"&gt;
                  &lt;Button size="large" className='btn'&gt;Create  an account&lt;/Button&gt;
                &lt;/Link&gt;
              &lt;/Form.Item&gt;
          &lt;/Form&gt;
        &lt;/Flex&gt;
      &lt;/Flex&gt;
    &lt;/Card&gt;
  )
}

export default Login
</code></pre>



<pre class="wp-block-code"><code>// src/pages/Dashboard.jsx
import React from 'react'
import { Avatar ,Button, Card, Flex, Typography } from 'antd'
import { useAuth } from '../contexts/AuthContext'
import { UserOutlined } from '@ant-design/icons'

const Dashboard = () =&gt; {
  const { userData, logout } = useAuth();

  const handleLogout = async () =&gt; {
    await logout();
  };
  return (
    &lt;Card className='profile-card'&gt;
      &lt;Flex vertical gap='small' align='center'&gt;
        &lt;Avatar size={150} icon={&lt;UserOutlined /&gt;} className='avatar' /&gt;
        &lt;Typography.Title
          level={2}
          strong
          className='username'&gt;
            {userData.name}
        &lt;/Typography.Title&gt;
        &lt;Typography.Text type='secondary' strong&gt;
          Email: {userData.email}
        &lt;/Typography.Text&gt;
        &lt;Typography.Text type='secondary'&gt;
          Role: {userData.role}
        &lt;/Typography.Text&gt;
        &lt;Button size='large' type='primary' className='profile-btn' onClick={handleLogout}&gt;Logout&lt;/Button&gt;
      &lt;/Flex&gt;
    &lt;/Card&gt;
  )
}

export default Dashboard
</code></pre>



<pre class="wp-block-code"><code>// App.css
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800&amp;display=swap');

* {
  margin: 0;
  padding: 0;
  font-family: 'Poppins', sans-serif;
}

body {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background: #cbdbff;
}

/* form */
.form-container {
  width: 1000px;
}

.title,
.slogan {
  text-align: center;
}

.btn {
  width: 100%;
}

.auth-image {
  width: 100%;
  border-radius: 8px;
}

.alert {
  margin-bottom: 1.5rem;
}

/* profile-card */
.profile-card {
  width: 500px;
}

.avatar {
  margin-bottom: 1.5rem;
}

.username {
  text-transform: capitalize;
}

.profile-btn {
  margin-top: 1.3rem;
  width: 100%;
}
</code></pre>



<pre class="wp-block-code"><code>// src/hooks/useSignup.js
import { useState } from 'react';
import { message } from 'antd';
import { useAuth } from '../contexts/AuthContext.jsx';

const useSignup = () =&gt; {
  const { login } = useAuth();
  const &#91;error, setError] = useState(null);
  const &#91;loading, setLoading] = useState(null);

  const registerUser = async (values) =&gt; {
    if (values.password !== values.passwordConfirm) {
      return setError('Passwords are not the same');
    }

    try {
      setError(null);
      setLoading(true);
      const res = await fetch('http://localhost:3000/api/auth/signup', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(values),
      });

      const data = await res.json();
      if (res.status === 201) {
        message.success(data.message);
        login(data.token, data.user);
      } else if (res.status === 400) {
        setError(data.message);
      } else {
        message.error('Registration failed');
      }
    } catch (error) {
      message.error('Registration Failed');
    } finally {
      setLoading(false);
    }
  };

  return { loading, error, registerUser };
}

export default useSignup
</code></pre>



<pre class="wp-block-code"><code>// src/contexts/AuthContext.jsx
import React, { createContext, useContext, useEffect, useState } from 'react'

const AuthContext = createContext();

export const AuthProvider = ({ children }) =&gt; {
  const &#91;token, setToken] = useState(null);
  const &#91;userData, setUserData] = useState(null);
  const &#91;isAuthenticated, setIsAuthenticated] = useState(false);
  const storedData = JSON.parse(localStorage.getItem('user_data'));

  useEffect(() =&gt; {
    if (storedData) {
      const { userToken , user } = storedData;
      setToken(userToken);
      setUserData(user);
      setIsAuthenticated(true);
    }
  }, &#91;]);

  const login = (newToken, newData) =&gt; {
    localStorage.setItem(
      'user_data',
      JSON.stringify({ userToken: newToken, user: newData }),
    );

    setToken(newToken);
    setUserData(newData);
    setIsAuthenticated(true);
  };

  const logout = () =&gt; {
    localStorage.removeItem('user_data');
    setToken(null);
    setUserData(null);
    setIsAuthenticated(false);
  };
  return (
  &lt;AuthContext.Provider value={{token, isAuthenticated, login, logout, userData}}&gt;
    {children}
  &lt;/AuthContext.Provider&gt;
  );
}

export const useAuth = () =&gt; useContext(AuthContext);
</code></pre>



<pre class="wp-block-code"><code>// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { AuthProvider } from './contexts/AuthContext.jsx'

ReactDOM.createRoot(document.getElementById('root')).render(
  &lt;React.StrictMode&gt;
    &lt;AuthProvider&gt;
      &lt;App /&gt;
    &lt;/AuthProvider&gt;
  &lt;/React.StrictMode&gt;,
)
</code></pre>



<pre class="wp-block-code"><code>// src/hooks/useLogin.js
import { useState } from 'react';
import { message } from 'antd';
import { useAuth } from '../contexts/AuthContext.jsx';

const useLogin = () =&gt; {
  const { login } = useAuth();
  const &#91;error, setError] = useState(null);
  const &#91;loading, setLoading] = useState(null);

  const loginUser = async (values) =&gt; {
    try {
      setError(null);
      setLoading(true);
      const res = await fetch('http://localhost:3000/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(values),
      });

      const data = await res.json();
      if (res.status === 200) {
        message.success(data.message);
        login(data.token, data.user);
      } else if (res.status === 404) {
        setError(data.message);
      } else {
        message.error('Registration failed');
      }
    } catch (error) {
      message.error('Registration Failed');
    } finally {
      setLoading(false);
    }
  };

  return { loading, error, loginUser };
}

export default useLogin</code></pre>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>React Crash Course 2024</title>
		<link>/wordpress_blog/react-crash-2024/</link>
		
		<dc:creator><![CDATA[gee.hsu]]></dc:creator>
		<pubDate>Wed, 05 Jun 2024 03:43:41 +0000</pubDate>
				<category><![CDATA[Youtube]]></category>
		<guid isPermaLink="false">/wordpress_blog/?p=852</guid>

					<description><![CDATA[Learning From Youtube Channel: T [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Learning From Youtube Channel: Traversy Media<br>Video:&nbsp;<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=LDB4uaJ87e0" target="_blank">React Crash Course 2024</a><br>Thank you.</p>



<p>Learn the basics of React, such as components, props, state, data fetching, and more, while building a job listing frontend.</p>



<p>Timestamps:<br>00:00:00 – Intro<br>00:01:55 – What Is React? (Slide)<br>00:03:43 – Why React? (Slide)<br>00:07:19 – What Are Components? (Slide)<br>00:08:21 – What is State? (Slide)<br>00:10:00 – What Are Hooks? (Slide)<br>00:11:17 – What IS JSX? (Slide)<br>00:12:42 – SPA, SSR, SSG (Slide)<br>00:15:38 – Vite (Slide)<br>00:16:30 – Project Demo<br>00:19:53 – Setup React With Vite<br>00:22:29 – File Explanation<br>00:25:11 – Boilerplate Cleanup<br>00:26:48 – Tailwind CSS Setup<br>00:30:24 – JSX Crash Course<br>00:39:37 – Start Homepage<br>00:42:00 – Navbar Component<br>00:43:56 – Image Import<br>00:45:24 – Hero Component<br>00:46:17 – Props<br>00:48:00 – Default Props<br>00:48:51 – Wrapper Components<br>00:55:14 – JobListings Component<br>00:58:50 – Create Lists With map()<br>01:03:20 – Single JobListing Component<br>01:05:49 – Limit Jobs to 3<br>01:07:50 – useState() Hook &amp; Desc Toggle<br>01:13:07 – Creating an Event<br>01:14:20 – Updating Component State<br>01:16:00 – React Icons Package<br>01:18:00 – React Router Setup<br>01:20:21 – Create Routes From Elements<br>01:21:36 – Router Provider<br>01:22:36 – Homepage Component/Route<br>01:30:50 – Link Component<br>01:34:20 – Custom 404 Page<br>01:36:55 – Active Links With NavLink<br>01:41:00 – Conditional Rendering<br>01:43:10 – JSON Server Setup<br>01:47:00 – useEffect() &amp; Data Fetching<br>01:53:07 – Loading Spinner<br>01:51:06 – Conditional Fetching (Error Timestamp)<br>01:59:45 – Proxying<br>02:03:38 – Single Job Page<br>02:09:04 – useParams() to Get ID<br>02:12:25 – Data Loaders<br>02:16:36 – Single Job Output<br>02:22:00 – Add Job Page<br>02:23:40 – Working With Forms<br>02:30:05 – Form Submission<br>02:35:27 – Pass Function as Prop<br>02:39:32 – POST Request to Add Job<br>02:41:45 – Delete Job Button/function<br>02:45:12 – DELETE Request to Remove Job<br>02:46:50 – React Toastify Package<br>02:50:08 – Edit Job Page/Form<br>02:56:05 – Update Form Submission<br>02:58:54 – PUT Request to Update Job<br>3:02:10 – Build Static Assets For Production</p>



<h2 class="wp-block-heading">Intro</h2>



<h2 class="wp-block-heading">What Is React? (Slide)</h2>



<h3 class="wp-block-heading">What Is React?</h3>



<ul class="wp-block-list">
<li>React is a JavaScript library/framework for building user interfaces. It was created by Facebook.</li>



<li>Websites/UIs are looked at in terms of components.</li>



<li>React is currently the most popular out of the major front-end frameworks.</li>
</ul>



<h2 class="wp-block-heading">Why React? (Slide)</h2>



<h3 class="wp-block-heading">Why React?</h3>



<ul class="wp-block-list">
<li>React allows us to build very dynamic and interactive websites and user interfaces.</li>



<li>Very fast, especially with the new compiler.</li>



<li>There is a huge ecosystem from <strong>Next.js</strong> to <strong>React Native</strong>.</li>



<li>Best framework to learn to get a job.</li>
</ul>



<h2 class="wp-block-heading">What Are Components? (Slide)</h2>



<h3 class="wp-block-heading">Components</h3>



<ul class="wp-block-list">
<li>Reusable piece of code that can be used to build elements on the page.</li>



<li>Allows us to break down complex UIs, which makes them easier to maintain and scale.</li>



<li>Components can get <strong>props</strong> passed in and can hold their own <strong>state</strong>.</li>
</ul>



<h2 class="wp-block-heading">What Is State? (Slide)</h2>



<h3 class="wp-block-heading">State</h3>



<ul class="wp-block-list">
<li>State represents the data that a component manages internally.</li>



<li>This could be form input data, fetched data, UI-related data like if a modal is open/close.</li>



<li>There is also global state, which relates to the app as a whole and not a single component.</li>
</ul>



<h2 class="wp-block-heading">What Are Hooks? (Slide)</h2>



<h3 class="wp-block-heading">Hooks</h3>



<p>Allow us to use state and other React features within functional components</p>



<ul class="wp-block-list">
<li>useState</li>



<li>useEffect</li>



<li>useRef</li>



<li>useReducer</li>
</ul>



<p><strong>useContext</strong>,&nbsp;<strong>useMemo</strong>&nbsp;&amp;&nbsp;<strong>useCallback</strong>&nbsp;will be phased out in React 19</p>



<h2 class="wp-block-heading">What Is JSX? (Slide)</h2>



<h3 class="wp-block-heading">JSX (JavaScript Syntax Extension)</h3>



<p>An HTML-like syntax within JavaScript (components)</p>



<h2 class="wp-block-heading">SPA, SSR, SSG (Slide)</h2>



<h3 class="wp-block-heading">SPA, SSR &amp; SSG</h3>



<ul class="wp-block-list">
<li>Single Page App – Load a single HTML file and JavaScript loads the entire UI including routes.</li>



<li>Server-Side Rendered – Server sends fully rendered page to client. You can fetch data and load it as well.</li>



<li>Static Site Generation – React generates static HTML files at build time. These are very fast.</li>
</ul>



<h2 class="wp-block-heading">Vite (Slide)</h2>



<h3 class="wp-block-heading">Vite</h3>



<ul class="wp-block-list">
<li><strong>Vite</strong> is a super fast front-end toolkit that can be used for all kinds of JS projects including React.</li>



<li>It is built on top of ESBuild, which is a very fast JS bundler.</li>



<li>Fast development server wtih hot-reload.</li>



<li>Installed with npm create vite@latest</li>
</ul>



<h2 class="wp-block-heading">Project Demo</h2>



<ul class="wp-block-list">
<li><a href="https://github.com/bradtraversy/react-crash-2024" target="_blank" rel="noreferrer noopener">Code</a></li>



<li><a href="https://nodejs.org/en" target="_blank" rel="noreferrer noopener">Node.js</a></li>



<li>Google Chrome 擴充套件 – React Developer Tools</li>
</ul>



<h2 class="wp-block-heading">Setup React With Vite</h2>



<ul class="wp-block-list">
<li>使用終端機建立專案<br>npm create vite@latest react-crash-2024</li>



<li>Select a framework: React</li>



<li>Slect a variant: JavaScript</li>



<li>使用 VSCode 打開專案</li>



<li>修改 vite.config.js 檔案</li>



<li>執行 npm install 安裝</li>



<li>執行指令 npm run dev</li>
</ul>



<pre class="wp-block-code"><code>// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: &#91;react()],
  server: {
    port: 3000,
  },
});
</code></pre>



<h2 class="wp-block-heading">File Explanation</h2>



<ul class="wp-block-list">
<li>解釋 index.html、修改 index.html 檔案</li>



<li>解釋 src 資料夾</li>



<li>解釋 main.jsx 檔案</li>



<li>解釋 CSS 相關檔案，刪除 App.css、保留 index.css 檔案</li>



<li>解釋 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// index.html
&lt;!doctype html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;link rel="icon" type="image/svg+xml" href="/vite.svg" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&gt;
    &lt;title&gt;React Jobs&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id="root"&gt;&lt;/div&gt;
    &lt;script type="module" src="/src/main.jsx"&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h2 class="wp-block-heading">Boilerplate Cleanup</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案<br>建議安裝 VSCode 套件 ES7+React/Redux/React-Native snippets<br>快速建立程式碼片段介紹 rafce、rafc、rfc，這裡使用 rafce</li>



<li>清除 index.css 檔案程式碼</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
const App = () =&gt; {
  return (
    &lt;div&gt;
      App
    &lt;/div&gt;
  )
}

export default App
</code></pre>



<h2 class="wp-block-heading">Tailwind CSS Setup</h2>



<ul class="wp-block-list">
<li>Google 搜尋 vite react tailwind<br><a href="https://tailwindcss.com/docs/guides/vite" target="_blank" rel="noreferrer noopener">Install Tailwind CSS with Vite</a></li>



<li>Install Tailwind CSS<br>指令:<br>npm install -D tailwindcss postcss autoprefixer<br>npx tailwindcss init -p</li>



<li>Configure your template paths<br>複製官方範例並做修改</li>



<li>Add the Tailwind directives to your CSS<br>修改 index.css 檔案</li>



<li>重新執行 npm run dev</li>



<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: &#91;
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      fontFamily: {
        sans: &#91;'Roboto', 'sans-serif']
      },
      gridTemplateColumns: {
        '70/30': '70% 28%',
      },
    },
  },
  plugins: &#91;],
}
</code></pre>



<pre class="wp-block-code"><code>// index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
const App = () =&gt; {
  return (
    &lt;div className="text-5xl"&gt;
      App
    &lt;/div&gt;
  )
}

export default App
</code></pre>



<h2 class="wp-block-heading">JSX Crash Course</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>



<li>簡單介紹 JSX 語法</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
const App = () =&gt; {
  const name = 'John';
  const x = 10;
  const y = 20;
  const names = &#91;'Brad', 'Mary', 'Joe', 'Sara'];
  const loggedIn = true;
  const styles = {
    color: 'red',
    fontSize: '55px'
  }

  return (
    &lt;&gt;
      &lt;div className="text-5xl"&gt;
        App
      &lt;/div&gt;
      &lt;p style={styles}&gt;Hello {name}&lt;/p&gt;
      &lt;p&gt;The sum of {x} and {y} is { x + y }&lt;/p&gt;
      &lt;ul&gt;
        {names.map((name, index) =&gt; (
          &lt;li key={index}&gt;{ name }&lt;/li&gt;
        ))}
      &lt;/ul&gt;
      {loggedIn &amp;&amp; &lt;h1&gt;Hello Member&lt;/h1&gt;}
    &lt;/&gt;
  );
};

export default App;
</code></pre>



<h2 class="wp-block-heading">Start Homepage</h2>



<ul class="wp-block-list">
<li>到 _theme_files/index.html 檔案複製 &lt;body> 裡面需要的程式碼</li>



<li>修改 App.jsx 檔案</li>



<li>可以使用 Select All Occurrences of Find Match<br>Keyboard Shortcuts: Alt + F3</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
const App = () =&gt; {
  return (
    &lt;&gt;
      &lt;nav className="bg-indigo-700 border-b border-indigo-500"&gt;
      &lt;div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8"&gt;
        &lt;div className="flex h-20 items-center justify-between"&gt;
          &lt;div
            className="flex flex-1 items-center justify-center md:items-stretch md:justify-start"
          &gt;
            {/* &lt;!-- Logo --&gt; */}
            &lt;a className="flex flex-shrink-0 items-center mr-4" href="/index.html"&gt;
              &lt;img
                className="h-10 w-auto"
                src="images/logo.png"
                alt="React Jobs"
              /&gt;
              &lt;span className="hidden md:block text-white text-2xl font-bold ml-2"
                &gt;React Jobs&lt;/span
              &gt;
            &lt;/a&gt;
            &lt;div className="md:ml-auto"&gt;
              &lt;div className="flex space-x-2"&gt;
                &lt;a
                  href="/index.html"
                  className="text-white bg-black hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Home&lt;/a
                &gt;
                &lt;a
                  href="/jobs.html"
                  className="text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Jobs&lt;/a
                &gt;
                &lt;a
                  href="/add-job.html"
                  className="text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Add Job&lt;/a
                &gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/nav&gt;

    {/* &lt;!-- Hero --&gt; */}
    &lt;section className="bg-indigo-700 py-20 mb-4"&gt;
      &lt;div
        className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col items-center"
      &gt;
        &lt;div className="text-center"&gt;
          &lt;h1
            className="text-4xl font-extrabold text-white sm:text-5xl md:text-6xl"
          &gt;
            Become a React Dev
          &lt;/h1&gt;
          &lt;p className="my-4 text-xl text-white"&gt;
            Find the React job that fits your skills and needs
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    {/* &lt;!-- Developers and Employers --&gt; */}
    &lt;section className="py-4"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-lg"&gt;
          &lt;div className="bg-gray-100 p-6 rounded-lg shadow-md"&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Developers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              Browse our React jobs and start your career today
            &lt;/p&gt;
            &lt;a
              href="/jobs.html"
              className="inline-block bg-black text-white rounded-lg px-4 py-2 hover:bg-gray-700"
            &gt;
              Browse Jobs
            &lt;/a&gt;
          &lt;/div&gt;
          &lt;div className="bg-indigo-100 p-6 rounded-lg shadow-md"&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Employers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              List your job to find the perfect developer for the role
            &lt;/p&gt;
            &lt;a
              href="/add-job.html"
              className="inline-block bg-indigo-500 text-white rounded-lg px-4 py-2 hover:bg-indigo-600"
            &gt;
              Add Job
            &lt;/a&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    {/* &lt;!-- Browse Jobs --&gt; */}
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          Browse Jobs
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {/* &lt;!-- Job Listing 1 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Full-Time&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Senior React Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70 - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Boston, MA
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 2 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Front-End Engineer (React)&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Miami, FL
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 3 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;React.js Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Brooklyn, NY
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    &lt;section className="m-auto max-w-lg my-10 px-6"&gt;
      &lt;a
        href="jobs.html"
        className="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
        &gt;View All Jobs&lt;/a
      &gt;
    &lt;/section&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>



<h2 class="wp-block-heading">Navbar Component</h2>



<ul class="wp-block-list">
<li>在 src 資料夾裡面建立 components 資料夾</li>



<li>在 components 資料夾裡面建立 Navbar.jsx 檔案</li>



<li>修改 Navbar.jsx 檔案<br>rafce 建立片段程式碼</li>



<li>修改 App.jsx 檔案</li>



<li>查看 React Developer Tools > Components</li>
</ul>



<pre class="wp-block-code"><code>// Navbar.jsx
const Navbar = () =&gt; {
  return (
    &lt;nav className="bg-indigo-700 border-b border-indigo-500"&gt;
      &lt;div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8"&gt;
        &lt;div className="flex h-20 items-center justify-between"&gt;
          &lt;div
            className="flex flex-1 items-center justify-center md:items-stretch md:justify-start"
          &gt;
            {/* &lt;!-- Logo --&gt; */}
            &lt;a className="flex flex-shrink-0 items-center mr-4" href="/index.html"&gt;
              &lt;img
                className="h-10 w-auto"
                src="images/logo.png"
                alt="React Jobs"
              /&gt;
              &lt;span className="hidden md:block text-white text-2xl font-bold ml-2"
                &gt;React Jobs&lt;/span
              &gt;
            &lt;/a&gt;
            &lt;div className="md:ml-auto"&gt;
              &lt;div className="flex space-x-2"&gt;
                &lt;a
                  href="/index.html"
                  className="text-white bg-black hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Home&lt;/a
                &gt;
                &lt;a
                  href="/jobs.html"
                  className="text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Jobs&lt;/a
                &gt;
                &lt;a
                  href="/add-job.html"
                  className="text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Add Job&lt;/a
                &gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/nav&gt;
  )
}

export default Navbar
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import Navbar from './components/Navbar';

const App = () =&gt; {
  return (
    &lt;&gt;
      &lt;Navbar /&gt;

    {/* &lt;!-- Hero --&gt; */}
    &lt;section className="bg-indigo-700 py-20 mb-4"&gt;
      &lt;div
        className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col items-center"
      &gt;
        &lt;div className="text-center"&gt;
          &lt;h1
            className="text-4xl font-extrabold text-white sm:text-5xl md:text-6xl"
          &gt;
            Become a React Dev
          &lt;/h1&gt;
          &lt;p className="my-4 text-xl text-white"&gt;
            Find the React job that fits your skills and needs
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    {/* &lt;!-- Developers and Employers --&gt; */}
    &lt;section className="py-4"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-lg"&gt;
          &lt;div className="bg-gray-100 p-6 rounded-lg shadow-md"&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Developers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              Browse our React jobs and start your career today
            &lt;/p&gt;
            &lt;a
              href="/jobs.html"
              className="inline-block bg-black text-white rounded-lg px-4 py-2 hover:bg-gray-700"
            &gt;
              Browse Jobs
            &lt;/a&gt;
          &lt;/div&gt;
          &lt;div className="bg-indigo-100 p-6 rounded-lg shadow-md"&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Employers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              List your job to find the perfect developer for the role
            &lt;/p&gt;
            &lt;a
              href="/add-job.html"
              className="inline-block bg-indigo-500 text-white rounded-lg px-4 py-2 hover:bg-indigo-600"
            &gt;
              Add Job
            &lt;/a&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    {/* &lt;!-- Browse Jobs --&gt; */}
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          Browse Jobs
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {/* &lt;!-- Job Listing 1 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Full-Time&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Senior React Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70 - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Boston, MA
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 2 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Front-End Engineer (React)&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Miami, FL
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 3 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;React.js Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Brooklyn, NY
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    &lt;section className="m-auto max-w-lg my-10 px-6"&gt;
      &lt;a
        href="jobs.html"
        className="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
        &gt;View All Jobs&lt;/a
      &gt;
    &lt;/section&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>



<h2 class="wp-block-heading">Image Import</h2>



<ul class="wp-block-list">
<li>刪除 assets 資料夾裡面的 react.svg 檔案</li>



<li>在 assets 資料夾裡面建立 images 資料夾</li>



<li>到 _theme_files/images/logo.png 複製檔案到 images 資料夾裡面</li>



<li>修改 Navbar.jsx 檔案，把圖片匯入</li>
</ul>



<pre class="wp-block-code"><code>// Navbar.jsx
import logo from '../assets/images/logo.png';

const Navbar = () =&gt; {
  return (
    &lt;nav className="bg-indigo-700 border-b border-indigo-500"&gt;
      &lt;div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8"&gt;
        &lt;div className="flex h-20 items-center justify-between"&gt;
          &lt;div
            className="flex flex-1 items-center justify-center md:items-stretch md:justify-start"
          &gt;
            {/* &lt;!-- Logo --&gt; */}
            &lt;a className="flex flex-shrink-0 items-center mr-4" href="/index.html"&gt;
              &lt;img
                className="h-10 w-auto"
                src={logo}
                alt="React Jobs"
              /&gt;
              &lt;span className="hidden md:block text-white text-2xl font-bold ml-2"
                &gt;React Jobs&lt;/span
              &gt;
            &lt;/a&gt;
            &lt;div className="md:ml-auto"&gt;
              &lt;div className="flex space-x-2"&gt;
                &lt;a
                  href="/index.html"
                  className="text-white bg-black hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Home&lt;/a
                &gt;
                &lt;a
                  href="/jobs.html"
                  className="text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Jobs&lt;/a
                &gt;
                &lt;a
                  href="/add-job.html"
                  className="text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Add Job&lt;/a
                &gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/nav&gt;
  )
}

export default Navbar
</code></pre>



<h2 class="wp-block-heading">Hero Component</h2>



<ul class="wp-block-list">
<li>在 components 資料夾裡面建立 Hero.jsx 檔案</li>



<li>程式碼片段 rafce 快速建立</li>



<li>修改 App.jsx 檔案</li>



<li>查看 React Developer Tools > Components</li>
</ul>



<pre class="wp-block-code"><code>// Hero.jsx
const Hero = () =&gt; {
  return (
    &lt;section className="bg-indigo-700 py-20 mb-4"&gt;
      &lt;div
        className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col items-center"
      &gt;
        &lt;div className="text-center"&gt;
          &lt;h1
            className="text-4xl font-extrabold text-white sm:text-5xl md:text-6xl"
          &gt;
            Become a React Dev
          &lt;/h1&gt;
          &lt;p className="my-4 text-xl text-white"&gt;
            Find the React job that fits your skills and needs
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default Hero
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import Navbar from './components/Navbar';
import Hero from './components/Hero';

const App = () =&gt; {
  return (
    &lt;&gt;
      &lt;Navbar /&gt;
      &lt;Hero /&gt;

    {/* &lt;!-- Developers and Employers --&gt; */}
    &lt;section className="py-4"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-lg"&gt;
          &lt;div className="bg-gray-100 p-6 rounded-lg shadow-md"&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Developers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              Browse our React jobs and start your career today
            &lt;/p&gt;
            &lt;a
              href="/jobs.html"
              className="inline-block bg-black text-white rounded-lg px-4 py-2 hover:bg-gray-700"
            &gt;
              Browse Jobs
            &lt;/a&gt;
          &lt;/div&gt;
          &lt;div className="bg-indigo-100 p-6 rounded-lg shadow-md"&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Employers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              List your job to find the perfect developer for the role
            &lt;/p&gt;
            &lt;a
              href="/add-job.html"
              className="inline-block bg-indigo-500 text-white rounded-lg px-4 py-2 hover:bg-indigo-600"
            &gt;
              Add Job
            &lt;/a&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    {/* &lt;!-- Browse Jobs --&gt; */}
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          Browse Jobs
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {/* &lt;!-- Job Listing 1 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Full-Time&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Senior React Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70 - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Boston, MA
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 2 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Front-End Engineer (React)&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Miami, FL
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 3 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;React.js Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Brooklyn, NY
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    &lt;section className="m-auto max-w-lg my-10 px-6"&gt;
      &lt;a
        href="jobs.html"
        className="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
        &gt;View All Jobs&lt;/a
      &gt;
    &lt;/section&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>



<h2 class="wp-block-heading">Props</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>



<li>修改 Hero.jsx 檔案</li>



<li>(props) 也可以改寫成 ({ title, subtitle })<br>{props.title} 也可以改寫成 {title}<br>{props.subtitle} 也可以改寫成 {subtitle}</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
import Navbar from './components/Navbar';
import Hero from './components/Hero';

const App = () =&gt; {
  return (
    &lt;&gt;
      &lt;Navbar /&gt;
      &lt;Hero title="Test Title" subtitle="This is the subtitle" /&gt;

    {/* &lt;!-- Developers and Employers --&gt; */}
    &lt;section className="py-4"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-lg"&gt;
          &lt;div className="bg-gray-100 p-6 rounded-lg shadow-md"&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Developers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              Browse our React jobs and start your career today
            &lt;/p&gt;
            &lt;a
              href="/jobs.html"
              className="inline-block bg-black text-white rounded-lg px-4 py-2 hover:bg-gray-700"
            &gt;
              Browse Jobs
            &lt;/a&gt;
          &lt;/div&gt;
          &lt;div className="bg-indigo-100 p-6 rounded-lg shadow-md"&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Employers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              List your job to find the perfect developer for the role
            &lt;/p&gt;
            &lt;a
              href="/add-job.html"
              className="inline-block bg-indigo-500 text-white rounded-lg px-4 py-2 hover:bg-indigo-600"
            &gt;
              Add Job
            &lt;/a&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    {/* &lt;!-- Browse Jobs --&gt; */}
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          Browse Jobs
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {/* &lt;!-- Job Listing 1 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Full-Time&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Senior React Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70 - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Boston, MA
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 2 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Front-End Engineer (React)&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Miami, FL
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 3 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;React.js Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Brooklyn, NY
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    &lt;section className="m-auto max-w-lg my-10 px-6"&gt;
      &lt;a
        href="jobs.html"
        className="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
        &gt;View All Jobs&lt;/a
      &gt;
    &lt;/section&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// Hero.jsx
const Hero = ({ title, subtitle }) =&gt; {
  return (
    &lt;section className="bg-indigo-700 py-20 mb-4"&gt;
      &lt;div
        className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col items-center"
      &gt;
        &lt;div className="text-center"&gt;
          &lt;h1
            className="text-4xl font-extrabold text-white sm:text-5xl md:text-6xl"
          &gt;
            {title}
          &lt;/h1&gt;
          &lt;p className="my-4 text-xl text-white"&gt;
            {subtitle}
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default Hero
</code></pre>



<h2 class="wp-block-heading">Default Props</h2>



<ul class="wp-block-list">
<li>修改 Hero.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// Hero.jsx
const Hero = ({
  title = 'Become a React Dev',
  subtitle = 'Find the React job that fits your skill set'
}) =&gt; {
  return (
    &lt;section className="bg-indigo-700 py-20 mb-4"&gt;
      &lt;div
        className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col items-center"
      &gt;
        &lt;div className="text-center"&gt;
          &lt;h1
            className="text-4xl font-extrabold text-white sm:text-5xl md:text-6xl"
          &gt;
            {title}
          &lt;/h1&gt;
          &lt;p className="my-4 text-xl text-white"&gt;
            {subtitle}
          &lt;/p&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default Hero
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import Navbar from './components/Navbar';
import Hero from './components/Hero';

const App = () =&gt; {
  return (
    &lt;&gt;
      &lt;Navbar /&gt;
      &lt;Hero /&gt;

    {/* &lt;!-- Developers and Employers --&gt; */}
    &lt;section className="py-4"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-lg"&gt;
          &lt;div className="bg-gray-100 p-6 rounded-lg shadow-md"&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Developers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              Browse our React jobs and start your career today
            &lt;/p&gt;
            &lt;a
              href="/jobs.html"
              className="inline-block bg-black text-white rounded-lg px-4 py-2 hover:bg-gray-700"
            &gt;
              Browse Jobs
            &lt;/a&gt;
          &lt;/div&gt;
          &lt;div className="bg-indigo-100 p-6 rounded-lg shadow-md"&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Employers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              List your job to find the perfect developer for the role
            &lt;/p&gt;
            &lt;a
              href="/add-job.html"
              className="inline-block bg-indigo-500 text-white rounded-lg px-4 py-2 hover:bg-indigo-600"
            &gt;
              Add Job
            &lt;/a&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    {/* &lt;!-- Browse Jobs --&gt; */}
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          Browse Jobs
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {/* &lt;!-- Job Listing 1 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Full-Time&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Senior React Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70 - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Boston, MA
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 2 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Front-End Engineer (React)&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Miami, FL
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 3 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;React.js Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Brooklyn, NY
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    &lt;section className="m-auto max-w-lg my-10 px-6"&gt;
      &lt;a
        href="jobs.html"
        className="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
        &gt;View All Jobs&lt;/a
      &gt;
    &lt;/section&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>



<h2 class="wp-block-heading">Wrapper Component</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>



<li>在 components 資料夾裡面建立 HomeCards.jsx 檔案</li>



<li>快速建立程式碼片段 – rafce<br>修改 HomeCards.jsx 檔案</li>



<li>在 components 資料夾裡面建立 Card.jsx 檔案</li>



<li>快速建立程式碼片段 – rafce<br>修改 Card.jsx 檔案</li>



<li>修改 HomeCards.jsx 檔案，匯入 Card</li>



<li>修改 Card.jsx 檔案</li>



<li>修改 HomeCards.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
import Navbar from './components/Navbar';
import Hero from './components/Hero';
import HomeCards from './components/HomeCards';

const App = () =&gt; {
  return (
    &lt;&gt;
      &lt;Navbar /&gt;
      &lt;Hero /&gt;
      &lt;HomeCards /&gt;
    
    {/* &lt;!-- Browse Jobs --&gt; */}
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          Browse Jobs
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {/* &lt;!-- Job Listing 1 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Full-Time&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Senior React Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70 - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Boston, MA
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 2 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Front-End Engineer (React)&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Miami, FL
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 3 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;React.js Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Brooklyn, NY
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;

    &lt;section className="m-auto max-w-lg my-10 px-6"&gt;
      &lt;a
        href="jobs.html"
        className="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
        &gt;View All Jobs&lt;/a
      &gt;
    &lt;/section&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// HomeCards.jsx
import Card from './Card'

const HomeCards = () =&gt; {
  return (
    &lt;section className="py-4"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-lg"&gt;
          &lt;Card&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Developers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              Browse our React jobs and start your career today
            &lt;/p&gt;
            &lt;a
              href="/jobs.html"
              className="inline-block bg-black text-white rounded-lg px-4 py-2 hover:bg-gray-700"
            &gt;
              Browse Jobs
            &lt;/a&gt;
          &lt;/Card&gt;
          &lt;Card bg='bg-indigo-100'&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Employers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              List your job to find the perfect developer for the role
            &lt;/p&gt;
            &lt;a
              href="/add-job.html"
              className="inline-block bg-indigo-500 text-white rounded-lg px-4 py-2 hover:bg-indigo-600"
            &gt;
              Add Job
            &lt;/a&gt;
          &lt;/Card&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default HomeCards</code></pre>



<pre class="wp-block-code"><code>// Card.jsx
const Card = ({ children, bg = 'bg-gray-100' }) =&gt; {
  return (
    &lt;div className={`${bg} p-6 rounded-lg shadow-md`}&gt;
      {children}
    &lt;/div&gt;
  )
}

export default Card
</code></pre>



<h2 class="wp-block-heading">JobListings Component</h2>



<ul class="wp-block-list">
<li>到 src/jobs.json 複製貼到 src 資料夾</li>



<li>修改 jobs.json 檔案</li>



<li>在 components 資料夾裡面建立 JobListings.jsx 檔案</li>



<li>使用 rafce 快速鍵立程式碼片段</li>



<li>修改 JobListings.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>



<li>修改 JobListings.jsx 檔案，匯入 jobs.json 檔案</li>



<li>簡單推薦 console ninja VSCode 套件 (非必要)</li>
</ul>



<pre class="wp-block-code"><code>// jobs.json
&#91;
  {
    "id": "1",
    "title": "Senior React Developer",
    "type": "Full-Time",
    "description": "We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript, with experience working with modern JavaScript frameworks such as React or Angular.",
    "location": "Boston, MA",
    "salary": "$70K - $80K",
    "company": {
      "name": "NewTek Solutions",
      "description": "NewTek Solutions is a leading technology company specializing in web development and digital solutions. We pride ourselves on delivering high-quality products and services to our clients while fostering a collaborative and innovative work environment.",
      "contactEmail": "contact@teksolutions.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": "2",
    "title": "Front-End Engineer (React &amp; Redux)",
    "type": "Full-Time",
    "location": "Miami, FL",
    "description": "Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion for crafting beautiful and responsive web applications. Experience with UI/UX design principles and a strong attention to detail are highly desirable.",
    "salary": "$70K - $80K",
    "company": {
      "name": "Veneer Solutions",
      "description": "Veneer Solutions is a creative agency specializing in digital design and development. Our team is dedicated to pushing the boundaries of creativity and innovation to deliver exceptional results for our clients.",
      "contactEmail": "contact@loremipsum.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": "3",
    "title": "React.js Dev",
    "type": "Full-Time",
    "location": "Brooklyn, NY",
    "description": "Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference. We offer competitive compensation and a collaborative work environment where your ideas are valued.",
    "salary": "$70K - $80K",
    "company": {
      "name": "Dolor Cloud",
      "description": "Dolor Cloud is a leading technology company specializing in digital solutions for businesses of all sizes. With a focus on innovation and customer satisfaction, we are committed to delivering cutting-edge products and services.",
      "contactEmail": "contact@dolorsitamet.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": "4",
    "title": "React Front-End Developer",
    "type": "Part-Time",
    "description": "Join our team as a Part-Time Front-End Developer in beautiful Pheonix, AZ. We are looking for a self-motivated individual with a passion for creating engaging user experiences. This position offers flexible hours and the opportunity to work remotely.",
    "location": "Pheonix, AZ",
    "salary": "$60K - $70K",
    "company": {
      "name": "Alpha Elite",
      "description": "Alpha Elite is a dynamic startup specializing in digital marketing and web development. We are committed to fostering a diverse and inclusive workplace where creativity and innovation thrive.",
      "contactEmail": "contact@adipisicingelit.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": "5",
    "title": "Full Stack React Developer",
    "type": "Full-Time",
    "description": "Exciting opportunity for a Full-Time Front-End Developer in bustling Atlanta, GA. We are seeking a talented individual with a passion for building elegant and scalable web applications. Join our team and make an impact!",
    "location": "Atlanta, GA",
    "salary": "$90K - $100K",
    "company": {
      "name": "Browning Technologies",
      "description": "Browning Technologies is a rapidly growing technology company specializing in e-commerce solutions. We offer a dynamic and collaborative work environment where employees are encouraged to think creatively and innovate.",
      "contactEmail": "contact@consecteturadipisicing.com",
      "contactPhone": "555-555-5555"
    }
  },
  {
    "id": "6",
    "title": "React Native Developer",
    "type": "Full-Time",
    "description": "Join our team as a Front-End Developer in beautiful Portland, OR. We are looking for a skilled and enthusiastic individual to help us create innovative web solutions. Competitive salary and great benefits package available.",
    "location": "Portland, OR",
    "salary": "$100K - $110K",
    "company": {
      "name": "Port Solutions INC",
      "description": "Port Solutions is a leading technology company specializing in software development and digital marketing. We are committed to providing our clients with cutting-edge solutions and our employees with a supportive and rewarding work environment.",
      "contactEmail": "contact@ipsumlorem.com",
      "contactPhone": "555-555-5555"
    }
  }
]
</code></pre>



<pre class="wp-block-code"><code>// JobListings.jsx
import jobs from '../jobs.json';

const JobListings = () =&gt; {
  console.log(jobs);

  return (
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          Browse Jobs
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {/* &lt;!-- Job Listing 1 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Full-Time&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Senior React Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70 - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Boston, MA
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 2 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;Front-End Engineer (React)&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Miami, FL
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          {/* &lt;!-- Job Listing 3 --&gt; */}
          &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
            &lt;div className="p-4"&gt;
              &lt;div className="mb-6"&gt;
                &lt;div className="text-gray-600 my-2"&gt;Remote&lt;/div&gt;
                &lt;h3 className="text-xl font-bold"&gt;React.js Developer&lt;/h3&gt;
              &lt;/div&gt;

              &lt;div className="mb-5"&gt;
                Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference...
              &lt;/div&gt;

              &lt;h3 className="text-indigo-500 mb-2"&gt;$70K - $80K / Year&lt;/h3&gt;

              &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

              &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                &lt;div className="text-orange-700 mb-3"&gt;
                  &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                  Brooklyn, NY
                &lt;/div&gt;
                &lt;a
                  href="job.html"
                  className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                &gt;
                  Read More
                &lt;/a&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default JobListings
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import Navbar from './components/Navbar';
import Hero from './components/Hero';
import HomeCards from './components/HomeCards';
import JobListings from './components/JobListings';

const App = () =&gt; {
  return (
    &lt;&gt;
      &lt;Navbar /&gt;
      &lt;Hero /&gt;
      &lt;HomeCards /&gt;
      &lt;JobListings /&gt;

    &lt;section className="m-auto max-w-lg my-10 px-6"&gt;
      &lt;a
        href="jobs.html"
        className="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
        &gt;View All Jobs&lt;/a
      &gt;
    &lt;/section&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>



<h2 class="wp-block-heading">Create Lists With map()</h2>



<ul class="wp-block-list">
<li>修改 JobListings.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// JobListings.jsx
import jobs from '../jobs.json';

const JobListings = () =&gt; {
  return (
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          Browse Jobs
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {jobs.map((job) =&gt; (
            &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
              &lt;div className="p-4"&gt;
                &lt;div className="mb-6"&gt;
                  &lt;div className="text-gray-600 my-2"&gt;{job.type}&lt;/div&gt;
                  &lt;h3 className="text-xl font-bold"&gt;{job.title}&lt;/h3&gt;
                &lt;/div&gt;

                &lt;div className="mb-5"&gt;
                  {job.description}
                &lt;/div&gt;

                &lt;h3 className="text-indigo-500 mb-2"&gt;{job.salary} / Year&lt;/h3&gt;

                &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

                &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
                  &lt;div className="text-orange-700 mb-3"&gt;
                    &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
                    {job.location}
                  &lt;/div&gt;
                  &lt;a
                    href={`/job/${job.id}`}
                    className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
                  &gt;
                    Read More
                  &lt;/a&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          ))}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default JobListings
</code></pre>



<h2 class="wp-block-heading">Single JobListing Component</h2>



<ul class="wp-block-list">
<li>在 components 資料夾裡面建立 JobListing.jsx 檔案</li>



<li>快速片段建立程式碼 – rafce</li>



<li>修改 JobListing.jsx 檔案</li>



<li>修改 JobListings.jsx 檔案</li>



<li>使用 React Developer Tools 套件工具</li>
</ul>



<pre class="wp-block-code"><code>// JobListing.jsx
const JobListing = ({ job }) =&gt; {
  return (
    &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
      &lt;div className="p-4"&gt;
        &lt;div className="mb-6"&gt;
          &lt;div className="text-gray-600 my-2"&gt;{job.type}&lt;/div&gt;
          &lt;h3 className="text-xl font-bold"&gt;{job.title}&lt;/h3&gt;
        &lt;/div&gt;

        &lt;div className="mb-5"&gt;
          {job.description}
        &lt;/div&gt;

        &lt;h3 className="text-indigo-500 mb-2"&gt;{job.salary} / Year&lt;/h3&gt;

        &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

        &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
          &lt;div className="text-orange-700 mb-3"&gt;
            &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
            {job.location}
          &lt;/div&gt;
          &lt;a
            href={`/job/${job.id}`}
            className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
          &gt;
            Read More
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default JobListing
</code></pre>



<pre class="wp-block-code"><code>// JobListings.jsx
import JobListing from './JobListing';
import jobs from '../jobs.json';

const JobListings = () =&gt; {
  return (
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          Browse Jobs
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {jobs.map((job) =&gt; (
            &lt;JobListing key={job.id} job={ job } /&gt;
          ))}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default JobListings
</code></pre>



<h2 class="wp-block-heading">Limit Jobs to 3</h2>



<ul class="wp-block-list">
<li>修改 JobListings.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>



<li>在 components 資料夾裡面建立 ViewAllJobs.jsx 檔案</li>



<li>使用片段 rafce 快速建立程式碼<br>修改 ViewAllJobs.jsx 檔案</li>



<li>修改 App.jsx 檔案，匯入 ViewAllJobs</li>
</ul>



<pre class="wp-block-code"><code>// JobListings.jsx
import JobListing from './JobListing';
import jobs from '../jobs.json';

const JobListings = () =&gt; {
  const recentJobs = jobs.slice(0, 3);

  return (
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          Browse Jobs
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {recentJobs.map((job) =&gt; (
            &lt;JobListing key={job.id} job={ job } /&gt;
          ))}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default JobListings
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import Navbar from './components/Navbar';
import Hero from './components/Hero';
import HomeCards from './components/HomeCards';
import JobListings from './components/JobListings';
import ViewAllJobs from './components/ViewAllJobs';

const App = () =&gt; {
  return (
    &lt;&gt;
      &lt;Navbar /&gt;
      &lt;Hero /&gt;
      &lt;HomeCards /&gt;
      &lt;JobListings /&gt;
      &lt;ViewAllJobs /&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// ViewAllJobs.jsx
const ViewAllJobs = () =&gt; {
  return (
    &lt;section className="m-auto max-w-lg my-10 px-6"&gt;
      &lt;a
        href="/jobs"
        className="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
        &gt;View All Jobs&lt;/a
      &gt;
    &lt;/section&gt;
  )
}

export default ViewAllJobs
</code></pre>



<h2 class="wp-block-heading">useState() Hook &amp; Desc Toggle</h2>



<ul class="wp-block-list">
<li>修改 JobListing.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// JobListing.jsx
import { useState } from "react";

const JobListing = ({ job }) =&gt; {
  const &#91;showFullDescription, setShowFullDescription] = useState(false);

  let description = job.description;

  if(!showFullDescription) {
    description = description.substring(0, 90) + '...';
  }

  return (
    &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
      &lt;div className="p-4"&gt;
        &lt;div className="mb-6"&gt;
          &lt;div className="text-gray-600 my-2"&gt;{job.type}&lt;/div&gt;
          &lt;h3 className="text-xl font-bold"&gt;{job.title}&lt;/h3&gt;
        &lt;/div&gt;

        &lt;div className="mb-5"&gt;
          {description}
        &lt;/div&gt;

        &lt;button className="text-indigo-500 mb-5 hover:text-indigo-600"&gt;
          { showFullDescription ? 'Less' : 'More' }
        &lt;/button&gt;

        &lt;h3 className="text-indigo-500 mb-2"&gt;{job.salary} / Year&lt;/h3&gt;

        &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

        &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
          &lt;div className="text-orange-700 mb-3"&gt;
            &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
            {job.location}
          &lt;/div&gt;
          &lt;a
            href={`/job/${job.id}`}
            className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
          &gt;
            Read More
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default JobListing
</code></pre>



<h2 class="wp-block-heading">Creating an Event</h2>



<ul class="wp-block-list">
<li>修改 JobListing.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// JobListing.jsx
import { useState } from "react";

const JobListing = ({ job }) =&gt; {
  const &#91;showFullDescription, setShowFullDescription] = useState(false);

  let description = job.description;

  if(!showFullDescription) {
    description = description.substring(0, 90) + '...';
  }

  return (
    &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
      &lt;div className="p-4"&gt;
        &lt;div className="mb-6"&gt;
          &lt;div className="text-gray-600 my-2"&gt;{job.type}&lt;/div&gt;
          &lt;h3 className="text-xl font-bold"&gt;{job.title}&lt;/h3&gt;
        &lt;/div&gt;

        &lt;div className="mb-5"&gt;
          {description}
        &lt;/div&gt;

        &lt;button onClick={() =&gt; setShowFullDescription(!setShowFullDescription)} className="text-indigo-500 mb-5 hover:text-indigo-600"&gt;
          { showFullDescription ? 'Less' : 'More' }
        &lt;/button&gt;

        &lt;h3 className="text-indigo-500 mb-2"&gt;{job.salary} / Year&lt;/h3&gt;

        &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

        &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
          &lt;div className="text-orange-700 mb-3"&gt;
            &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
            {job.location}
          &lt;/div&gt;
          &lt;a
            href={`/job/${job.id}`}
            className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
          &gt;
            Read More
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default JobListing
</code></pre>



<h2 class="wp-block-heading">Updating Component State</h2>



<ul class="wp-block-list">
<li>修改 JobListing.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// JobListing.jsx
import { useState } from "react";

const JobListing = ({ job }) =&gt; {
  const &#91;showFullDescription, setShowFullDescription] = useState(false);

  let description = job.description;

  if(!showFullDescription) {
    description = description.substring(0, 90) + '...';
  }

  return (
    &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
      &lt;div className="p-4"&gt;
        &lt;div className="mb-6"&gt;
          &lt;div className="text-gray-600 my-2"&gt;{job.type}&lt;/div&gt;
          &lt;h3 className="text-xl font-bold"&gt;{job.title}&lt;/h3&gt;
        &lt;/div&gt;

        &lt;div className="mb-5"&gt;
          {description}
        &lt;/div&gt;

        &lt;button onClick={() =&gt; setShowFullDescription((prevState) =&gt; !prevState)} className="text-indigo-500 mb-5 hover:text-indigo-600"&gt;
          { showFullDescription ? 'Less' : 'More' }
        &lt;/button&gt;

        &lt;h3 className="text-indigo-500 mb-2"&gt;{job.salary} / Year&lt;/h3&gt;

        &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

        &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
          &lt;div className="text-orange-700 mb-3"&gt;
            &lt;i className="fa-solid fa-location-dot text-lg"&gt;&lt;/i&gt;
            {job.location}
          &lt;/div&gt;
          &lt;a
            href={`/job/${job.id}`}
            className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
          &gt;
            Read More
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default JobListing
</code></pre>



<h2 class="wp-block-heading">React Icons Package</h2>



<ul class="wp-block-list">
<li>安裝 react-icons 套件<br>npm i react-icons</li>



<li>修改 JobListing.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// JobListing.jsx
import { useState } from "react";
import { FaMapMarker } from 'react-icons/fa';

const JobListing = ({ job }) =&gt; {
  const &#91;showFullDescription, setShowFullDescription] = useState(false);

  let description = job.description;

  if(!showFullDescription) {
    description = description.substring(0, 90) + '...';
  }

  return (
    &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
      &lt;div className="p-4"&gt;
        &lt;div className="mb-6"&gt;
          &lt;div className="text-gray-600 my-2"&gt;{job.type}&lt;/div&gt;
          &lt;h3 className="text-xl font-bold"&gt;{job.title}&lt;/h3&gt;
        &lt;/div&gt;

        &lt;div className="mb-5"&gt;
          {description}
        &lt;/div&gt;

        &lt;button onClick={() =&gt; setShowFullDescription((prevState) =&gt; !prevState)} className="text-indigo-500 mb-5 hover:text-indigo-600"&gt;
          { showFullDescription ? 'Less' : 'More' }
        &lt;/button&gt;

        &lt;h3 className="text-indigo-500 mb-2"&gt;{job.salary} / Year&lt;/h3&gt;

        &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

        &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
          &lt;div className="text-orange-700 mb-3"&gt;
            &lt;FaMapMarker className="inline text-lg mb-1 mr-1" /&gt;
            {job.location}
          &lt;/div&gt;
          &lt;a
            href={`/job/${job.id}`}
            className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
          &gt;
            Read More
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default JobListing
</code></pre>



<h2 class="wp-block-heading">React Router Setup</h2>



<ul class="wp-block-list">
<li>安裝 react-router-dom 套件</li>



<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import Navbar from './components/Navbar';
import Hero from './components/Hero';
import HomeCards from './components/HomeCards';
import JobListings from './components/JobListings';
import ViewAllJobs from './components/ViewAllJobs';

const App = () =&gt; {
  return (
    &lt;&gt;
      &lt;Navbar /&gt;
      &lt;Hero /&gt;
      &lt;HomeCards /&gt;
      &lt;JobListings /&gt;
      &lt;ViewAllJobs /&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>



<h2 class="wp-block-heading">Create Routes From Elements</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import Navbar from './components/Navbar';
import Hero from './components/Hero';
import HomeCards from './components/HomeCards';
import JobListings from './components/JobListings';
import ViewAllJobs from './components/ViewAllJobs';

const router = createBrowserRouter(
  createRoutesFromElements(&lt;Route index element={ &lt;h1&gt;My App&lt;/h1&gt;} /&gt;)
);

const App = () =&gt; {
  return (
    &lt;&gt;
      &lt;Navbar /&gt;
      &lt;Hero /&gt;
      &lt;HomeCards /&gt;
      &lt;JobListings /&gt;
      &lt;ViewAllJobs /&gt;
    &lt;/&gt;
  )
}

export default App
</code></pre>



<h2 class="wp-block-heading">Router Provider</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>



<li>index 換成 path=’/about’ 也可運作</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import Navbar from './components/Navbar';
import Hero from './components/Hero';
import HomeCards from './components/HomeCards';
import JobListings from './components/JobListings';
import ViewAllJobs from './components/ViewAllJobs';

const router = createBrowserRouter(
  createRoutesFromElements(&lt;Route index element={ &lt;h1&gt;My App&lt;/h1&gt;} /&gt;)
);

const App = () =&gt; {
  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<h2 class="wp-block-heading">Homepage Component/Route</h2>



<ul class="wp-block-list">
<li>在 src 資料夾裡面建立 pages 資料夾</li>



<li>在 pages 資料夾裡面建立 HomePage.jsx 檔案</li>



<li>修改 HomePage.jsx 檔案<br>使用 rafce 片段快速建立程式碼</li>



<li>修改 App.jsx 檔案</li>



<li>修改 HomePage.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// HomePage.jsx
import Hero from '../components/Hero';

const HomePage = () =&gt; {
  return (
    &lt;&gt;
      &lt;Hero /&gt;
    &lt;/&gt;
  )
}

export default HomePage
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import HomePage from './pages/HomePage';

const router = createBrowserRouter(
  createRoutesFromElements(&lt;Route index element={&lt;HomePage /&gt;} /&gt;)
);

const App = () =&gt; {
  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<h2 class="wp-block-heading">Layouts</h2>



<ul class="wp-block-list">
<li>在 src 資料夾裡面建立 layouts 資料夾</li>



<li>在 layouts 資料夾裡面建立 MainLayout.jsx 檔案</li>



<li>修改 MainLayout.jsx 檔案<br>使用 rafce 片段快速建立程式碼</li>



<li>修改 App.jsx 檔案</li>



<li>修改 MainLayout.jsx 檔案</li>



<li>修改 HomePage.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// MainLayout.jsx
import { Outlet } from 'react-router-dom';
import Navbar from '../components/Navbar';

const MainLayout = () =&gt; {
  return (
    &lt;&gt;
      &lt;Navbar /&gt;
      &lt;Outlet /&gt;
    &lt;/&gt;
  )
}

export default MainLayout
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';

const router = createBrowserRouter(
  createRoutesFromElements(
  &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
    &lt;Route index element={&lt;HomePage /&gt;} /&gt;
  &lt;/Route&gt;
  )
);

const App = () =&gt; {
  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// HomePage.jsx
import Hero from '../components/Hero';
import HomeCards from '../components/HomeCards';
import JobListings from '../components/JobListings';
import ViewAllJobs from '../components/ViewAllJobs';

const HomePage = () =&gt; {
  return (
    &lt;&gt;
      &lt;Hero /&gt;
      &lt;HomeCards /&gt;
      &lt;JobListings /&gt;
      &lt;ViewAllJobs /&gt;
    &lt;/&gt;
  )
}

export default HomePage
</code></pre>



<h2 class="wp-block-heading">Jobs Page Component/Route</h2>



<ul class="wp-block-list">
<li>在 pages 資料夾裡面建立 JobsPage.jsx 檔案</li>



<li>使用 rafce 片段快速建立程式碼<br>修改 JobsPage.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>



<li>修改 Navbar.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// JobsPage.jsx
const JobsPage = () =&gt; {
  return (
    &lt;div&gt;JobsPage&lt;/div&gt;
  )
}

export default JobsPage
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';

const router = createBrowserRouter(
  createRoutesFromElements(
  &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
    &lt;Route index element={&lt;HomePage /&gt;} /&gt;
    &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
  &lt;/Route&gt;
  )
);

const App = () =&gt; {
  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// components/Navbar.jsx
import logo from '../assets/images/logo.png';

const Navbar = () =&gt; {
  return (
    &lt;nav className="bg-indigo-700 border-b border-indigo-500"&gt;
      &lt;div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8"&gt;
        &lt;div className="flex h-20 items-center justify-between"&gt;
          &lt;div
            className="flex flex-1 items-center justify-center md:items-stretch md:justify-start"
          &gt;
            {/* &lt;!-- Logo --&gt; */}
            &lt;a className="flex flex-shrink-0 items-center mr-4" href="/index.html"&gt;
              &lt;img
                className="h-10 w-auto"
                src={logo}
                alt="React Jobs"
              /&gt;
              &lt;span className="hidden md:block text-white text-2xl font-bold ml-2"
                &gt;React Jobs&lt;/span
              &gt;
            &lt;/a&gt;
            &lt;div className="md:ml-auto"&gt;
              &lt;div className="flex space-x-2"&gt;
                &lt;a
                  href="/"
                  className="text-white bg-black hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Home&lt;/a
                &gt;
                &lt;a
                  href="/jobs"
                  className="text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Jobs&lt;/a
                &gt;
                &lt;a
                  href="/add-job"
                  className="text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Add Job&lt;/a
                &gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/nav&gt;
  )
}

export default Navbar
</code></pre>



<h2 class="wp-block-heading">Link Component</h2>



<ul class="wp-block-list">
<li>修改 Navbar.jsx 檔案</li>



<li>修改 HomeCards.jsx 檔案</li>



<li>修改 JobListing.jsx 檔案</li>



<li>修改 ViewAllJobs.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// Navbar.jsx
import { Link } from 'react-router-dom';
import logo from '../assets/images/logo.png';

const Navbar = () =&gt; {
  return (
    &lt;nav className="bg-indigo-700 border-b border-indigo-500"&gt;
      &lt;div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8"&gt;
        &lt;div className="flex h-20 items-center justify-between"&gt;
          &lt;div
            className="flex flex-1 items-center justify-center md:items-stretch md:justify-start"
          &gt;
            &lt;Link className="flex flex-shrink-0 items-center mr-4" to="/"&gt;
              &lt;img
                className="h-10 w-auto"
                src={logo}
                alt="React Jobs"
              /&gt;
              &lt;span className="hidden md:block text-white text-2xl font-bold ml-2"
                &gt;React Jobs&lt;/span
              &gt;
            &lt;/Link&gt;
            &lt;div className="md:ml-auto"&gt;
              &lt;div className="flex space-x-2"&gt;
                &lt;Link
                  to="/"
                  className="text-white bg-black hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Home
                &lt;/Link&gt;
                &lt;Link
                  to="/jobs"
                  className="text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Jobs
                &lt;/Link&gt;
                &lt;Link
                  to="/add-job"
                  className="text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2"
                  &gt;Add Job
                &lt;/Link&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/nav&gt;
  )
}

export default Navbar
</code></pre>



<pre class="wp-block-code"><code>// HomeCards.jsx
import { Link } from 'react-router-dom';
import Card from './Card';

const HomeCards = () =&gt; {
  return (
    &lt;section className="py-4"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 rounded-lg"&gt;
          &lt;Card&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Developers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              Browse our React jobs and start your career today
            &lt;/p&gt;
            &lt;Link
              to="/jobs"
              className="inline-block bg-black text-white rounded-lg px-4 py-2 hover:bg-gray-700"
            &gt;
              Browse Jobs
            &lt;/Link&gt;
          &lt;/Card&gt;
          &lt;Card bg='bg-indigo-100'&gt;
            &lt;h2 className="text-2xl font-bold"&gt;For Employers&lt;/h2&gt;
            &lt;p className="mt-2 mb-4"&gt;
              List your job to find the perfect developer for the role
            &lt;/p&gt;
            &lt;Link
              to="/add-job"
              className="inline-block bg-indigo-500 text-white rounded-lg px-4 py-2 hover:bg-indigo-600"
            &gt;
              Add Job
            &lt;/Link&gt;
          &lt;/Card&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default HomeCards
</code></pre>



<pre class="wp-block-code"><code>// JobListing.jsx
import { useState } from "react";
import { FaMapMarker } from 'react-icons/fa';
import { Link } from 'react-router-dom';

const JobListing = ({ job }) =&gt; {
  const &#91;showFullDescription, setShowFullDescription] = useState(false);

  let description = job.description;

  if(!showFullDescription) {
    description = description.substring(0, 90) + '...';
  }

  return (
    &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
      &lt;div className="p-4"&gt;
        &lt;div className="mb-6"&gt;
          &lt;div className="text-gray-600 my-2"&gt;{job.type}&lt;/div&gt;
          &lt;h3 className="text-xl font-bold"&gt;{job.title}&lt;/h3&gt;
        &lt;/div&gt;

        &lt;div className="mb-5"&gt;
          {description}
        &lt;/div&gt;

        &lt;button onClick={() =&gt; setShowFullDescription((prevState) =&gt; !prevState)} className="text-indigo-500 mb-5 hover:text-indigo-600"&gt;
          { showFullDescription ? 'Less' : 'More' }
        &lt;/button&gt;

        &lt;h3 className="text-indigo-500 mb-2"&gt;{job.salary} / Year&lt;/h3&gt;

        &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

        &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
          &lt;div className="text-orange-700 mb-3"&gt;
            &lt;FaMapMarker className="inline text-lg mb-1 mr-1" /&gt;
            {job.location}
          &lt;/div&gt;
          &lt;Link
            to={`/job/${job.id}`}
            className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
          &gt;
            Read More
          &lt;/Link&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default JobListing
</code></pre>



<pre class="wp-block-code"><code>// ViewAllJobs.jsx
import  { Link } from 'react-router-dom';

const ViewAllJobs = () =&gt; {
  return (
    &lt;section className="m-auto max-w-lg my-10 px-6"&gt;
      &lt;Link
        to="/jobs"
        className="block bg-black text-white text-center py-4 px-6 rounded-xl hover:bg-gray-700"
        &gt;View All Jobs
      &lt;/Link&gt;
    &lt;/section&gt;
  )
}

export default ViewAllJobs
</code></pre>



<h2 class="wp-block-heading">Custom 404 Page</h2>



<ul class="wp-block-list">
<li>到 _theme_files/not-found.html 複製程式碼</li>



<li>在 pages 資料夾裡面建立 NotFoundPage.jsx 檔案</li>



<li>使用 rafce 片段快速建立程式碼<br>修改 NotFoundPage.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// NotFoundPage.jsx
import { Link } from 'react-router-dom';
import { FaExclamationTriangle } from 'react-icons/fa';

const NotFoundPage = () =&gt; {
  return (
    &lt;section className="text-center flex flex-col justify-center items-center h-96"&gt;
      &lt;FaExclamationTriangle className="text-yellow-400 text-6xl mb-4" /&gt;
      &lt;h1 className="text-6xl font-bold mb-4"&gt;404 Not Found&lt;/h1&gt;
      &lt;p className="text-xl mb-5"&gt;This page does not exist&lt;/p&gt;
      &lt;Link
        to="/"
        className="text-white bg-indigo-700 hover:bg-indigo-900 rounded-md px-3 py-2 mt-4"
        &gt;Go Back
      &lt;/Link&gt;
    &lt;/section&gt;
  )
}

export default NotFoundPage
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';
import NotFoundPage from './pages/NotFoundPage';

const router = createBrowserRouter(
  createRoutesFromElements(
  &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
    &lt;Route index element={&lt;HomePage /&gt;} /&gt;
    &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
    &lt;Route path='*' element={&lt;NotFoundPage /&gt;} /&gt;
  &lt;/Route&gt;
  )
);

const App = () =&gt; {
  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<h2 class="wp-block-heading">Active Links With NavLink</h2>



<ul class="wp-block-list">
<li>修改 Navbar.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// Navbar.jsx
import { NavLink } from 'react-router-dom';
import logo from '../assets/images/logo.png';

const Navbar = () =&gt; {
  const linkClass = ({ isActive }) =&gt; isActive ? 'bg-black text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2' : 'text-white hover:bg-gray-900 hover:text-white rounded-md px-3 py-2';

  return (
    &lt;nav className="bg-indigo-700 border-b border-indigo-500"&gt;
      &lt;div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8"&gt;
        &lt;div className="flex h-20 items-center justify-between"&gt;
          &lt;div
            className="flex flex-1 items-center justify-center md:items-stretch md:justify-start"
          &gt;
            &lt;NavLink className="flex flex-shrink-0 items-center mr-4" to="/"&gt;
              &lt;img
                className="h-10 w-auto"
                src={logo}
                alt="React Jobs"
              /&gt;
              &lt;span className="hidden md:block text-white text-2xl font-bold ml-2"
                &gt;React Jobs&lt;/span
              &gt;
            &lt;/NavLink&gt;
            &lt;div className="md:ml-auto"&gt;
              &lt;div className="flex space-x-2"&gt;
                &lt;NavLink
                  to="/"
                  className={linkClass}
                  &gt;Home
                &lt;/NavLink&gt;
                &lt;NavLink
                  to="/jobs"
                  className={linkClass}
                  &gt;Jobs
                &lt;/NavLink&gt;
                &lt;NavLink
                  to="/add-job"
                  className={linkClass}
                  &gt;Add Job
                &lt;/NavLink&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/nav&gt;
  )
}

export default Navbar
</code></pre>



<h2 class="wp-block-heading">Conditional Fetching</h2>



<ul class="wp-block-list">
<li>修改 JobsPage.jsx 檔案</li>



<li>修改 JobListings.jsx 檔案</li>



<li>修改 HomePage.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// pages/JobsPage.jsx
import JobListings from "../components/JobListings";

const JobsPage = () =&gt; {
  return &lt;section className="bg-blue-50 px-4 py-6"&gt;
    &lt;JobListings /&gt;
  &lt;/section&gt;
}

export default JobsPage
</code></pre>



<pre class="wp-block-code"><code>// JobListings.jsx
import JobListing from './JobListing';
import jobs from '../jobs.json';

const JobListings = ({ isHome = false }) =&gt; {
  const jobListings = isHome ? jobs.slice(0, 3) : jobs;

  return (
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          { isHome ? 'Recent Jobs' : 'Browse Jobs' }
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {jobListings.map((job) =&gt; (
            &lt;JobListing key={job.id} job={ job } /&gt;
          ))}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default JobListings
</code></pre>



<pre class="wp-block-code"><code>// HomePage.jsx
import Hero from '../components/Hero';
import HomeCards from '../components/HomeCards';
import JobListings from '../components/JobListings';
import ViewAllJobs from '../components/ViewAllJobs';

const HomePage = () =&gt; {
  return (
    &lt;&gt;
      &lt;Hero /&gt;
      &lt;HomeCards /&gt;
      &lt;JobListings isHome={true} /&gt;
      &lt;ViewAllJobs /&gt;
    &lt;/&gt;
  )
}

export default HomePage
</code></pre>



<h2 class="wp-block-heading">JSON Server Setup</h2>



<ul class="wp-block-list">
<li>npm <a href="https://www.npmjs.com/package/json-server" target="_blank" rel="noreferrer noopener">json-server</a></li>



<li>修改 jobs.json 檔案</li>



<li>安裝 json-server 套件<br>npm i -D json-server</li>



<li>修改 package.json 檔案</li>



<li>執行 server – npm run server</li>
</ul>



<pre class="wp-block-code"><code>// jobs.json
{
  "jobs": &#91;
    {
      "id": "1",
      "title": "Senior React Developer",
      "type": "Full-Time",
      "description": "We are seeking a talented Front-End Developer to join our team in Boston, MA. The ideal candidate will have strong skills in HTML, CSS, and JavaScript, with experience working with modern JavaScript frameworks such as React or Angular.",
      "location": "Boston, MA",
      "salary": "$70K - $80K",
      "company": {
        "name": "NewTek Solutions",
        "description": "NewTek Solutions is a leading technology company specializing in web development and digital solutions. We pride ourselves on delivering high-quality products and services to our clients while fostering a collaborative and innovative work environment.",
        "contactEmail": "contact@teksolutions.com",
        "contactPhone": "555-555-5555"
      }
    },
    {
      "id": "2",
      "title": "Front-End Engineer (React &amp; Redux)",
      "type": "Full-Time",
      "location": "Miami, FL",
      "description": "Join our team as a Front-End Developer in sunny Miami, FL. We are looking for a motivated individual with a passion for crafting beautiful and responsive web applications. Experience with UI/UX design principles and a strong attention to detail are highly desirable.",
      "salary": "$70K - $80K",
      "company": {
        "name": "Veneer Solutions",
        "description": "Veneer Solutions is a creative agency specializing in digital design and development. Our team is dedicated to pushing the boundaries of creativity and innovation to deliver exceptional results for our clients.",
        "contactEmail": "contact@loremipsum.com",
        "contactPhone": "555-555-5555"
      }
    },
    {
      "id": "3",
      "title": "React.js Dev",
      "type": "Full-Time",
      "location": "Brooklyn, NY",
      "description": "Are you passionate about front-end development? Join our team in vibrant Brooklyn, NY, and work on exciting projects that make a difference. We offer competitive compensation and a collaborative work environment where your ideas are valued.",
      "salary": "$70K - $80K",
      "company": {
        "name": "Dolor Cloud",
        "description": "Dolor Cloud is a leading technology company specializing in digital solutions for businesses of all sizes. With a focus on innovation and customer satisfaction, we are committed to delivering cutting-edge products and services.",
        "contactEmail": "contact@dolorsitamet.com",
        "contactPhone": "555-555-5555"
      }
    },
    {
      "id": "4",
      "title": "React Front-End Developer",
      "type": "Part-Time",
      "description": "Join our team as a Part-Time Front-End Developer in beautiful Pheonix, AZ. We are looking for a self-motivated individual with a passion for creating engaging user experiences. This position offers flexible hours and the opportunity to work remotely.",
      "location": "Pheonix, AZ",
      "salary": "$60K - $70K",
      "company": {
        "name": "Alpha Elite",
        "description": "Alpha Elite is a dynamic startup specializing in digital marketing and web development. We are committed to fostering a diverse and inclusive workplace where creativity and innovation thrive.",
        "contactEmail": "contact@adipisicingelit.com",
        "contactPhone": "555-555-5555"
      }
    },
    {
      "id": "5",
      "title": "Full Stack React Developer",
      "type": "Full-Time",
      "description": "Exciting opportunity for a Full-Time Front-End Developer in bustling Atlanta, GA. We are seeking a talented individual with a passion for building elegant and scalable web applications. Join our team and make an impact!",
      "location": "Atlanta, GA",
      "salary": "$90K - $100K",
      "company": {
        "name": "Browning Technologies",
        "description": "Browning Technologies is a rapidly growing technology company specializing in e-commerce solutions. We offer a dynamic and collaborative work environment where employees are encouraged to think creatively and innovate.",
        "contactEmail": "contact@consecteturadipisicing.com",
        "contactPhone": "555-555-5555"
      }
    },
    {
      "id": "6",
      "title": "React Native Developer",
      "type": "Full-Time",
      "description": "Join our team as a Front-End Developer in beautiful Portland, OR. We are looking for a skilled and enthusiastic individual to help us create innovative web solutions. Competitive salary and great benefits package available.",
      "location": "Portland, OR",
      "salary": "$100K - $110K",
      "company": {
        "name": "Port Solutions INC",
        "description": "Port Solutions is a leading technology company specializing in software development and digital marketing. We are committed to providing our clients with cutting-edge solutions and our employees with a supportive and rewarding work environment.",
        "contactEmail": "contact@ipsumlorem.com",
        "contactPhone": "555-555-5555"
      }
    }
  ]
}
</code></pre>



<pre class="wp-block-code"><code>// package.json
{
  "name": "react_crash_2024",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
    "server": "json-server --watch src/jobs.json --port 8000"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-icons": "^5.2.1",
    "react-router-dom": "^6.23.1"
  },
  "devDependencies": {
    "@types/react": "^18.2.66",
    "@types/react-dom": "^18.2.22",
    "@vitejs/plugin-react": "^4.2.1",
    "autoprefixer": "^10.4.19",
    "eslint": "^8.57.0",
    "eslint-plugin-react": "^7.34.1",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.6",
    "json-server": "^1.0.0-beta.0",
    "postcss": "^8.4.38",
    "tailwindcss": "^3.4.3",
    "vite": "^5.2.0"
  }
}
</code></pre>



<h2 class="wp-block-heading">useEffect() &amp; Data Fetching</h2>



<ul class="wp-block-list">
<li>修改 JobListings.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// JobListings.jsx
import { useState, useEffect } from 'react';
import JobListing from './JobListing';

const JobListings = ({ isHome = false }) =&gt; {
  const &#91;jobs, setJobs] = useState(&#91;]);
  const &#91;loading, setLoading] = useState(true);

  useEffect(() =&gt; {
    const fetchJobs = async () =&gt; {
      try {
        const res = await fetch('http://localhost:8000/jobs');
        const data = await res.json();
        setJobs(data);
      } catch (error) {
        console.log('Error fetching data', error);
      } finally {
        setLoading(false);
      }
    }

    fetchJobs();
  }, &#91;]);

  return (
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          { isHome ? 'Recent Jobs' : 'Browse Jobs' }
        &lt;/h2&gt;
        &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
          {jobs.map((job) =&gt; (
            &lt;JobListing key={job.id} job={ job } /&gt;
          ))}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default JobListings
</code></pre>



<h2 class="wp-block-heading">Loading Spinner</h2>



<ul class="wp-block-list">
<li>修改 JobListings.jsx 檔案</li>



<li>安裝 <a href="https://www.davidhu.io/react-spinners/" target="_blank" rel="noreferrer noopener">react-spinners</a> 套件<br>npm i react-spinners</li>



<li>在 components 資料夾裡面建立 Spinner.jsx 檔案</li>



<li>使用 rafce 片段快速建立程式碼<br>修改 Spinner.jsx 檔案</li>



<li>修改 JobListings.jsx 檔案，匯入 Spinner</li>
</ul>



<pre class="wp-block-code"><code>// JobListings.jsx
import { useState, useEffect } from 'react';
import JobListing from './JobListing';
import Spinner from './Spinner';

const JobListings = ({ isHome = false }) =&gt; {
  const &#91;jobs, setJobs] = useState(&#91;]);
  const &#91;loading, setLoading] = useState(true);

  useEffect(() =&gt; {
    const fetchJobs = async () =&gt; {
      const apiUrl = isHome
        ? 'http://localhost:8000/jobs?_limit=3'
        : 'http://localhost:8000/jobs';
      try {
        const res = await fetch(apiUrl);
        const data = await res.json();
        setJobs(data);
      } catch (error) {
        console.log('Error fetching data', error);
      } finally {
        setLoading(false);
      }
    }

    fetchJobs();
  }, &#91;]);

  return (
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          { isHome ? 'Recent Jobs' : 'Browse Jobs' }
        &lt;/h2&gt;
          { loading ? (
            &lt;Spinner loading={loading} /&gt;
          ) : (
          &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
            {jobs.map((job) =&gt; (
              &lt;JobListing key={job.id} job={ job } /&gt;
            ))}
          &lt;/div&gt;
        )}
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default JobListings
</code></pre>



<pre class="wp-block-code"><code>// Spinner.jsx
import { ClipLoader } from "react-spinners";

const override = {
  display: 'block',
  margin: '100px auto'
};

const Spinner = ({ loading }) =&gt; {
  return (
    &lt;ClipLoader
      color='#4338ca'
      loading={loading}
      cssOverride={override}
      sice={150}
    /&gt;
  )
}

export default Spinner
</code></pre>



<h2 class="wp-block-heading">Proxying</h2>



<ul class="wp-block-list">
<li>修改 vite.config.js 檔案</li>



<li>修改 JobListings.jsx 檔案</li>



<li>講解 Proxying 只是其中一種方式</li>
</ul>



<pre class="wp-block-code"><code>// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: &#91;react()],
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8000/',
        changeOrigin: true,
        rewrite: (path) =&gt; path.replace(/^\/api/, ''),
      },
    },
  },
});
</code></pre>



<pre class="wp-block-code"><code>// JobListings.jsx
import { useState, useEffect } from 'react';
import JobListing from './JobListing';
import Spinner from './Spinner';

const JobListings = ({ isHome = false }) =&gt; {
  const &#91;jobs, setJobs] = useState(&#91;]);
  const &#91;loading, setLoading] = useState(true);

  useEffect(() =&gt; {
    const fetchJobs = async () =&gt; {
      const apiUrl = isHome
        ? '/api/jobs?_limit=3'
        : '/api/jobs';
      try {
        const res = await fetch(apiUrl);
        const data = await res.json();
        setJobs(data);
      } catch (error) {
        console.log('Error fetching data', error);
      } finally {
        setLoading(false);
      }
    }

    fetchJobs();
  }, &#91;]);

  return (
    &lt;section className="bg-blue-50 px-4 py-10"&gt;
      &lt;div className="container-xl lg:container m-auto"&gt;
        &lt;h2 className="text-3xl font-bold text-indigo-500 mb-6 text-center"&gt;
          { isHome ? 'Recent Jobs' : 'Browse Jobs' }
        &lt;/h2&gt;
          { loading ? (
            &lt;Spinner loading={loading} /&gt;
          ) : (
          &lt;div className="grid grid-cols-1 md:grid-cols-3 gap-6"&gt;
            {jobs.map((job) =&gt; (
              &lt;JobListing key={job.id} job={ job } /&gt;
            ))}
          &lt;/div&gt;
        )}
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default JobListings
</code></pre>



<h2 class="wp-block-heading">Single Job Page</h2>



<ul class="wp-block-list">
<li>在 pages 資料夾裡面建立 JobPage.jsx 檔案</li>



<li>使用 rafce 片段快速建立程式碼<br>修改 JobPage.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>



<li>修改 JobListing.jsx 檔案</li>



<li>修改 JobPage.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// JobPage.jsx
import { useState, useEffect } from 'react';

const JobPage = () =&gt; {
  const &#91;job, setJob] = useState(null);

  useEffect(() =&gt; {
    const fetchJob = async () =&gt; {
      try {
        const res = await fetch('/api/job');
        const data = await res.json();
        setJobs(data);
      } catch (error) {
        console.log('Error fetching data', error);
      } finally {
        setLoading(false);
      }
    }

    fetchJob();
  }, &#91;])

  return (
    &lt;div&gt;JobPage&lt;/div&gt;
  )
}

export default JobPage
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';
import NotFoundPage from './pages/NotFoundPage';
import JobPage from './pages/JobPage';

const router = createBrowserRouter(
  createRoutesFromElements(
  &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
    &lt;Route index element={&lt;HomePage /&gt;} /&gt;
    &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
    &lt;Route path='/jobs/:id' element={&lt;JobPage /&gt;} /&gt;
    &lt;Route path='*' element={&lt;NotFoundPage /&gt;} /&gt;
  &lt;/Route&gt;
  )
);

const App = () =&gt; {
  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// JobListing.jsx
import { useState } from "react";
import { FaMapMarker } from 'react-icons/fa';
import { Link } from 'react-router-dom';

const JobListing = ({ job }) =&gt; {
  const &#91;showFullDescription, setShowFullDescription] = useState(false);

  let description = job.description;

  if(!showFullDescription) {
    description = description.substring(0, 90) + '...';
  }

  return (
    &lt;div className="bg-white rounded-xl shadow-md relative"&gt;
      &lt;div className="p-4"&gt;
        &lt;div className="mb-6"&gt;
          &lt;div className="text-gray-600 my-2"&gt;{job.type}&lt;/div&gt;
          &lt;h3 className="text-xl font-bold"&gt;{job.title}&lt;/h3&gt;
        &lt;/div&gt;

        &lt;div className="mb-5"&gt;
          {description}
        &lt;/div&gt;

        &lt;button onClick={() =&gt; setShowFullDescription((prevState) =&gt; !prevState)} className="text-indigo-500 mb-5 hover:text-indigo-600"&gt;
          { showFullDescription ? 'Less' : 'More' }
        &lt;/button&gt;

        &lt;h3 className="text-indigo-500 mb-2"&gt;{job.salary} / Year&lt;/h3&gt;

        &lt;div className="border border-gray-100 mb-5"&gt;&lt;/div&gt;

        &lt;div className="flex flex-col lg:flex-row justify-between mb-4"&gt;
          &lt;div className="text-orange-700 mb-3"&gt;
            &lt;FaMapMarker className="inline text-lg mb-1 mr-1" /&gt;
            {job.location}
          &lt;/div&gt;
          &lt;Link
            to={`/jobs/${job.id}`}
            className="h-&#91;36px] bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg text-center text-sm"
          &gt;
            Read More
          &lt;/Link&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default JobListing
</code></pre>



<h2 class="wp-block-heading">useParams() to Get ID</h2>



<ul class="wp-block-list">
<li>修改 JobPage.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// JobPage.jsx
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import Spinner from '../components/Spinner';

const JobPage = () =&gt; {
  const { id } = useParams();
  const &#91;job, setJob] = useState(null);
  const &#91;loading, setLoading] = useState(true);

  useEffect(() =&gt; {
    const fetchJob = async () =&gt; {
      try {
        const res = await fetch(`/api/jobs/${id}`);
        const data = await res.json();
        setJob(data);
      } catch (error) {
        console.log('Error fetching data', error);
      } finally {
        setLoading(false);
      }
    }

    fetchJob();
  }, &#91;])

  return loading ? &lt;Spinner /&gt; : (
    &lt;h1&gt;{ job.title }&lt;/h1&gt;
  );
}

export default JobPage
</code></pre>



<h2 class="wp-block-heading">Data Loaders</h2>



<ul class="wp-block-list">
<li>修改 JobPage.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// JobPage.jsx
import { useParams, useLoaderData } from 'react-router-dom';

const JobPage = () =&gt; {
  const { id } = useParams();
  const job = useLoaderData();
  
  return &lt;h1&gt;{ job.title }&lt;/h1&gt;;
};

const jobLoader = async ({ params }) =&gt; {
  const res = await fetch(`/api/jobs/${params.id}`);
  const data = await res.json();
  return data;
};

export { JobPage as default, jobLoader };
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';
import NotFoundPage from './pages/NotFoundPage';
import JobPage, { jobLoader } from './pages/JobPage';

const router = createBrowserRouter(
  createRoutesFromElements(
  &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
    &lt;Route index element={&lt;HomePage /&gt;} /&gt;
    &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
    &lt;Route path='/jobs/:id' element={&lt;JobPage /&gt;} loader={jobLoader} /&gt;
    &lt;Route path='*' element={&lt;NotFoundPage /&gt;} /&gt;
  &lt;/Route&gt;
  )
);

const App = () =&gt; {
  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<h2 class="wp-block-heading">Single Job Output</h2>



<ul class="wp-block-list">
<li>到 _theme_files/job.html 檔案複製程式碼</li>



<li>修改 JobPage.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// JobPage.jsx
import { useParams, useLoaderData } from 'react-router-dom';
import { FaArrowLeft, FaMapMarker } from 'react-icons/fa';
import { Link } from 'react-router-dom';

const JobPage = () =&gt; {
  const { id } = useParams();
  const job = useLoaderData();
  
  return (
    &lt;&gt;
      &lt;section&gt;
        &lt;div className="container m-auto py-6 px-6"&gt;
          &lt;Link
            to="/jobs"
            className="text-indigo-500 hover:text-indigo-600 flex items-center"
          &gt;
            &lt;FaArrowLeft className="mr-2" /&gt; Back to Job Listings
          &lt;/Link&gt;
        &lt;/div&gt;
      &lt;/section&gt;

      &lt;section className="bg-indigo-50"&gt;
        &lt;div className="container m-auto py-10 px-6"&gt;
          &lt;div className="grid grid-cols-1 md:grid-cols-70/30 w-full gap-6"&gt;
            &lt;main&gt;
              &lt;div
                className="bg-white p-6 rounded-lg shadow-md text-center md:text-left"
              &gt;
                &lt;div className="text-gray-500 mb-4"&gt;{job.type}&lt;/div&gt;
                &lt;h1 className="text-3xl font-bold mb-4"&gt;
                  {job.title}
                &lt;/h1&gt;
                &lt;div
                  className="text-gray-500 mb-4 flex align-middle justify-center md:justify-start"
                &gt;
                  &lt;FaMapMarker className='text-orange-700 mr-1' /&gt;
                  &lt;p className="text-orange-700"&gt;{job.location}&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;

              &lt;div className="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
                &lt;h3 className="text-indigo-800 text-lg font-bold mb-6"&gt;
                  Job Description
                &lt;/h3&gt;

                &lt;p className="mb-4"&gt;
                  {job.description}
                &lt;/p&gt;

                &lt;h3 className="text-indigo-800 text-lg font-bold mb-2"&gt;Salary&lt;/h3&gt;

                &lt;p className="mb-4"&gt;{job.salary} / Year&lt;/p&gt;
              &lt;/div&gt;
            &lt;/main&gt;

            &lt;aside&gt;
              &lt;div className="bg-white p-6 rounded-lg shadow-md"&gt;
                &lt;h3 className="text-xl font-bold mb-6"&gt;Company Info&lt;/h3&gt;

                &lt;h2 className="text-2xl"&gt;{job.company.name}&lt;/h2&gt;

                &lt;p className="my-2"&gt;
                  {job.company.description}
                &lt;/p&gt;

                &lt;hr className="my-4" /&gt;

                &lt;h3 className="text-xl"&gt;Contact Email:&lt;/h3&gt;

                &lt;p className="my-2 bg-indigo-100 p-2 font-bold"&gt;
                  {job.company.contactEmail}
                &lt;/p&gt;

                &lt;h3 className="text-xl"&gt;Contact Phone:&lt;/h3&gt;

                &lt;p className="my-2 bg-indigo-100 p-2 font-bold"&gt;{job.company.contactPhone}&lt;/p&gt;
              &lt;/div&gt;

              &lt;div className="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
                &lt;h3 className="text-xl font-bold mb-6"&gt;Manage Job&lt;/h3&gt;
                &lt;Link
                  to={`/jobs/edit/${job.id}`}
                  className="bg-indigo-500 hover:bg-indigo-600 text-white text-center font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
                  &gt;Edit Job
                &lt;/Link&gt;
                &lt;button
                  className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
                &gt;
                  Delete Job
                &lt;/button&gt;
              &lt;/div&gt;
            &lt;/aside&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/section&gt;
    &lt;/&gt;
  );
};

const jobLoader = async ({ params }) =&gt; {
  const res = await fetch(`/api/jobs/${params.id}`);
  const data = await res.json();
  return data;
};

export { JobPage as default, jobLoader };
</code></pre>



<h2 class="wp-block-heading">Add Job Page</h2>



<ul class="wp-block-list">
<li>在 pages 資料夾裡面建立 AddJobPage.jsx 檔案</li>



<li>使用 rafce 片段快速建立程式碼<br>修改 AddJobPage.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>



<li>到 _theme_files/add-job.html 檔案複製程式碼</li>
</ul>



<pre class="wp-block-code"><code>// AddJobPage.jsx
const AddJobPage = () =&gt; {
  return (
    &lt;section className="bg-indigo-50"&gt;
      &lt;div className="container m-auto max-w-2xl py-24"&gt;
        &lt;div
          className="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"
        &gt;
          &lt;form&gt;
            &lt;h2 className="text-3xl text-center font-semibold mb-6"&gt;Add Job&lt;/h2&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"
                &gt;Job Type&lt;/label
              &gt;
              &lt;select
                id="type"
                name="type"
                className="border rounded w-full py-2 px-3"
                required
              &gt;
                &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
                &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
                &lt;option value="Remote"&gt;Remote&lt;/option&gt;
                &lt;option value="Internship"&gt;Internship&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label className="block text-gray-700 font-bold mb-2"
                &gt;Job Listing Name&lt;/label
              &gt;
              &lt;input
                type="text"
                id="title"
                name="title"
                className="border rounded w-full py-2 px-3 mb-2"
                placeholder="eg. Beautiful Apartment In Miami"
                required
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="description"
                className="block text-gray-700 font-bold mb-2"
                &gt;Description&lt;/label
              &gt;
              &lt;textarea
                id="description"
                name="description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="Add any job duties, expectations, requirements, etc"
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"
                &gt;Salary&lt;/label
              &gt;
              &lt;select
                id="salary"
                name="salary"
                className="border rounded w-full py-2 px-3"
                required
              &gt;
                &lt;option value="Under $50K"&gt;Under $50K&lt;/option&gt;
                &lt;option value="$50K - 60K"&gt;$50K - $60K&lt;/option&gt;
                &lt;option value="$60K - 70K"&gt;$60K - $70K&lt;/option&gt;
                &lt;option value="$70K - 80K"&gt;$70K - $80K&lt;/option&gt;
                &lt;option value="$80K - 90K"&gt;$80K - $90K&lt;/option&gt;
                &lt;option value="$90K - 100K"&gt;$90K - $100K&lt;/option&gt;
                &lt;option value="$100K - 125K"&gt;$100K - $125K&lt;/option&gt;
                &lt;option value="$125K - 150K"&gt;$125K - $150K&lt;/option&gt;
                &lt;option value="$150K - 175K"&gt;$150K - $175K&lt;/option&gt;
                &lt;option value="$175K - 200K"&gt;$175K - $200K&lt;/option&gt;
                &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className='mb-4'&gt;
              &lt;label className='block text-gray-700 font-bold mb-2'&gt;
                Location
              &lt;/label&gt;
              &lt;input
                type='text'
                id='location'
                name='location'
                className='border rounded w-full py-2 px-3 mb-2'
                placeholder='Company Location'
                required           
              /&gt;
            &lt;/div&gt;

            &lt;h3 className="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="company" className="block text-gray-700 font-bold mb-2"
                &gt;Company Name&lt;/label
              &gt;
              &lt;input
                type="text"
                id="company"
                name="company"
                className="border rounded w-full py-2 px-3"
                placeholder="Company Name"
              /&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="company_description"
                className="block text-gray-700 font-bold mb-2"
                &gt;Company Description&lt;/label
              &gt;
              &lt;textarea
                id="company_description"
                name="company_description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="What does your company do?"
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_email"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Email&lt;/label
              &gt;
              &lt;input
                type="email"
                id="contact_email"
                name="contact_email"
                className="border rounded w-full py-2 px-3"
                placeholder="Email address for applicants"
                required
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_phone"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Phone&lt;/label
              &gt;
              &lt;input
                type="tel"
                id="contact_phone"
                name="contact_phone"
                className="border rounded w-full py-2 px-3"
                placeholder="Optional phone for applicants"
              /&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;button
                className="bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
                type="submit"
              &gt;
                Add Job
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/form&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default AddJobPage
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';
import NotFoundPage from './pages/NotFoundPage';
import JobPage, { jobLoader } from './pages/JobPage';
import AddJobPage from './pages/AddJobPage';

const router = createBrowserRouter(
  createRoutesFromElements(
  &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
    &lt;Route index element={&lt;HomePage /&gt;} /&gt;
    &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
    &lt;Route path='/add-job' element={&lt;AddJobPage /&gt;} /&gt;
    &lt;Route path='/jobs/:id' element={&lt;JobPage /&gt;} loader={jobLoader} /&gt;
    &lt;Route path='*' element={&lt;NotFoundPage /&gt;} /&gt;
  &lt;/Route&gt;
  )
);

const App = () =&gt; {
  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<h2 class="wp-block-heading">Working With Forms</h2>



<ul class="wp-block-list">
<li>修改 AddJobPage.jsx 檔案</li>



<li>使用 React Developer Tools Chrome 擴充工具</li>



<li>簡單介紹 Multiple cursor case preserve VSCode 套件工具(非必要)</li>
</ul>



<pre class="wp-block-code"><code>// AddJobPage.jsx
import { useState } from 'react';

const AddJobPage = () =&gt; {
  const &#91;title, setTitle] = useState('');
  const &#91;type, setType] = useState('');
  const &#91;location, setLocation] = useState('');
  const &#91;description, setDescription] = useState('');
  const &#91;salary, setSalary] = useState('');
  const &#91;companyName, setCompanyName] = useState('');
  const &#91;companyDescription, setCompanyDescription] = useState('');
  const &#91;contactEmail, setContactEmail] = useState('');
  const &#91;contactPhone, setContactPhone] = useState('');

  return (
    &lt;section className="bg-indigo-50"&gt;
      &lt;div className="container m-auto max-w-2xl py-24"&gt;
        &lt;div
          className="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"
        &gt;
          &lt;form&gt;
            &lt;h2 className="text-3xl text-center font-semibold mb-6"&gt;Add Job&lt;/h2&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"
                &gt;Job Type&lt;/label
              &gt;
              &lt;select
                id="type"
                name="type"
                className="border rounded w-full py-2 px-3"
                required
                value={type}
                onChange={(e) =&gt;setType(e.target.value)}
              &gt;
                &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
                &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
                &lt;option value="Remote"&gt;Remote&lt;/option&gt;
                &lt;option value="Internship"&gt;Internship&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label className="block text-gray-700 font-bold mb-2"
                &gt;Job Listing Name&lt;/label
              &gt;
              &lt;input
                type="text"
                id="title"
                name="title"
                className="border rounded w-full py-2 px-3 mb-2"
                placeholder="eg. Beautiful Apartment In Miami"
                required
                value={title}
                onChange={(e) =&gt;setTitle(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="description"
                className="block text-gray-700 font-bold mb-2"
                &gt;Description&lt;/label
              &gt;
              &lt;textarea
                id="description"
                name="description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="Add any job duties, expectations, requirements, etc"
                value={description}
                onChange={(e) =&gt;setDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"
                &gt;Salary&lt;/label
              &gt;
              &lt;select
                id="salary"
                name="salary"
                className="border rounded w-full py-2 px-3"
                required
                value={salary}
                onChange={(e) =&gt;setSalary(e.target.value)}
              &gt;
                &lt;option value="Under $50K"&gt;Under $50K&lt;/option&gt;
                &lt;option value="$50K - 60K"&gt;$50K - $60K&lt;/option&gt;
                &lt;option value="$60K - 70K"&gt;$60K - $70K&lt;/option&gt;
                &lt;option value="$70K - 80K"&gt;$70K - $80K&lt;/option&gt;
                &lt;option value="$80K - 90K"&gt;$80K - $90K&lt;/option&gt;
                &lt;option value="$90K - 100K"&gt;$90K - $100K&lt;/option&gt;
                &lt;option value="$100K - 125K"&gt;$100K - $125K&lt;/option&gt;
                &lt;option value="$125K - 150K"&gt;$125K - $150K&lt;/option&gt;
                &lt;option value="$150K - 175K"&gt;$150K - $175K&lt;/option&gt;
                &lt;option value="$175K - 200K"&gt;$175K - $200K&lt;/option&gt;
                &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className='mb-4'&gt;
              &lt;label className='block text-gray-700 font-bold mb-2'&gt;
                Location
              &lt;/label&gt;
              &lt;input
                type='text'
                id='location'
                name='location'
                className='border rounded w-full py-2 px-3 mb-2'
                placeholder='Company Location'
                required
                value={location}
                onChange={(e) =&gt;setLocation(e.target.value)}         
              /&gt;
            &lt;/div&gt;

            &lt;h3 className="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="company" className="block text-gray-700 font-bold mb-2"
                &gt;Company Name&lt;/label
              &gt;
              &lt;input
                type="text"
                id="company"
                name="company"
                className="border rounded w-full py-2 px-3"
                placeholder="Company Name"
                value={companyName}
                onChange={(e) =&gt;setCompanyName(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="company_description"
                className="block text-gray-700 font-bold mb-2"
                &gt;Company Description&lt;/label
              &gt;
              &lt;textarea
                id="company_description"
                name="company_description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="What does your company do?"
                value={companyDescription}
                onChange={(e) =&gt;setCompanyDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_email"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Email&lt;/label
              &gt;
              &lt;input
                type="email"
                id="contact_email"
                name="contact_email"
                className="border rounded w-full py-2 px-3"
                placeholder="Email address for applicants"
                required
                value={contactEmail}
                onChange={(e) =&gt;setContactEmail(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_phone"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Phone&lt;/label
              &gt;
              &lt;input
                type="tel"
                id="contact_phone"
                name="contact_phone"
                className="border rounded w-full py-2 px-3"
                placeholder="Optional phone for applicants"
                value={contactPhone}
                onChange={(e) =&gt;setContactPhone(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;button
                className="bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
                type="submit"
              &gt;
                Add Job
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/form&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default AddJobPage
</code></pre>



<h2 class="wp-block-heading">Form Submission</h2>



<ul class="wp-block-list">
<li>修改 AddJobPage.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// AddJobPage.jsx
import { useState } from 'react';

const AddJobPage = () =&gt; {
  const &#91;title, setTitle] = useState('');
  const &#91;type, setType] = useState('Full-Time');
  const &#91;location, setLocation] = useState('');
  const &#91;description, setDescription] = useState('');
  const &#91;salary, setSalary] = useState('Under $50K');
  const &#91;companyName, setCompanyName] = useState('');
  const &#91;companyDescription, setCompanyDescription] = useState('');
  const &#91;contactEmail, setContactEmail] = useState('');
  const &#91;contactPhone, setContactPhone] = useState('');

  const submitForm = (e) =&gt; {
    e.preventDefault();

    const newJob = {
      title,
      type,
      location,
      description,
      salary,
      company: {
        name: companyName,
        description: companyDescription,
        contactEmail,
        contactPhone
      }
    }

    console.log(newJob);
  }

  return (
    &lt;section className="bg-indigo-50"&gt;
      &lt;div className="container m-auto max-w-2xl py-24"&gt;
        &lt;div
          className="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"
        &gt;
          &lt;form onSubmit={submitForm}&gt;
            &lt;h2 className="text-3xl text-center font-semibold mb-6"&gt;Add Job&lt;/h2&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"
                &gt;Job Type&lt;/label
              &gt;
              &lt;select
                id="type"
                name="type"
                className="border rounded w-full py-2 px-3"
                required
                value={type}
                onChange={(e) =&gt;setType(e.target.value)}
              &gt;
                &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
                &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
                &lt;option value="Remote"&gt;Remote&lt;/option&gt;
                &lt;option value="Internship"&gt;Internship&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label className="block text-gray-700 font-bold mb-2"
                &gt;Job Listing Name&lt;/label
              &gt;
              &lt;input
                type="text"
                id="title"
                name="title"
                className="border rounded w-full py-2 px-3 mb-2"
                placeholder="eg. Beautiful Apartment In Miami"
                required
                value={title}
                onChange={(e) =&gt;setTitle(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="description"
                className="block text-gray-700 font-bold mb-2"
                &gt;Description&lt;/label
              &gt;
              &lt;textarea
                id="description"
                name="description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="Add any job duties, expectations, requirements, etc"
                value={description}
                onChange={(e) =&gt;setDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"
                &gt;Salary&lt;/label
              &gt;
              &lt;select
                id="salary"
                name="salary"
                className="border rounded w-full py-2 px-3"
                required
                value={salary}
                onChange={(e) =&gt;setSalary(e.target.value)}
              &gt;
                &lt;option value="Under $50K"&gt;Under $50K&lt;/option&gt;
                &lt;option value="$50K - 60K"&gt;$50K - $60K&lt;/option&gt;
                &lt;option value="$60K - 70K"&gt;$60K - $70K&lt;/option&gt;
                &lt;option value="$70K - 80K"&gt;$70K - $80K&lt;/option&gt;
                &lt;option value="$80K - 90K"&gt;$80K - $90K&lt;/option&gt;
                &lt;option value="$90K - 100K"&gt;$90K - $100K&lt;/option&gt;
                &lt;option value="$100K - 125K"&gt;$100K - $125K&lt;/option&gt;
                &lt;option value="$125K - 150K"&gt;$125K - $150K&lt;/option&gt;
                &lt;option value="$150K - 175K"&gt;$150K - $175K&lt;/option&gt;
                &lt;option value="$175K - 200K"&gt;$175K - $200K&lt;/option&gt;
                &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className='mb-4'&gt;
              &lt;label className='block text-gray-700 font-bold mb-2'&gt;
                Location
              &lt;/label&gt;
              &lt;input
                type='text'
                id='location'
                name='location'
                className='border rounded w-full py-2 px-3 mb-2'
                placeholder='Company Location'
                required
                value={location}
                onChange={(e) =&gt;setLocation(e.target.value)}         
              /&gt;
            &lt;/div&gt;

            &lt;h3 className="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="company" className="block text-gray-700 font-bold mb-2"
                &gt;Company Name&lt;/label
              &gt;
              &lt;input
                type="text"
                id="company"
                name="company"
                className="border rounded w-full py-2 px-3"
                placeholder="Company Name"
                value={companyName}
                onChange={(e) =&gt;setCompanyName(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="company_description"
                className="block text-gray-700 font-bold mb-2"
                &gt;Company Description&lt;/label
              &gt;
              &lt;textarea
                id="company_description"
                name="company_description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="What does your company do?"
                value={companyDescription}
                onChange={(e) =&gt;setCompanyDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_email"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Email&lt;/label
              &gt;
              &lt;input
                type="email"
                id="contact_email"
                name="contact_email"
                className="border rounded w-full py-2 px-3"
                placeholder="Email address for applicants"
                required
                value={contactEmail}
                onChange={(e) =&gt;setContactEmail(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_phone"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Phone&lt;/label
              &gt;
              &lt;input
                type="tel"
                id="contact_phone"
                name="contact_phone"
                className="border rounded w-full py-2 px-3"
                placeholder="Optional phone for applicants"
                value={contactPhone}
                onChange={(e) =&gt;setContactPhone(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;button
                className="bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
                type="submit"
              &gt;
                Add Job
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/form&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default AddJobPage
</code></pre>



<h2 class="wp-block-heading">Pass Function as Prop</h2>



<ul class="wp-block-list">
<li>修改 AddJobPage.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// AddJobPage.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const AddJobPage = ({ addJobSubmit }) =&gt; {
  const &#91;title, setTitle] = useState('');
  const &#91;type, setType] = useState('Full-Time');
  const &#91;location, setLocation] = useState('');
  const &#91;description, setDescription] = useState('');
  const &#91;salary, setSalary] = useState('Under $50K');
  const &#91;companyName, setCompanyName] = useState('');
  const &#91;companyDescription, setCompanyDescription] = useState('');
  const &#91;contactEmail, setContactEmail] = useState('');
  const &#91;contactPhone, setContactPhone] = useState('');

  const navigate = useNavigate();

  const submitForm = (e) =&gt; {
    e.preventDefault();

    const newJob = {
      title,
      type,
      location,
      description,
      salary,
      company: {
        name: companyName,
        description: companyDescription,
        contactEmail,
        contactPhone
      }
    }

    addJobSubmit(newJob);

    return navigate('/jobs');
  }

  return (
    &lt;section className="bg-indigo-50"&gt;
      &lt;div className="container m-auto max-w-2xl py-24"&gt;
        &lt;div className="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"&gt;
          &lt;form onSubmit={submitForm}&gt;
            &lt;h2 className="text-3xl text-center font-semibold mb-6"&gt;Add Job&lt;/h2&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"
                &gt;Job Type
              &lt;/label&gt;
              &lt;select
                id="type"
                name="type"
                className="border rounded w-full py-2 px-3"
                required
                value={type}
                onChange={(e) =&gt;setType(e.target.value)}
              &gt;
                &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
                &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
                &lt;option value="Remote"&gt;Remote&lt;/option&gt;
                &lt;option value="Internship"&gt;Internship&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label className="block text-gray-700 font-bold mb-2"
                &gt;Job Listing Name
              &lt;/label&gt;
              &lt;input
                type="text"
                id="title"
                name="title"
                className="border rounded w-full py-2 px-3 mb-2"
                placeholder="eg. Beautiful Apartment In Miami"
                required
                value={title}
                onChange={(e) =&gt;setTitle(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="description"
                className="block text-gray-700 font-bold mb-2"&gt;
                  Description
              &lt;/label&gt;
              &lt;textarea
                id="description"
                name="description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="Add any job duties, expectations, requirements, etc"
                value={description}
                onChange={(e) =&gt;setDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"&gt;
                Salary
              &lt;/label&gt;
              &lt;select
                id="salary"
                name="salary"
                className="border rounded w-full py-2 px-3"
                required
                value={salary}
                onChange={(e) =&gt;setSalary(e.target.value)}
              &gt;
                &lt;option value="Under $50K"&gt;Under $50K&lt;/option&gt;
                &lt;option value="$50K - 60K"&gt;$50K - $60K&lt;/option&gt;
                &lt;option value="$60K - 70K"&gt;$60K - $70K&lt;/option&gt;
                &lt;option value="$70K - 80K"&gt;$70K - $80K&lt;/option&gt;
                &lt;option value="$80K - 90K"&gt;$80K - $90K&lt;/option&gt;
                &lt;option value="$90K - 100K"&gt;$90K - $100K&lt;/option&gt;
                &lt;option value="$100K - 125K"&gt;$100K - $125K&lt;/option&gt;
                &lt;option value="$125K - 150K"&gt;$125K - $150K&lt;/option&gt;
                &lt;option value="$150K - 175K"&gt;$150K - $175K&lt;/option&gt;
                &lt;option value="$175K - 200K"&gt;$175K - $200K&lt;/option&gt;
                &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className='mb-4'&gt;
              &lt;label className='block text-gray-700 font-bold mb-2'&gt;
                Location
              &lt;/label&gt;
              &lt;input
                type='text'
                id='location'
                name='location'
                className='border rounded w-full py-2 px-3 mb-2'
                placeholder='Company Location'
                required
                value={location}
                onChange={(e) =&gt;setLocation(e.target.value)}         
              /&gt;
            &lt;/div&gt;

            &lt;h3 className="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="company" className="block text-gray-700 font-bold mb-2"
                &gt;Company Name
              &lt;/label&gt;
              &lt;input
                type="text"
                id="company"
                name="company"
                className="border rounded w-full py-2 px-3"
                placeholder="Company Name"
                value={companyName}
                onChange={(e) =&gt;setCompanyName(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="company_description"
                className="block text-gray-700 font-bold mb-2"
                &gt;Company Description&lt;/label
              &gt;
              &lt;textarea
                id="company_description"
                name="company_description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="What does your company do?"
                value={companyDescription}
                onChange={(e) =&gt;setCompanyDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_email"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Email
              &lt;/label&gt;
              &lt;input
                type="email"
                id="contact_email"
                name="contact_email"
                className="border rounded w-full py-2 px-3"
                placeholder="Email address for applicants"
                required
                value={contactEmail}
                onChange={(e) =&gt;setContactEmail(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_phone"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Phone
              &lt;/label&gt;
              &lt;input
                type="tel"
                id="contact_phone"
                name="contact_phone"
                className="border rounded w-full py-2 px-3"
                placeholder="Optional phone for applicants"
                value={contactPhone}
                onChange={(e) =&gt;setContactPhone(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;button
                className="bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
                type="submit"
              &gt;
                Add Job
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/form&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default AddJobPage
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';
import NotFoundPage from './pages/NotFoundPage';
import JobPage, { jobLoader } from './pages/JobPage';
import AddJobPage from './pages/AddJobPage';

const App = () =&gt; {
  const addJob = (newJob) =&gt; {
    console.log(newJob);
  };
  
  const router = createBrowserRouter(
    createRoutesFromElements(
    &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
      &lt;Route index element={&lt;HomePage /&gt;} /&gt;
      &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
      &lt;Route path='/add-job' element={&lt;AddJobPage addJobSubmit={addJob} /&gt;} /&gt;
      &lt;Route path='/jobs/:id' element={&lt;JobPage /&gt;} loader={jobLoader} /&gt;
      &lt;Route path='*' element={&lt;NotFoundPage /&gt;} /&gt;
    &lt;/Route&gt;
    )
  );

  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<h2 class="wp-block-heading">POST Request to Add Job</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';
import NotFoundPage from './pages/NotFoundPage';
import JobPage, { jobLoader } from './pages/JobPage';
import AddJobPage from './pages/AddJobPage';

const App = () =&gt; {
  const addJob = async (newJob) =&gt; {
    const res = await fetch('/api/jobs', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(newJob),
    });
    return;
  };
  
  const router = createBrowserRouter(
    createRoutesFromElements(
    &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
      &lt;Route index element={&lt;HomePage /&gt;} /&gt;
      &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
      &lt;Route path='/add-job' element={&lt;AddJobPage addJobSubmit={addJob} /&gt;} /&gt;
      &lt;Route path='/jobs/:id' element={&lt;JobPage /&gt;} loader={jobLoader} /&gt;
      &lt;Route path='*' element={&lt;NotFoundPage /&gt;} /&gt;
    &lt;/Route&gt;
    )
  );

  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<h2 class="wp-block-heading">Delete Job Button/function</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>



<li>修改 JobPage.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';
import NotFoundPage from './pages/NotFoundPage';
import JobPage, { jobLoader } from './pages/JobPage';
import AddJobPage from './pages/AddJobPage';

const App = () =&gt; {
  // Add New Job
  const addJob = async (newJob) =&gt; {
    const res = await fetch('/api/jobs', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(newJob),
    });
    return;
  };

  // Delete Job
  const deleteJob = async (id) =&gt; {
    console.log('delete', id);
  };
  
  const router = createBrowserRouter(
    createRoutesFromElements(
    &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
      &lt;Route index element={&lt;HomePage /&gt;} /&gt;
      &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
      &lt;Route path='/add-job' element={&lt;AddJobPage addJobSubmit={addJob} /&gt;} /&gt;
      &lt;Route path='/jobs/:id' element={&lt;JobPage deleteJob={deleteJob} /&gt;} loader={jobLoader} /&gt;
      &lt;Route path='*' element={&lt;NotFoundPage /&gt;} /&gt;
    &lt;/Route&gt;
    )
  );

  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// JobPage.jsx
import { useParams, useLoaderData, useNavigate } from 'react-router-dom';
import { FaArrowLeft, FaMapMarker } from 'react-icons/fa';
import { Link } from 'react-router-dom';

const JobPage = ({ deleteJob }) =&gt; {
  const navigate = useNavigate();
  const { id } = useParams();
  const job = useLoaderData();

  const onDeleteClick = (jobId) =&gt; {
    const confirm = window.confirm('Are you sure you want to delete this listing?');

    if (!confirm) return;

    deleteJob(jobId);

    navigate('/jobs');
  };
  
  return (
    &lt;&gt;
      &lt;section&gt;
        &lt;div className="container m-auto py-6 px-6"&gt;
          &lt;Link
            to="/jobs"
            className="text-indigo-500 hover:text-indigo-600 flex items-center"
          &gt;
            &lt;FaArrowLeft className="mr-2" /&gt; Back to Job Listings
          &lt;/Link&gt;
        &lt;/div&gt;
      &lt;/section&gt;

      &lt;section className="bg-indigo-50"&gt;
        &lt;div className="container m-auto py-10 px-6"&gt;
          &lt;div className="grid grid-cols-1 md:grid-cols-70/30 w-full gap-6"&gt;
            &lt;main&gt;
              &lt;div
                className="bg-white p-6 rounded-lg shadow-md text-center md:text-left"
              &gt;
                &lt;div className="text-gray-500 mb-4"&gt;{job.type}&lt;/div&gt;
                &lt;h1 className="text-3xl font-bold mb-4"&gt;
                  {job.title}
                &lt;/h1&gt;
                &lt;div
                  className="text-gray-500 mb-4 flex align-middle justify-center md:justify-start"
                &gt;
                  &lt;FaMapMarker className='text-orange-700 mr-1' /&gt;
                  &lt;p className="text-orange-700"&gt;{job.location}&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;

              &lt;div className="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
                &lt;h3 className="text-indigo-800 text-lg font-bold mb-6"&gt;
                  Job Description
                &lt;/h3&gt;

                &lt;p className="mb-4"&gt;
                  {job.description}
                &lt;/p&gt;

                &lt;h3 className="text-indigo-800 text-lg font-bold mb-2"&gt;Salary&lt;/h3&gt;

                &lt;p className="mb-4"&gt;{job.salary} / Year&lt;/p&gt;
              &lt;/div&gt;
            &lt;/main&gt;

            &lt;aside&gt;
              &lt;div className="bg-white p-6 rounded-lg shadow-md"&gt;
                &lt;h3 className="text-xl font-bold mb-6"&gt;Company Info&lt;/h3&gt;

                &lt;h2 className="text-2xl"&gt;{job.company.name}&lt;/h2&gt;

                &lt;p className="my-2"&gt;
                  {job.company.description}
                &lt;/p&gt;

                &lt;hr className="my-4" /&gt;

                &lt;h3 className="text-xl"&gt;Contact Email:&lt;/h3&gt;

                &lt;p className="my-2 bg-indigo-100 p-2 font-bold"&gt;
                  {job.company.contactEmail}
                &lt;/p&gt;

                &lt;h3 className="text-xl"&gt;Contact Phone:&lt;/h3&gt;

                &lt;p className="my-2 bg-indigo-100 p-2 font-bold"&gt;{job.company.contactPhone}&lt;/p&gt;
              &lt;/div&gt;

              &lt;div className="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
                &lt;h3 className="text-xl font-bold mb-6"&gt;Manage Job&lt;/h3&gt;
                &lt;Link
                  to={`/jobs/edit/${job.id}`}
                  className="bg-indigo-500 hover:bg-indigo-600 text-white text-center font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
                  &gt;Edit Job
                &lt;/Link&gt;
                &lt;button
                  onClick={ () =&gt; onDeleteClick(job.id) }
                  className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
                &gt;
                  Delete Job
                &lt;/button&gt;
              &lt;/div&gt;
            &lt;/aside&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/section&gt;
    &lt;/&gt;
  );
};

const jobLoader = async ({ params }) =&gt; {
  const res = await fetch(`/api/jobs/${params.id}`);
  const data = await res.json();
  return data;
};

export { JobPage as default, jobLoader };
</code></pre>



<h2 class="wp-block-heading">DELETE Request to Remove Job</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';
import NotFoundPage from './pages/NotFoundPage';
import JobPage, { jobLoader } from './pages/JobPage';
import AddJobPage from './pages/AddJobPage';

const App = () =&gt; {
  // Add New Job
  const addJob = async (newJob) =&gt; {
    const res = await fetch('/api/jobs', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(newJob),
    });
    return;
  };

  // Delete Job
  const deleteJob = async (id) =&gt; {
    const res = await fetch(`/api/jobs/${id}`, {
      method: 'DELETE',
    });
    return;
  };
  
  const router = createBrowserRouter(
    createRoutesFromElements(
    &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
      &lt;Route index element={&lt;HomePage /&gt;} /&gt;
      &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
      &lt;Route path='/add-job' element={&lt;AddJobPage addJobSubmit={addJob} /&gt;} /&gt;
      &lt;Route path='/jobs/:id' element={&lt;JobPage deleteJob={deleteJob} /&gt;} loader={jobLoader} /&gt;
      &lt;Route path='*' element={&lt;NotFoundPage /&gt;} /&gt;
    &lt;/Route&gt;
    )
  );

  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<h2 class="wp-block-heading">React Toastify Package</h2>



<ul class="wp-block-list">
<li>npm 套件安裝<br>npm i react-toastify</li>



<li>修改 MainLayout.jsx 檔案</li>



<li>修改 JobPage.jsx 檔案</li>



<li>修改 AddJobPage.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// MainLayout.jsx
import { Outlet } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/ReactToastify.css';
import Navbar from '../components/Navbar';

const MainLayout = () =&gt; {
  return (
    &lt;&gt;
      &lt;Navbar /&gt;
      &lt;Outlet /&gt;
      &lt;ToastContainer /&gt;
    &lt;/&gt;
  )
}

export default MainLayout
</code></pre>



<pre class="wp-block-code"><code>// JobPage.jsx
import { useParams, useLoaderData, useNavigate } from 'react-router-dom';
import { FaArrowLeft, FaMapMarker } from 'react-icons/fa';
import { Link } from 'react-router-dom';
import { toast } from 'react-toastify';

const JobPage = ({ deleteJob }) =&gt; {
  const navigate = useNavigate();
  const { id } = useParams();
  const job = useLoaderData();

  const onDeleteClick = (jobId) =&gt; {
    const confirm = window.confirm('Are you sure you want to delete this listing?');

    if (!confirm) return;

    deleteJob(jobId);

    toast.success('Job deleted successfully');

    navigate('/jobs');
  };
  
  return (
    &lt;&gt;
      &lt;section&gt;
        &lt;div className="container m-auto py-6 px-6"&gt;
          &lt;Link
            to="/jobs"
            className="text-indigo-500 hover:text-indigo-600 flex items-center"
          &gt;
            &lt;FaArrowLeft className="mr-2" /&gt; Back to Job Listings
          &lt;/Link&gt;
        &lt;/div&gt;
      &lt;/section&gt;

      &lt;section className="bg-indigo-50"&gt;
        &lt;div className="container m-auto py-10 px-6"&gt;
          &lt;div className="grid grid-cols-1 md:grid-cols-70/30 w-full gap-6"&gt;
            &lt;main&gt;
              &lt;div
                className="bg-white p-6 rounded-lg shadow-md text-center md:text-left"
              &gt;
                &lt;div className="text-gray-500 mb-4"&gt;{job.type}&lt;/div&gt;
                &lt;h1 className="text-3xl font-bold mb-4"&gt;
                  {job.title}
                &lt;/h1&gt;
                &lt;div
                  className="text-gray-500 mb-4 flex align-middle justify-center md:justify-start"
                &gt;
                  &lt;FaMapMarker className='text-orange-700 mr-1' /&gt;
                  &lt;p className="text-orange-700"&gt;{job.location}&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;

              &lt;div className="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
                &lt;h3 className="text-indigo-800 text-lg font-bold mb-6"&gt;
                  Job Description
                &lt;/h3&gt;

                &lt;p className="mb-4"&gt;
                  {job.description}
                &lt;/p&gt;

                &lt;h3 className="text-indigo-800 text-lg font-bold mb-2"&gt;Salary&lt;/h3&gt;

                &lt;p className="mb-4"&gt;{job.salary} / Year&lt;/p&gt;
              &lt;/div&gt;
            &lt;/main&gt;

            &lt;aside&gt;
              &lt;div className="bg-white p-6 rounded-lg shadow-md"&gt;
                &lt;h3 className="text-xl font-bold mb-6"&gt;Company Info&lt;/h3&gt;

                &lt;h2 className="text-2xl"&gt;{job.company.name}&lt;/h2&gt;

                &lt;p className="my-2"&gt;
                  {job.company.description}
                &lt;/p&gt;

                &lt;hr className="my-4" /&gt;

                &lt;h3 className="text-xl"&gt;Contact Email:&lt;/h3&gt;

                &lt;p className="my-2 bg-indigo-100 p-2 font-bold"&gt;
                  {job.company.contactEmail}
                &lt;/p&gt;

                &lt;h3 className="text-xl"&gt;Contact Phone:&lt;/h3&gt;

                &lt;p className="my-2 bg-indigo-100 p-2 font-bold"&gt;{job.company.contactPhone}&lt;/p&gt;
              &lt;/div&gt;

              &lt;div className="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
                &lt;h3 className="text-xl font-bold mb-6"&gt;Manage Job&lt;/h3&gt;
                &lt;Link
                  to={`/jobs/edit/${job.id}`}
                  className="bg-indigo-500 hover:bg-indigo-600 text-white text-center font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
                  &gt;Edit Job
                &lt;/Link&gt;
                &lt;button
                  onClick={ () =&gt; onDeleteClick(job.id) }
                  className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
                &gt;
                  Delete Job
                &lt;/button&gt;
              &lt;/div&gt;
            &lt;/aside&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/section&gt;
    &lt;/&gt;
  );
};

const jobLoader = async ({ params }) =&gt; {
  const res = await fetch(`/api/jobs/${params.id}`);
  const data = await res.json();
  return data;
};

export { JobPage as default, jobLoader };
</code></pre>



<pre class="wp-block-code"><code>// AddJobPage.jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';

const AddJobPage = ({ addJobSubmit }) =&gt; {
  const &#91;title, setTitle] = useState('');
  const &#91;type, setType] = useState('Full-Time');
  const &#91;location, setLocation] = useState('');
  const &#91;description, setDescription] = useState('');
  const &#91;salary, setSalary] = useState('Under $50K');
  const &#91;companyName, setCompanyName] = useState('');
  const &#91;companyDescription, setCompanyDescription] = useState('');
  const &#91;contactEmail, setContactEmail] = useState('');
  const &#91;contactPhone, setContactPhone] = useState('');

  const navigate = useNavigate();

  const submitForm = (e) =&gt; {
    e.preventDefault();

    const newJob = {
      title,
      type,
      location,
      description,
      salary,
      company: {
        name: companyName,
        description: companyDescription,
        contactEmail,
        contactPhone
      }
    }

    addJobSubmit(newJob);

    toast.success('Job Added Successfully');

    return navigate('/jobs');
  }

  return (
    &lt;section className="bg-indigo-50"&gt;
      &lt;div className="container m-auto max-w-2xl py-24"&gt;
        &lt;div className="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"&gt;
          &lt;form onSubmit={submitForm}&gt;
            &lt;h2 className="text-3xl text-center font-semibold mb-6"&gt;Add Job&lt;/h2&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"
                &gt;Job Type
              &lt;/label&gt;
              &lt;select
                id="type"
                name="type"
                className="border rounded w-full py-2 px-3"
                required
                value={type}
                onChange={(e) =&gt;setType(e.target.value)}
              &gt;
                &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
                &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
                &lt;option value="Remote"&gt;Remote&lt;/option&gt;
                &lt;option value="Internship"&gt;Internship&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label className="block text-gray-700 font-bold mb-2"
                &gt;Job Listing Name
              &lt;/label&gt;
              &lt;input
                type="text"
                id="title"
                name="title"
                className="border rounded w-full py-2 px-3 mb-2"
                placeholder="eg. Beautiful Apartment In Miami"
                required
                value={title}
                onChange={(e) =&gt;setTitle(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="description"
                className="block text-gray-700 font-bold mb-2"&gt;
                  Description
              &lt;/label&gt;
              &lt;textarea
                id="description"
                name="description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="Add any job duties, expectations, requirements, etc"
                value={description}
                onChange={(e) =&gt;setDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"&gt;
                Salary
              &lt;/label&gt;
              &lt;select
                id="salary"
                name="salary"
                className="border rounded w-full py-2 px-3"
                required
                value={salary}
                onChange={(e) =&gt;setSalary(e.target.value)}
              &gt;
                &lt;option value="Under $50K"&gt;Under $50K&lt;/option&gt;
                &lt;option value="$50K - 60K"&gt;$50K - $60K&lt;/option&gt;
                &lt;option value="$60K - 70K"&gt;$60K - $70K&lt;/option&gt;
                &lt;option value="$70K - 80K"&gt;$70K - $80K&lt;/option&gt;
                &lt;option value="$80K - 90K"&gt;$80K - $90K&lt;/option&gt;
                &lt;option value="$90K - 100K"&gt;$90K - $100K&lt;/option&gt;
                &lt;option value="$100K - 125K"&gt;$100K - $125K&lt;/option&gt;
                &lt;option value="$125K - 150K"&gt;$125K - $150K&lt;/option&gt;
                &lt;option value="$150K - 175K"&gt;$150K - $175K&lt;/option&gt;
                &lt;option value="$175K - 200K"&gt;$175K - $200K&lt;/option&gt;
                &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className='mb-4'&gt;
              &lt;label className='block text-gray-700 font-bold mb-2'&gt;
                Location
              &lt;/label&gt;
              &lt;input
                type='text'
                id='location'
                name='location'
                className='border rounded w-full py-2 px-3 mb-2'
                placeholder='Company Location'
                required
                value={location}
                onChange={(e) =&gt;setLocation(e.target.value)}         
              /&gt;
            &lt;/div&gt;

            &lt;h3 className="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="company" className="block text-gray-700 font-bold mb-2"
                &gt;Company Name
              &lt;/label&gt;
              &lt;input
                type="text"
                id="company"
                name="company"
                className="border rounded w-full py-2 px-3"
                placeholder="Company Name"
                value={companyName}
                onChange={(e) =&gt;setCompanyName(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="company_description"
                className="block text-gray-700 font-bold mb-2"
                &gt;Company Description&lt;/label
              &gt;
              &lt;textarea
                id="company_description"
                name="company_description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="What does your company do?"
                value={companyDescription}
                onChange={(e) =&gt;setCompanyDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_email"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Email
              &lt;/label&gt;
              &lt;input
                type="email"
                id="contact_email"
                name="contact_email"
                className="border rounded w-full py-2 px-3"
                placeholder="Email address for applicants"
                required
                value={contactEmail}
                onChange={(e) =&gt;setContactEmail(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_phone"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Phone
              &lt;/label&gt;
              &lt;input
                type="tel"
                id="contact_phone"
                name="contact_phone"
                className="border rounded w-full py-2 px-3"
                placeholder="Optional phone for applicants"
                value={contactPhone}
                onChange={(e) =&gt;setContactPhone(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;button
                className="bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
                type="submit"
              &gt;
                Add Job
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/form&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default AddJobPage
</code></pre>



<h2 class="wp-block-heading">Edit Job Page/Form</h2>



<ul class="wp-block-list">
<li>在 pages 資料夾裡面建立 EditJobPage.jsx 檔案</li>



<li>使用 rafce 片段快速建立程式碼<br>修改 EditJobPage.jsx 檔案</li>



<li>修改 App.jsx 檔案</li>



<li>修改 JobPage.jsx 檔案</li>



<li>修改 EditJobPage.jsx 檔案</li>



<li>複製 AddJobPage.jsx 程式碼 &lt;section> 部分、useState(”) 部分<br>修改 EditJobPage.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// EditJobPage.jsx
import { useState } from 'react';
import { useParams, useLoaderData, useNavigate } from 'react-router-dom';

const EditJobPage = () =&gt; {
  const job = useLoaderData();
  const &#91;title, setTitle] = useState(job.title);
  const &#91;type, setType] = useState(job.type);
  const &#91;location, setLocation] = useState(job.location);
  const &#91;description, setDescription] = useState(job.description);
  const &#91;salary, setSalary] = useState(job.salary);
  const &#91;companyName, setCompanyName] = useState(job.company.name);
  const &#91;companyDescription, setCompanyDescription] = useState(job.company.description);
  const &#91;contactEmail, setContactEmail] = useState(job.company.contactEmail);
  const &#91;contactPhone, setContactPhone] = useState(job.company.contactPhone);

  const submitForm = (e) =&gt; {};

  return (
    &lt;section className="bg-indigo-50"&gt;
      &lt;div className="container m-auto max-w-2xl py-24"&gt;
        &lt;div className="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"&gt;
          &lt;form onSubmit={submitForm}&gt;
            &lt;h2 className="text-3xl text-center font-semibold mb-6"&gt;Add Job&lt;/h2&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"
                &gt;Job Type
              &lt;/label&gt;
              &lt;select
                id="type"
                name="type"
                className="border rounded w-full py-2 px-3"
                required
                value={type}
                onChange={(e) =&gt;setType(e.target.value)}
              &gt;
                &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
                &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
                &lt;option value="Remote"&gt;Remote&lt;/option&gt;
                &lt;option value="Internship"&gt;Internship&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label className="block text-gray-700 font-bold mb-2"
                &gt;Job Listing Name
              &lt;/label&gt;
              &lt;input
                type="text"
                id="title"
                name="title"
                className="border rounded w-full py-2 px-3 mb-2"
                placeholder="eg. Beautiful Apartment In Miami"
                required
                value={title}
                onChange={(e) =&gt;setTitle(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="description"
                className="block text-gray-700 font-bold mb-2"&gt;
                  Description
              &lt;/label&gt;
              &lt;textarea
                id="description"
                name="description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="Add any job duties, expectations, requirements, etc"
                value={description}
                onChange={(e) =&gt;setDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"&gt;
                Salary
              &lt;/label&gt;
              &lt;select
                id="salary"
                name="salary"
                className="border rounded w-full py-2 px-3"
                required
                value={salary}
                onChange={(e) =&gt;setSalary(e.target.value)}
              &gt;
                &lt;option value="Under $50K"&gt;Under $50K&lt;/option&gt;
                &lt;option value="$50K - 60K"&gt;$50K - $60K&lt;/option&gt;
                &lt;option value="$60K - 70K"&gt;$60K - $70K&lt;/option&gt;
                &lt;option value="$70K - 80K"&gt;$70K - $80K&lt;/option&gt;
                &lt;option value="$80K - 90K"&gt;$80K - $90K&lt;/option&gt;
                &lt;option value="$90K - 100K"&gt;$90K - $100K&lt;/option&gt;
                &lt;option value="$100K - 125K"&gt;$100K - $125K&lt;/option&gt;
                &lt;option value="$125K - 150K"&gt;$125K - $150K&lt;/option&gt;
                &lt;option value="$150K - 175K"&gt;$150K - $175K&lt;/option&gt;
                &lt;option value="$175K - 200K"&gt;$175K - $200K&lt;/option&gt;
                &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className='mb-4'&gt;
              &lt;label className='block text-gray-700 font-bold mb-2'&gt;
                Location
              &lt;/label&gt;
              &lt;input
                type='text'
                id='location'
                name='location'
                className='border rounded w-full py-2 px-3 mb-2'
                placeholder='Company Location'
                required
                value={location}
                onChange={(e) =&gt;setLocation(e.target.value)}         
              /&gt;
            &lt;/div&gt;

            &lt;h3 className="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="company" className="block text-gray-700 font-bold mb-2"
                &gt;Company Name
              &lt;/label&gt;
              &lt;input
                type="text"
                id="company"
                name="company"
                className="border rounded w-full py-2 px-3"
                placeholder="Company Name"
                value={companyName}
                onChange={(e) =&gt;setCompanyName(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="company_description"
                className="block text-gray-700 font-bold mb-2"
                &gt;Company Description&lt;/label
              &gt;
              &lt;textarea
                id="company_description"
                name="company_description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="What does your company do?"
                value={companyDescription}
                onChange={(e) =&gt;setCompanyDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_email"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Email
              &lt;/label&gt;
              &lt;input
                type="email"
                id="contact_email"
                name="contact_email"
                className="border rounded w-full py-2 px-3"
                placeholder="Email address for applicants"
                required
                value={contactEmail}
                onChange={(e) =&gt;setContactEmail(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_phone"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Phone
              &lt;/label&gt;
              &lt;input
                type="tel"
                id="contact_phone"
                name="contact_phone"
                className="border rounded w-full py-2 px-3"
                placeholder="Optional phone for applicants"
                value={contactPhone}
                onChange={(e) =&gt;setContactPhone(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;button
                className="bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
                type="submit"
              &gt;
                Add Job
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/form&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default EditJobPage
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';
import NotFoundPage from './pages/NotFoundPage';
import JobPage, { jobLoader } from './pages/JobPage';
import AddJobPage from './pages/AddJobPage';
import EditJobPage from './pages/EditJobPage';

const App = () =&gt; {
  // Add New Job
  const addJob = async (newJob) =&gt; {
    const res = await fetch('/api/jobs', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(newJob),
    });
    return;
  };

  // Delete Job
  const deleteJob = async (id) =&gt; {
    const res = await fetch(`/api/jobs/${id}`, {
      method: 'DELETE',
    });
    return;
  };
  
  const router = createBrowserRouter(
    createRoutesFromElements(
    &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
      &lt;Route index element={&lt;HomePage /&gt;} /&gt;
      &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
      &lt;Route path='/add-job' element={&lt;AddJobPage addJobSubmit={addJob} /&gt;} /&gt;
      &lt;Route path='/edit-job/:id' element={&lt;EditJobPage /&gt;} loader={jobLoader} /&gt;
      &lt;Route path='/jobs/:id' element={&lt;JobPage deleteJob={deleteJob} /&gt;} loader={jobLoader} /&gt;
      &lt;Route path='*' element={&lt;NotFoundPage /&gt;} /&gt;
    &lt;/Route&gt;
    )
  );

  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// JobPage.jsx
import { useParams, useLoaderData, useNavigate } from 'react-router-dom';
import { FaArrowLeft, FaMapMarker } from 'react-icons/fa';
import { Link } from 'react-router-dom';
import { toast } from 'react-toastify';

const JobPage = ({ deleteJob }) =&gt; {
  const navigate = useNavigate();
  const { id } = useParams();
  const job = useLoaderData();

  const onDeleteClick = (jobId) =&gt; {
    const confirm = window.confirm('Are you sure you want to delete this listing?');

    if (!confirm) return;

    deleteJob(jobId);

    toast.success('Job deleted successfully');

    navigate('/jobs');
  };
  
  return (
    &lt;&gt;
      &lt;section&gt;
        &lt;div className="container m-auto py-6 px-6"&gt;
          &lt;Link
            to="/jobs"
            className="text-indigo-500 hover:text-indigo-600 flex items-center"
          &gt;
            &lt;FaArrowLeft className="mr-2" /&gt; Back to Job Listings
          &lt;/Link&gt;
        &lt;/div&gt;
      &lt;/section&gt;

      &lt;section className="bg-indigo-50"&gt;
        &lt;div className="container m-auto py-10 px-6"&gt;
          &lt;div className="grid grid-cols-1 md:grid-cols-70/30 w-full gap-6"&gt;
            &lt;main&gt;
              &lt;div
                className="bg-white p-6 rounded-lg shadow-md text-center md:text-left"
              &gt;
                &lt;div className="text-gray-500 mb-4"&gt;{job.type}&lt;/div&gt;
                &lt;h1 className="text-3xl font-bold mb-4"&gt;
                  {job.title}
                &lt;/h1&gt;
                &lt;div
                  className="text-gray-500 mb-4 flex align-middle justify-center md:justify-start"
                &gt;
                  &lt;FaMapMarker className='text-orange-700 mr-1' /&gt;
                  &lt;p className="text-orange-700"&gt;{job.location}&lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;

              &lt;div className="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
                &lt;h3 className="text-indigo-800 text-lg font-bold mb-6"&gt;
                  Job Description
                &lt;/h3&gt;

                &lt;p className="mb-4"&gt;
                  {job.description}
                &lt;/p&gt;

                &lt;h3 className="text-indigo-800 text-lg font-bold mb-2"&gt;Salary&lt;/h3&gt;

                &lt;p className="mb-4"&gt;{job.salary} / Year&lt;/p&gt;
              &lt;/div&gt;
            &lt;/main&gt;

            &lt;aside&gt;
              &lt;div className="bg-white p-6 rounded-lg shadow-md"&gt;
                &lt;h3 className="text-xl font-bold mb-6"&gt;Company Info&lt;/h3&gt;

                &lt;h2 className="text-2xl"&gt;{job.company.name}&lt;/h2&gt;

                &lt;p className="my-2"&gt;
                  {job.company.description}
                &lt;/p&gt;

                &lt;hr className="my-4" /&gt;

                &lt;h3 className="text-xl"&gt;Contact Email:&lt;/h3&gt;

                &lt;p className="my-2 bg-indigo-100 p-2 font-bold"&gt;
                  {job.company.contactEmail}
                &lt;/p&gt;

                &lt;h3 className="text-xl"&gt;Contact Phone:&lt;/h3&gt;

                &lt;p className="my-2 bg-indigo-100 p-2 font-bold"&gt;{job.company.contactPhone}&lt;/p&gt;
              &lt;/div&gt;

              &lt;div className="bg-white p-6 rounded-lg shadow-md mt-6"&gt;
                &lt;h3 className="text-xl font-bold mb-6"&gt;Manage Job&lt;/h3&gt;
                &lt;Link
                  to={`/edit-job/${job.id}`}
                  className="bg-indigo-500 hover:bg-indigo-600 text-white text-center font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
                  &gt;Edit Job
                &lt;/Link&gt;
                &lt;button
                  onClick={ () =&gt; onDeleteClick(job.id) }
                  className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline mt-4 block"
                &gt;
                  Delete Job
                &lt;/button&gt;
              &lt;/div&gt;
            &lt;/aside&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/section&gt;
    &lt;/&gt;
  );
};

const jobLoader = async ({ params }) =&gt; {
  const res = await fetch(`/api/jobs/${params.id}`);
  const data = await res.json();
  return data;
};

export { JobPage as default, jobLoader };
</code></pre>



<h2 class="wp-block-heading">Update Form Submission</h2>



<ul class="wp-block-list">
<li>修改 EditJobPage.jsx 檔案<br>複製 AddJobPage.jsx 檔案 submitForm 程式碼部分並做修改</li>



<li>修改 App.jsx 檔案</li>
</ul>



<pre class="wp-block-code"><code>// EditJobPage.jsx
import { useState } from 'react';
import { useLoaderData, useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';

const EditJobPage = ({ updateJobSubmit }) =&gt; {
  const job = useLoaderData();
  const &#91;title, setTitle] = useState(job.title);
  const &#91;type, setType] = useState(job.type);
  const &#91;location, setLocation] = useState(job.location);
  const &#91;description, setDescription] = useState(job.description);
  const &#91;salary, setSalary] = useState(job.salary);
  const &#91;companyName, setCompanyName] = useState(job.company.name);
  const &#91;companyDescription, setCompanyDescription] = useState(job.company.description);
  const &#91;contactEmail, setContactEmail] = useState(job.company.contactEmail);
  const &#91;contactPhone, setContactPhone] = useState(job.company.contactPhone);

  const navigate = useNavigate();

  const submitForm = (e) =&gt; {
    e.preventDefault();

    const updatedJob = {
      id,
      title,
      type,
      location,
      description,
      salary,
      company: {
        name: companyName,
        description: companyDescription,
        contactEmail,
        contactPhone
      }
    }

    updateJobSubmit(updatedJob);

    toast.success('Job Updated Successfully');

    return navigate(`/jobs/${id}`);
  }

  return (
    &lt;section className="bg-indigo-50"&gt;
      &lt;div className="container m-auto max-w-2xl py-24"&gt;
        &lt;div className="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"&gt;
          &lt;form onSubmit={submitForm}&gt;
            &lt;h2 className="text-3xl text-center font-semibold mb-6"&gt;Add Job&lt;/h2&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"
                &gt;Job Type
              &lt;/label&gt;
              &lt;select
                id="type"
                name="type"
                className="border rounded w-full py-2 px-3"
                required
                value={type}
                onChange={(e) =&gt;setType(e.target.value)}
              &gt;
                &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
                &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
                &lt;option value="Remote"&gt;Remote&lt;/option&gt;
                &lt;option value="Internship"&gt;Internship&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label className="block text-gray-700 font-bold mb-2"
                &gt;Job Listing Name
              &lt;/label&gt;
              &lt;input
                type="text"
                id="title"
                name="title"
                className="border rounded w-full py-2 px-3 mb-2"
                placeholder="eg. Beautiful Apartment In Miami"
                required
                value={title}
                onChange={(e) =&gt;setTitle(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="description"
                className="block text-gray-700 font-bold mb-2"&gt;
                  Description
              &lt;/label&gt;
              &lt;textarea
                id="description"
                name="description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="Add any job duties, expectations, requirements, etc"
                value={description}
                onChange={(e) =&gt;setDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"&gt;
                Salary
              &lt;/label&gt;
              &lt;select
                id="salary"
                name="salary"
                className="border rounded w-full py-2 px-3"
                required
                value={salary}
                onChange={(e) =&gt;setSalary(e.target.value)}
              &gt;
                &lt;option value="Under $50K"&gt;Under $50K&lt;/option&gt;
                &lt;option value="$50K - 60K"&gt;$50K - $60K&lt;/option&gt;
                &lt;option value="$60K - 70K"&gt;$60K - $70K&lt;/option&gt;
                &lt;option value="$70K - 80K"&gt;$70K - $80K&lt;/option&gt;
                &lt;option value="$80K - 90K"&gt;$80K - $90K&lt;/option&gt;
                &lt;option value="$90K - 100K"&gt;$90K - $100K&lt;/option&gt;
                &lt;option value="$100K - 125K"&gt;$100K - $125K&lt;/option&gt;
                &lt;option value="$125K - 150K"&gt;$125K - $150K&lt;/option&gt;
                &lt;option value="$150K - 175K"&gt;$150K - $175K&lt;/option&gt;
                &lt;option value="$175K - 200K"&gt;$175K - $200K&lt;/option&gt;
                &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className='mb-4'&gt;
              &lt;label className='block text-gray-700 font-bold mb-2'&gt;
                Location
              &lt;/label&gt;
              &lt;input
                type='text'
                id='location'
                name='location'
                className='border rounded w-full py-2 px-3 mb-2'
                placeholder='Company Location'
                required
                value={location}
                onChange={(e) =&gt;setLocation(e.target.value)}         
              /&gt;
            &lt;/div&gt;

            &lt;h3 className="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="company" className="block text-gray-700 font-bold mb-2"
                &gt;Company Name
              &lt;/label&gt;
              &lt;input
                type="text"
                id="company"
                name="company"
                className="border rounded w-full py-2 px-3"
                placeholder="Company Name"
                value={companyName}
                onChange={(e) =&gt;setCompanyName(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="company_description"
                className="block text-gray-700 font-bold mb-2"
                &gt;Company Description&lt;/label
              &gt;
              &lt;textarea
                id="company_description"
                name="company_description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="What does your company do?"
                value={companyDescription}
                onChange={(e) =&gt;setCompanyDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_email"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Email
              &lt;/label&gt;
              &lt;input
                type="email"
                id="contact_email"
                name="contact_email"
                className="border rounded w-full py-2 px-3"
                placeholder="Email address for applicants"
                required
                value={contactEmail}
                onChange={(e) =&gt;setContactEmail(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_phone"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Phone
              &lt;/label&gt;
              &lt;input
                type="tel"
                id="contact_phone"
                name="contact_phone"
                className="border rounded w-full py-2 px-3"
                placeholder="Optional phone for applicants"
                value={contactPhone}
                onChange={(e) =&gt;setContactPhone(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;button
                className="bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
                type="submit"
              &gt;
                Add Job
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/form&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default EditJobPage
</code></pre>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';
import NotFoundPage from './pages/NotFoundPage';
import JobPage, { jobLoader } from './pages/JobPage';
import AddJobPage from './pages/AddJobPage';
import EditJobPage from './pages/EditJobPage';

const App = () =&gt; {
  // Add New Job
  const addJob = async (newJob) =&gt; {
    const res = await fetch('/api/jobs', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(newJob),
    });
    return;
  };

  // Delete Job
  const deleteJob = async (id) =&gt; {
    const res = await fetch(`/api/jobs/${id}`, {
      method: 'DELETE',
    });
    return;
  };

  // Update Job
  const updateJob = async () =&gt; {
    
  }
  
  const router = createBrowserRouter(
    createRoutesFromElements(
    &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
      &lt;Route index element={&lt;HomePage /&gt;} /&gt;
      &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
      &lt;Route path='/add-job' element={&lt;AddJobPage addJobSubmit={addJob} /&gt;} /&gt;
      &lt;Route path='/edit-job/:id' element={&lt;EditJobPage updateJobSubmit={updateJob} /&gt;} loader={jobLoader} /&gt;
      &lt;Route path='/jobs/:id' element={&lt;JobPage deleteJob={deleteJob} /&gt;} loader={jobLoader} /&gt;
      &lt;Route path='*' element={&lt;NotFoundPage /&gt;} /&gt;
    &lt;/Route&gt;
    )
  );

  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<h2 class="wp-block-heading">PUT Request to Update Job</h2>



<ul class="wp-block-list">
<li>修改 App.jsx 檔案</li>



<li>修改 EditJobPage.jsx 檔案</li>



<li>簡單提到 authentication</li>
</ul>



<pre class="wp-block-code"><code>// App.jsx
import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  RouterProvider
} from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import JobsPage from './pages/JobsPage';
import NotFoundPage from './pages/NotFoundPage';
import JobPage, { jobLoader } from './pages/JobPage';
import AddJobPage from './pages/AddJobPage';
import EditJobPage from './pages/EditJobPage';

const App = () =&gt; {
  // Add New Job
  const addJob = async (newJob) =&gt; {
    const res = await fetch('/api/jobs', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(newJob),
    });
    return;
  };

  // Delete Job
  const deleteJob = async (id) =&gt; {
    const res = await fetch(`/api/jobs/${id}`, {
      method: 'DELETE',
    });
    return;
  };

  // Update Job
  const updateJob = async (job) =&gt; {
    const res = await fetch(`/api/jobs/${job.id}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(job),
    });
    return;
  }
  
  const router = createBrowserRouter(
    createRoutesFromElements(
    &lt;Route path='/' element={&lt;MainLayout /&gt;}&gt;
      &lt;Route index element={&lt;HomePage /&gt;} /&gt;
      &lt;Route path='/jobs' element={&lt;JobsPage /&gt;} /&gt;
      &lt;Route path='/add-job' element={&lt;AddJobPage addJobSubmit={addJob} /&gt;} /&gt;
      &lt;Route path='/edit-job/:id' element={&lt;EditJobPage updateJobSubmit={updateJob} /&gt;} loader={jobLoader} /&gt;
      &lt;Route path='/jobs/:id' element={&lt;JobPage deleteJob={deleteJob} /&gt;} loader={jobLoader} /&gt;
      &lt;Route path='*' element={&lt;NotFoundPage /&gt;} /&gt;
    &lt;/Route&gt;
    )
  );

  return &lt;RouterProvider router={router} /&gt;;
}

export default App
</code></pre>



<pre class="wp-block-code"><code>// EditJobPage.jsx
import { useState } from 'react';
import { useParams, useLoaderData, useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';

const EditJobPage = ({ updateJobSubmit }) =&gt; {
  const job = useLoaderData();
  const &#91;title, setTitle] = useState(job.title);
  const &#91;type, setType] = useState(job.type);
  const &#91;location, setLocation] = useState(job.location);
  const &#91;description, setDescription] = useState(job.description);
  const &#91;salary, setSalary] = useState(job.salary);
  const &#91;companyName, setCompanyName] = useState(job.company.name);
  const &#91;companyDescription, setCompanyDescription] = useState(job.company.description);
  const &#91;contactEmail, setContactEmail] = useState(job.company.contactEmail);
  const &#91;contactPhone, setContactPhone] = useState(job.company.contactPhone);

  const navigate = useNavigate();
  const { id } = useParams();

  const submitForm = (e) =&gt; {
    e.preventDefault();

    const updatedJob = {
      id,
      title,
      type,
      location,
      description,
      salary,
      company: {
        name: companyName,
        description: companyDescription,
        contactEmail,
        contactPhone
      }
    }

    updateJobSubmit(updatedJob);

    toast.success('Job Updated Successfully');

    return navigate(`/jobs/${id}`);
  }

  return (
    &lt;section className="bg-indigo-50"&gt;
      &lt;div className="container m-auto max-w-2xl py-24"&gt;
        &lt;div className="bg-white px-6 py-8 mb-4 shadow-md rounded-md border m-4 md:m-0"&gt;
          &lt;form onSubmit={submitForm}&gt;
            &lt;h2 className="text-3xl text-center font-semibold mb-6"&gt;Update Job&lt;/h2&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"
                &gt;Job Type
              &lt;/label&gt;
              &lt;select
                id="type"
                name="type"
                className="border rounded w-full py-2 px-3"
                required
                value={type}
                onChange={(e) =&gt;setType(e.target.value)}
              &gt;
                &lt;option value="Full-Time"&gt;Full-Time&lt;/option&gt;
                &lt;option value="Part-Time"&gt;Part-Time&lt;/option&gt;
                &lt;option value="Remote"&gt;Remote&lt;/option&gt;
                &lt;option value="Internship"&gt;Internship&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label className="block text-gray-700 font-bold mb-2"
                &gt;Job Listing Name
              &lt;/label&gt;
              &lt;input
                type="text"
                id="title"
                name="title"
                className="border rounded w-full py-2 px-3 mb-2"
                placeholder="eg. Beautiful Apartment In Miami"
                required
                value={title}
                onChange={(e) =&gt;setTitle(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="description"
                className="block text-gray-700 font-bold mb-2"&gt;
                  Description
              &lt;/label&gt;
              &lt;textarea
                id="description"
                name="description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="Add any job duties, expectations, requirements, etc"
                value={description}
                onChange={(e) =&gt;setDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="type" className="block text-gray-700 font-bold mb-2"&gt;
                Salary
              &lt;/label&gt;
              &lt;select
                id="salary"
                name="salary"
                className="border rounded w-full py-2 px-3"
                required
                value={salary}
                onChange={(e) =&gt;setSalary(e.target.value)}
              &gt;
                &lt;option value="Under $50K"&gt;Under $50K&lt;/option&gt;
                &lt;option value="$50K - 60K"&gt;$50K - $60K&lt;/option&gt;
                &lt;option value="$60K - 70K"&gt;$60K - $70K&lt;/option&gt;
                &lt;option value="$70K - 80K"&gt;$70K - $80K&lt;/option&gt;
                &lt;option value="$80K - 90K"&gt;$80K - $90K&lt;/option&gt;
                &lt;option value="$90K - 100K"&gt;$90K - $100K&lt;/option&gt;
                &lt;option value="$100K - 125K"&gt;$100K - $125K&lt;/option&gt;
                &lt;option value="$125K - 150K"&gt;$125K - $150K&lt;/option&gt;
                &lt;option value="$150K - 175K"&gt;$150K - $175K&lt;/option&gt;
                &lt;option value="$175K - 200K"&gt;$175K - $200K&lt;/option&gt;
                &lt;option value="Over $200K"&gt;Over $200K&lt;/option&gt;
              &lt;/select&gt;
            &lt;/div&gt;

            &lt;div className='mb-4'&gt;
              &lt;label className='block text-gray-700 font-bold mb-2'&gt;
                Location
              &lt;/label&gt;
              &lt;input
                type='text'
                id='location'
                name='location'
                className='border rounded w-full py-2 px-3 mb-2'
                placeholder='Company Location'
                required
                value={location}
                onChange={(e) =&gt;setLocation(e.target.value)}         
              /&gt;
            &lt;/div&gt;

            &lt;h3 className="text-2xl mb-5"&gt;Company Info&lt;/h3&gt;

            &lt;div className="mb-4"&gt;
              &lt;label htmlFor="company" className="block text-gray-700 font-bold mb-2"
                &gt;Company Name
              &lt;/label&gt;
              &lt;input
                type="text"
                id="company"
                name="company"
                className="border rounded w-full py-2 px-3"
                placeholder="Company Name"
                value={companyName}
                onChange={(e) =&gt;setCompanyName(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="company_description"
                className="block text-gray-700 font-bold mb-2"
                &gt;Company Description&lt;/label
              &gt;
              &lt;textarea
                id="company_description"
                name="company_description"
                className="border rounded w-full py-2 px-3"
                rows="4"
                placeholder="What does your company do?"
                value={companyDescription}
                onChange={(e) =&gt;setCompanyDescription(e.target.value)}
              &gt;&lt;/textarea&gt;
            &lt;/div&gt;

            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_email"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Email
              &lt;/label&gt;
              &lt;input
                type="email"
                id="contact_email"
                name="contact_email"
                className="border rounded w-full py-2 px-3"
                placeholder="Email address for applicants"
                required
                value={contactEmail}
                onChange={(e) =&gt;setContactEmail(e.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div className="mb-4"&gt;
              &lt;label
                htmlFor="contact_phone"
                className="block text-gray-700 font-bold mb-2"
                &gt;Contact Phone
              &lt;/label&gt;
              &lt;input
                type="tel"
                id="contact_phone"
                name="contact_phone"
                className="border rounded w-full py-2 px-3"
                placeholder="Optional phone for applicants"
                value={contactPhone}
                onChange={(e) =&gt;setContactPhone(e.target.value)}
              /&gt;
            &lt;/div&gt;

            &lt;div&gt;
              &lt;button
                className="bg-indigo-500 hover:bg-indigo-600 text-white font-bold py-2 px-4 rounded-full w-full focus:outline-none focus:shadow-outline"
                type="submit"
              &gt;
                Update Job
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/form&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  )
}

export default EditJobPage
</code></pre>



<h2 class="wp-block-heading">Build Static Assets For Production</h2>



<ul class="wp-block-list">
<li>停止 development sever</li>



<li>使用終端機執行指令<br>npm run build 編譯，會建立 dist 資料夾</li>



<li>使用終端機執行指令<br>npm run preview</li>



<li>這是前端課程，後端資料在 deploy 無法正常運作</li>
</ul>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
